本文原作者: 李伟,原文发布于: 印象笔记
https://app.yinxiang.com/fx/0390f0f2-1770-4bdc-a3c4-d134a6bc654b
引言
在接入 Flutter 技术时,通常都会遇到原生工程和 Flutter 工程混合开发的问题,即便是新项目从一开始就采用 Flutter 开发,可能会由于部分功能 Flutter 无法实现、效果不好等原因,也会需要考虑混合开发的问题。
本文给大家介绍一套混合开发实现方式,包括工程集成打包、开发调试,Flutter 基础组件选型和技术架构方式,支持原生、H5、Flutter (或其他容器) 的跨容器互通的页面跳转和事件路由,使用 Provider 实现一套类似于原生开发的 MVVM 业务框架,以及 Flutter 侧的组件化实现。我们团队从 2019 年 8 月开始接入 Flutter,经历了 1.9.1、1.12.13、1.17.5 Flutter 版本升级,今年 6 月底已经实现 80% 的页面使用 Flutter 替代,目前在做 Flutter 侧组件化拆分和复用,实现一套代码开发 Shein、Romwe 两个 APP (Android+iOS) 4 个端的能力。
集成引入
集成方式
混合工程避不开 Flutter 和原生工程集成的问题,在 1.12 之前的 Flutter 版本中官方只正式提供源码集成的方式,Flutter 编译产物 (Android 的 AAR,iOS 的 Framework) 的集成方式还处于预览版,有一些 bug 需要自己解决,在 1.12 及之后的版本中,官方提供了比较完善的源码集成和编译产物集成方式: https://flutter.cn/docs/development/add-to-app。
源码集成方式适合 flutter 和原生代码互相调用的开发,方便开发调试插件,尤其是 Android 端可以在一个 Android Studio IDE 窗口下编辑原生和 Flutter 代码,非常方便,但如果依赖的插件较多,每个插件都包含一个 Android Module 工程,工程结构会较为复杂,也会一定程度影响编译速度。
Flutter 编译产物有 3 种编译模式: debug、profile、release,debug 模式编译产物适合纯 Flutter 侧代码的开发、调试,profile 的用来做性能分析和测试,release 的用于打包发布。这和原生工程的编译配置可以一一对应,在开发、测试、发布时方便实现切换。
集成问题
官方提供的 Flutter 编译产物集成方式是在本地集成,而混合开发时,原生侧的开发是不希望依赖 Flutter 环境的,纯 Flutter 侧的开发也希望能有一个编译好的 debug 版本的 APP,然后使用 Attach 命令连上开发机即可进行开发 (支持 hot reload),测试和发布阶段也希望有一个统一的持续集成环境,需要考虑原生工程依赖 Flutter 编译产物的版本问题,比如提交了 Flutter 代码到 Git 仓库,想打包一个新版本 APP 包含刚才提交的 Flutter 代码,是否能不用手动修改依赖的 Flutter 产物版本号即可方便的打包。另外,还有多版本并行的问题,Git 分支如何支持等等。
Git 分支
要解决这些问题,首先要考虑的是 Git 分支模式的选择,分支模式会影响到整个开发和集成发布流程,主流的 Git 分支模式如 Git-Flow、GitHub-Flow、GitLab-Flow 等,这些模式如 Git-Flow 对于 APP 版本迭代来说过于复杂,GitHub-Flow 则不能很好的支持 APP 多版本并行等特点,而 GitLab-Flow 对于大型 APP 团队较适合,对于中小型 APP 研发团队 2-3 周一个版本的迭代来说,有一定的成本和复杂性,具体选择需要根据团队规模、工程结构、业务特点来决定。我们团队使用的是类似于 GitHub-Flow 的版本分支模式,只不过把 feature branch 当成了版本分支来用 (如 v1.2.3),master 只作为版本记录用。版本分支可以有多个并行,当一个版本开始时,从 master 上拉取最新的代码创建版本分支,版本开发、修改 bug、发布都在版本分支上完成,发布后再合并到 master 分支和其他正在进行的版本分支,如下图:
混合工程至少会有两个工程,一个原生工程、一个 Flutter 工程,所以两个工程都使用相同的版本分支,一一对应,这样做的好处是本地源码集成或远端 Flutter 产物集成时,切换版本都会比较简单方便。本地源码集成切换版本只需要同时切换两个工程的 Git 分支,而远端 Flutter 产物集成可以根据 APP 的版本参数,依赖不同版本的 Flutter 产物文件夹目录。
集成打包
在开发同学提交代码到 Git 仓库时,可以由 Git 服务推送通知 Jenkins 打包服务开始打包,也可以由 Jenkins 打包服务手动或定时触发打包,首先需要拉取原生工程和 Flutter 工程相同版本分支的代码,可以使用 Jenkins 的 Multiple SCMs plugin,这个插件支持拉取多个 Git 仓库源。更新到对应版本分支的最新代码后,就可以开始打包了,打包过程主要有两个阶段,编译 Flutter 产物阶段和编译 APP 安装包阶段。
编译 Flutter 产物阶段大致步骤如下:
清理 Flutter 工程编译输出目录
执行 flutter pub upgrade 更新依赖的 flutter 组件
执行 flutter build aar (Android) 或 flutter build ios-framework (iOS) 编译 Flutter 产物
上传 Flutter 产物到 FTP 服务器 (或任意支持 http 下载服务)
更新原生工程依赖的 Flutter 产物版本号 (这里依赖的是第四步上传到 FTP 服务上的产物)
第五步 iOS 端:
需要在构建脚本中更新原生工程 Podfile 依赖的 Flutter 产物版本号,并提交更改到 Git 仓库。
pod 'flutter_module', ‘1.2.3.1001’
可采用 4 段式的版本号命名,如 "1.2.3" 是正式的版本号 (也是 git 分支名),第四段 "1001" 是 build 号,每次构建 +1。
第五步 Android 端:
可以使用 changing 标记实现 sync/clean 时自动拉取最新 aar,而不必每次修改版本号,如下图是上传到 FTP 服务器的 AAR 产物文件夹目录 (按照版本划分)
1、依赖远端 AAR:
repositories {
maven {
// 工程的versionName和版本分支名称一致,和Flutter产物版本文件夹名称也一致,ps:1.2.3
url 'http://<host>/flutter_module_android/' + rootProject.ext.versionName + '/repo/'
// 依赖本地工程编译产物,方便开发调试
// url '../../flutter_module/build/host/outputs/repo'
}
maven {
url 'https://storage.googleapis.com/download.flutter.io'
}
}
2、依赖的 flutter aar 标记 changing = true:
dependencies {
debugImplementation ('com.example.flutter_module:flutter_debug:1.0’) { changing = true }
profileImplementation ('com.example.flutter_module:flutter_profile:1.0’) { changing = true }
releaseImplementation ('com.example.flutter_module:flutter_release:1.0’) { changing = true }
}
3、修改 aar 缓存策略:
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
将 Flutter 编译产物上传到 FTP 服务,就可以开始原生工程的编译打包过程,介绍原生打包的文章比较多,这里不再赘述,原生打包可以根据需要实现 debug、profile、release 等不同编译模式的包。Flutter 侧开发可以直接使用 Jenkins 打出的 debug 包进行开发调试,原生侧开发可以依赖 FTP 服务上的 Flutter 编译产物开发,不需要 Flutter 开发环境。多版本并行时,Jenkins 也可以支持不同版本的打包和发布。
开发环境
为了保持一致的开发环境,方便团队开发,可以把 flutter sdk 以 Git submodule 形式集成在 Flutter Module 工程内,可以使用 Flutter Wrapper (https://github.com/passsy/flutter_wrapper) 工具方便的管理 sdk 版本,这样做的好处是 sdk 版本和工程代码是关联的,当升级 sdk 时,意味着这个工程依赖的 sdk 升级,并且通过 Git 同步给整个团队。
基础组件
UI 组件: Material vs Cupertino
众所周知,Flutter 提供了 Material 和 Cupertino 两套 UI 风格组件,而国内很多 APP 设计风格是偏向于 Cupertino 风格的,似乎我们要使用 Cupertino 组件来构建页面 UI,但如果这么做,很快就会发现 Cupertino 组件相比 Material 组件实在太少了,而且一些基础 Widgets 组件在 CupertinoApp 下的默认样式很难看,需要做额外的处理。
那么,是否可以使用 Material 组件,并统一修改风格为我们需要的样式?答案是可以的。
风格差异较大的地方主要是这几点:
水波纹效果
AppBar、TabBar 等组件的高度
加载指示器
Navigator 跳转页面过渡动画
第一点: 水波纹效果,可以通过如下代码去除
MaterialApp(
theme: ThemeData(
// 禁用水波纹效果
highlightColor: Colors.transparent,
splashFactory: const NoSplashFactory(),
primaryColor: Colors.white,
),
)
class NoSplashFactory extends InteractiveInkFeatureFactory {
const NoSplashFactory();
InteractiveInkFeature create({
@required MaterialInkController controller,
@required RenderBox referenceBox,
@required Offset position,
@required Color color,
TextDirection textDirection,
bool containedInkWell: false,
RectCallback rectCallback,
BorderRadius borderRadius,
ShapeBorder customBorder,
double radius,
VoidCallback onRemoved,
}) {
return new NoSplash(
controller: controller,
referenceBox: referenceBox,
color: color,
onRemoved: onRemoved,
);
}
}
class NoSplash extends InteractiveInkFeature {
NoSplash({
@required MaterialInkController controller,
@required RenderBox referenceBox,
Color color,
VoidCallback onRemoved,
}) : assert(controller != null),
assert(referenceBox != null),
super(
controller: controller,
referenceBox: referenceBox,
onRemoved: onRemoved) {
controller.addInkFeature(this);
}
@override
void paintFeature(Canvas canvas, Matrix4 transform) {}
}
第二点: AppBar、TabBar 等组件的高度,可以通过继承 AppBar、TabBar,覆盖 preferredSize 解决
第三点: 加载指示器,可自行实现自定义效果的 LoadingWidget
第四点: Navigator 跳转页面过渡动画,可以通过继承 PageRoute 自定义实现
而 AppBar 的返回按钮 Icon、标题居左 (Android)/居中 (iOS) 的差异,可能是我们需要的。若要统一,也可以自定义实现。
混合页面栈
混合开发的 APP,页面堆栈的管理一直是比较麻烦的问题,由于 APP 的主体仍然是原生框架,原生和跨平台页面 (H5/RN/Flutter) 会出现交叉堆叠的场景,所以需要解决如页面跳转、返回动画,页面生命周期一致性等问题。比较成熟的混合技术如原生 +H5 混合页面,每打开一个 H5 页面都是用一个单独的 WebView 容器承载,优点是实现简单,缺点是 H5 页面之间不能直接访问数据,需要原生桥接,多个 WebView 实例消耗内存也较多。
Flutter 官方最初给出的解决方案与原生 +H5 混合是类似的,把 FlutterView 当成一个容器视图,并且优化了一些场景,当连续打开两个 Flutter 页面时,第二个 Flutter 页面可以在当前 FlutterView 中打开。由于刚开始使用 Flutter 技术,原生和跨平台页面出现交叉堆叠的场景较多,能够使用优化的场景就比较少了。Flutter 1.12 版本中实现了复用引擎的方案,实际使用需要处理很多细节问题,较为繁琐。
闲鱼团队开源的 FlutterBoost (https://github.com/alibaba/flutter_boost) 帮我们解决了这个问题,FlutterBoost 将 Flutter 页面一一映射到原生页面,从原生角度来看,就是原生页面之间的跳转,如 Android 端的跳转: NativeActivity —> FlutterActivity(FlutterView) —> FlutterActivity(FlutterView),页面栈的管理就交给原生框架了。而所有 FlutterView 所关联的 FlutterEngine 复用同一个,可以减少内存使用,也能正常实现页面/模块之间的数据通信。
跨容器路由: 页面路由 & 事件路由
原生侧的页面跳转已有较多成熟的页面路由组件可以实现,通过给每一个页面定义一个 URI 来实现导航。但从原生的视角来看,WebView/Flutter 等技术实现的跨平台页面,相当于一个独立的容器空间,实际场景中会存在大量原生和跨平台页面之间相互跳转,甚至是相互发送广播事件,所以我们需要实现的是一个跨原生、Webview、Flutter 空间的页面路由和事件路由组件。
由于 FlutterBoost 将每个 Flutter 页面都映射为一个独立打开的FlutterActivity(Android)/FlutterViewController(iOS),这和所有的 H5 页面通常都由一个公共的 WebViewActivity/WebViewController 打开类似,需要将所有由 Flutter 实现的页面 URI 都指向 FlutterActivity/FlutterViewController,并传递原始 URI 和参数到 Flutter 容器中,再由 FlutterBoost 管理的页面 URI 路由映射表创建具体的 Flutter Widgets 页面。
实际 Flutter 开发场景可能是将已经存在的原生页面用 Flutter 技术重写并替换,考虑到可能的风险,需要实现灰度发布和降级回原生页面的能力。这显然不太可能由每个跳转的业务代码各自去判断,那么,由路由组件实现拦截并动态改变跳转的目标页面则会是比较好的实现方式。
原生页面开发中会存在一些使用事件总线 (如 EventBus) 的场景,部分原生页面转化为 Flutter 页面后,也会需要接收到原生的事件,这可以通过在 Flutter 中实现 EventBus 代理类,并桥接原生的 EventBus 实现。但这样只是桥接了原生和 Flutter 的事件总线,还不是事件路由。
页面路由通过定义一个特定 URI (如: /module/abc),可以导航页面并传递参数。如果我们把页面路由的概念推广一下,一个事件也定义一个特定的 URI (如:/event/login),那么是否也可以使用路由投递事件并传递参数?答案当然是可以的,并且还是和页面路由使用完全一致的 API。这样做有什么好处呢?如果路由只是在 APP 内部使用,确实没有什么用处,但如果路由是 APP、H5、服务端一起使用时,则可以实现巨大的灵活性。试想一下,如果所有支持页面路由的地方,还可以无缝支持事件路由,那么原本可能需要 APP 修改并发版本的功能,现在只需要 H5 或服务端修改一下路由链接,即可实现 H5 或服务端向 APP 发送事件!!(前提是 APP 中已经实现接收并处理该事件的代码)
事件路由具体的实现方式是,在路由组件中拦截所有路由,判断 URI 是否是 "/event/" 开头,如果是,则将当前事件使用 EventBus 发送出去,并终止路由跳转即可。
图片加载
APP 中显示的图片有如下这几个来源:
内置在安装包中的图片
网络加载的图片
手机本地存储的图片 (如照片、图片缓存)
这 3 种来源的图片 Flutter 官方已经帮我们实现了加载到内存并显示,但还有一些细节问题没有解决:
内置的图片,原生和 Flutter 不能共用同一套,需要分别放在各自工程目录下;
网络加载的图片,没有本地磁盘缓存,一些第三方图片加载库 (如cached_network_image) 虽然有缓存,但不能和原生图片加载库共用同一个图片缓存,需要分别下载;
同一张图片如果原生侧和 Flutter 侧都加载到内存,需要双倍的内存。
Flutter 社区对于这些问题涌现了各种解决方案,但也还存在一些瑕疵,我们目前也还在探索中,就目前而言,也有一些方案可以暂时使用。
内置的图片,原生工程和 Flutter 工程可以分别都放置一份,因为安装包是经过压缩的,所以并不会增加安装包的体积,只会增加安装到手机后的存储空间 (需要解压),目前手机的存储空间都比较大,这个影响还可以接受。官方虽然提供了原生侧读取 Flutter 图片的方法 (https://flutter.dev/docs/development/ui/assets-and-images#sharing-assets-with-the-underlying-platform),但这种共用方式需要改变原生加载图片的方式,不太方便。有第三方插件 (https://flutter.dev/docs/development/ui/assets-and-images#loading-ios-images-in-flutter) 可以支持 Flutter 读取原生资源,只支持 iOS 端,Android 理论上也可以实现,但需要考虑 Android 和 iOS 资源一致性的问题,需要的话可以研究一下。
网络图片缓存问题,可以通过自定义 ImageProvider,将图片加载桥接给原生的图片加载库,加载完成后返回本地缓存路径给 Flutter 侧,再通过 Image.file (File file, ...) 加载到内存中显示。
双倍的内存的问题,闲鱼团队分享了一个巧妙的方案 (https://mp.weixin.qq.com/s/98BxqW5QDHXLKMwHX_E7EQ),通过外接纹理解决图片内存复用问题,这个方案目前没有开源,似乎还存在一些问题。在我们实际业务中,分析发现,出现重复图片的场景主要是列表页 Item 的图片和商品详情头图,以及点击查看大图,但列表页 Item 的图片是较小的图片,和商品详情的头部大图并不是同一个图片文件,所以真正相同图片的场景并不多。而且图片在内存中真正需要显示的也不多,大部分都是内存缓存。所以,如果将图片缓存大小控制在一定范围,并且,对于出现重复图片的场景,要么将这两个页面全都迁移到 Flutter,要么就都保持原生页面,则可以暂时规避这个问题。
另外还有一个问题是,Flutter 工程内置的图片要放置多套图片的问题,如放置 1x 图在工程目录的 images/cat.png 路径,并在 images/2x/cat.png和images/3x/cat.png 路径分别放置 2x 和 3x 图片,使用时可以不指定密度,直接使用: Image.asset("images/cat.png")。虽然使用方便,但这样会增加安装包体积,实际我们可能只会放置两套图,甚至只放一套 3x 图,但如果省略掉 1x 的默认图,图片无法显示,而如果把 3x 图放在 1x 图的默认路径下,显示出来的图片会非常大,需要手动指定图片大小。
官方文档 (https://api.flutter.dev/flutter/widgets/Image/Image.asset.html) 中对于省略部分密度的图片有说明,虽然也有一些繁琐,不是很完美
"The images/cat.png image can be omitted from disk (though it must still be present in the manifest). If it is omitted, then on a device with a 1.0 device pixel ratio, the images/2x/cat.png image would be used instead."
意思是可以省略 images/cat.png 图片文件,但是必须在 pubspec.yaml 的 assets 中配置完整的图片路径 (不能只配置 "images" 目录),并且每一张图片都需要在 pubspec.yaml 中声明图片路径
assets:
- images/cat.png
Flutter 编译打包时,似乎只会自动识别 1x 的默认图和 pubspec.yaml 中明确配置的图片路径,对于 2x、3x 等其他变体资源不会自动识别。每个图片资源都需要配置会比较繁琐,不过,可以通过实现一个插件自动配置图片路径到 pubspec.yaml 中。
网络、埋点、数据
混合开发的 APP,原生侧通常已经有一套完善的网络、埋点、数据存储等基础组件了,而且这些组件中还会包含一些公共的业务逻辑处理,这些基础组件如果使用 Dart 语言重新编写一遍,工作量会很大,稳定性也不高,需要时间慢慢完善,后续还需要维护多套代码 (Android、iOS、Flutter) 等问题。
通过桥接原生侧来实现这些非 UI 层的基础组件,可能是一个比较好的选择。网络和埋点组件桥接起来比较简单,做好数据的解析和映射就可以了,但数据桥接会比较麻烦。原生侧的持久化数据一般会有数据库存储、序列化存储、键值对存储等持久化存储方式,这些持久化数据通常有统一的访问接口,桥接不难,但可能存在多线程并发读写的情况,也会需要在某一侧更新了数据后,通知另一侧更新数据或刷新页面的场景,而且,还有很多只会临时存储在内存中的业务数据,这些数据分散在各个业务模块中,各自负责管理和更新。
对于数据的桥接,持久化存储的数据,如果有统一的访问接口,并且不存在并发场景时,可以统一桥接底层持久化存储 API。如果有并发场景或只会临时存储在内存中的数据,则可以在各个业务模块中各自实现一个单例模式的 Manager,原生侧的 Manager 负责真正的管理和维护数据,Flutter 侧的 Manager 只是一个代理类,通过 Flutter MethodChannel 桥接实现双向通讯,Flutter 侧可以主动调用方法获取数据,原生侧数据有变化也可以主动通知到 Flutter 侧。
架构
MVVM
Flutter 社区中,大家似乎不怎么谈论 MVVM 框架,而是喜欢说状态管理,而状态管理的框架又特别多,如 Scoped Model、Provide、Provider、MobX、Redux、Fish Redux 等等。这些框架无一例外都基于 Flutter 内建的 InheritedWidget 组件提供的数据共享和传递机制,也就是模型数据的变化,会自动刷新 UI,再加上 Flutter 声明式 UI 的写法,就可以很轻松的构建 MVVM 中由数据到 UI 的绑定,而 UI 接收用户触摸事件到模型的绑定就简单了,只需要在 UI 触摸回调事件中获取到 Model,并调用对应方法处理即可,这样就构建好了 MVVM 业务层开发模式。相比原生的 MVC、MVP 等模式要简单不少,而原生开发即便使用 MVVM 模式,也需要额外实现模型和 UI 的双向绑定,像 Android 端虽然有官方提供的 DataBinding 自动生成绑定代码,但也不如声明式 UI 这样简洁。
但这些状态管理框架,要么比较重型,使用较为复杂,要么虽然简单 (如 Scoped Model、Provider),但似乎只适合小型应用开发,其提供的 Demo (https://github.com/2d-inc/developer_quest) 中只有一个顶层的单一数据源,对于大型应用来说,几百个数据源都写在顶层,维护起来可能是个灾难。
页面级数据源
官方推荐的 Provider 框架,虽然 Demo 中是顶层单一数据源,但为何一定要这样写呢?我们在原生开发中不是每个页面、模块独立管理各自的数据吗?在 Flutter 中也完全可以实现以页面/模块为单位来管理和维护数据,我们只需要 Provider 提供的数据刷新机制就可以了。
对于不需要跨页面共享的数据,其数据对象实例是定义在 Model 中的一个个字段,并在页面的根 Widget 下配置数据源。对于需要跨页面共享的数据,原生开发通常是将其存储在具有长生命周期的单例类或服务中,Flutter 也可以使用相同的方法,在所有需要使用共享数据的页面,其 Model 通过单例类或服务获取数据,并在页面的根 Widget 下配置数据源即可。
不配置在顶层的原因是,共享数据有时候难以界定,是只要有两个页面共用同一个数据就算共享数据呢,还是需要多个不同业务都使用的数据才算共享数据,具体又如何界定?可能在大型团队里面,每个人都有不同的看法,难以统一。即便统一或团队都认可的界定方式,实际上可能还是有很多共享数据,让维护变得困难。
熟悉 Android 开发的同学应该看出来了,这和 Android 官方的应用架构 (https://developer.android.google.cn/jetpack/docs/guide) 几乎一致,只是在数据层桥接了原生的数据存储、网络组件,实际开发中也可以根据业务情况决定是否需要 Repository 层来提供统一的数据获取接口。
组件化
原生开发中,当业务规模和团队规模越来越大时,为了更好的多团队协作开发,就有必要做组件化拆分,Flutter 开发也一样。对于我们团队还有一个更重要的原因是,我们需要维护 Shein 和 Romwe 两个 APP,业务逻辑非常相似,但 UI 风格不同,为了减少维护工作,提高开发效率,我们希望能将每个功能模块拆分成组件,集成时打包不同的资源,实现复用一套代码支持两个 APP 开发的能力。但如果再加上跨平台技术,就可以实现一套代码支持 4 个端的能力。
组件集成
Flutter 的 pub 集成工具提供了本地代码集成和远程 Git 库集成,具备了组件化改造的基础,在工程的 pubspec.yaml 文件中配置依赖:
#远端集成
xxx_module:
git:
url: https://github.com/xxx/xxx_module
ref: 1.2.3 #版本分支集成,若有更新需要执行[flutter pub upgrade]拉取最新更新
#本地集成
#xxx_module:
# path: ../xxx_module
默认使用远端集成,当需要修改某个组件时,将远端集成改为本地集成即可。另外,也可以使用 Git submodule 的方式集成。
资源问题
Flutter 提供的构建工具没有 Android Gradle 那么强大的功能,无法实现像 Gradle 的 productFlavors 提供的构建变体,所以我们只能自己在打包脚本中实现打包时选择不同的资源。具体实现是,在每个业务工程中,除了有默认的 res/ 资源目录,再创建一个变体资源目录 (如 res_romwe/ ),目录结构和文件名称同默认资源保持一致。相同的资源只需要放置一份在 res 目录中,不同的资源可以再放一份在变体资源目录中,在打包时,只需要将变体资源目录复制并覆盖默认资源目录,再执行正常的编译流程。
图片资源拆分到业务组件工程后,再使用 Image.asset('images/cat.png’) 方式显示内置图片时,发现图片无法显示。这是因为资源和代码一样,都有包隔离的特性,否则,不同的库中资源名相同就会出现冲突或覆盖了 (Android 采用的是优先级覆盖)。当我们需要调用 Flutter SDK 或第三方库中的代码时,需要 import 相应代码文件 (如 import 'package:flutter/material.dart’),同理,加载子工程中的资源也需要指定 package: Image.asset("images/cat.png", package: "module_name"),即便是在子工程中的代码加载自己工程下的资源也是一样。具体可以参考 Flutter 文档: https://api.flutter.dev/flutter/painting/AssetImage-class.html。
组件通信
Flutter 组件间通信方式,除了页面路由和事件总线,还需要有调用其他组件服务的能力,实现方式和原生组件类似:
业务工程可拆分为 Business module 和 Lib module,应先下沉业务组件,这些组件可以提供给其他业务工程复用,也可以提供接口给其他业务调用,接口具体实现在 Business module 中。Lib module 即能实现模块间通讯,又提供了业务工程和 Basic 工程的中间层,避免 Basic 工程的过度膨胀。接口通信优点是实现简单,适合实现一组复杂的数据访问接口,表达强依赖关系,缺点是需要定义和拆分较多的 module 工程,有一定成本。
组件架构
由于混合开发需要一个 Module 类型的 Flutter 工程来实现 Flutter 部分的编译构建,Flutter 侧组件化拆分后,两个 APP 集成各自需要的 Flutter 业务组件,需要两个 Module 类型的 Flutter 工程,如 shein_flutter_module 和 romwe_flutter_module。另外,Flutter 侧有很多桥接原生侧的基础组件,为了避免两个 APP 各自桥接,就需要原生侧能剥离出统一的 Basic 工程,实际上就是原生侧的组件化,最终的组件架构如下:
总结
与纯原生开发或纯 Flutter 开发相比,混合开发由于需要打通原生和 Flutter 的数据和服务,需要有大量桥接实现,各个模块互相协作也需要考虑各种异常或降级的情况,因此需要从整体架构上设计一套完善的跨平台混合开发框架,我们团队还在继续探索和实践。本文分享的一些实践方法欢迎各位同学讨论和交流,谢谢!
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"