导读:Flutter 支持以独立页面、甚至是 UI 片段的方式,集成到现有的应用中,即所谓的混合开发模式。本文主要谈谈 Android 平台下, Flutter 的混合开发与构建。
文|李成达 网易云信资深移动端开发工程师
Flutter 作为 Google 开源的新一代跨平台、高性能 UI 框架,旨在帮助开发者高效地构建出跨平台的、UI 与交互体验一致的精美应用,推出后一直倍受开发者的青睐。
当需要开发一个全新的应用时,我们可以很方便地从零开始,完全使用 Flutter 进行开发。但如果是针对一个现有的应用,需要引入 Flutter 技术,显然使用 Flutter 全部重写一遍是不现实的。幸运的是,Flutter 很好地支持了以独立页面、甚至是 UI 片段的方式集成到现有的应用中,即所谓的混合开发模式。本文主要从一个 Android 开发的视角,谈谈 Android 平台下, Flutter 的混合开发与构建。
Hello Flutter
相信现在应该很少会有移动端开发者不知道 Flutter,这里不再做过多介绍。对于这门技术,使用过的应该绝大多数都会说好;没用过的推荐尝试一下,跑个 Demo 体验体验,有可能它就是你需要学习和掌握的最后一门新技术了。回过头来,Flutter 究竟有什么独特的魅力让它能从一众技术中脱颖而出呢?总结一下,主要有以下几点:
-
跨平台:可以做到一套代码完美适配 Android、iOS 平台,未来还会覆盖更多平台,大大节省了开发人力与维护成本,同时拥有出色的跨端 UI 表现一致性。
-
高效开发:SDK 提供了丰富的 UI 组件,开箱即用;声明式的 UI 构建方式,大大减少出错率;Debug 模式提供热重载能力,可实时预览代码变更,不需要重新编译安装。
-
高性能:采用自建渲染引擎,独立于系统并可单独优化;区别于 RN、WEEX,没有中间层转换的额外开销;Release 模式下代码编译为 AOT 指令,运行高效。
受益于以上的核心优势,Flutter 推出后圈了很多移动开发者的粉,各互联网大厂也纷纷将其作为一项基础技术进行研究。在 Flutter 初期,其应用场景主要是从 0 构建一个全新 App,对混合开发的支持很不友好。但作为一门跨平台的技术框架,到底还是需要依赖原生平台提供的诸多系统能力,此外还有众多现存原生 App 跃跃欲试,因此在这个需求背景下,混合开发的支持与完善至今已发展得越来越好,下面我们就用一个简单的示例开始 Android 端的 Flutter 混合开发与构建之旅。
引入 Flutter 模块
要在一个已有的 Android Project 中使用 Flutter,需要引入一个 Flutter Module。在 Android Studio(需要确保 Flutter 插件已经成功安装并启用)中打开现有 Android 工程,通过使用 File > New > New Module… 菜单,我们可以新创建一个 Flutter 模块或是导入一个外部的 Flutter 模块。
这里以最简单的 Android App 项目为例,导入 Flutter 模块。在 Flutter 模块导入成功之后,原工程文件、结构都会发生一些变化,主要有:
-
settings.gradle 文件新增了以下内容。其实就是执行对应 Flutter 模块下 .android/include_flutter.groovy 脚本文件,该步骤会引入一个名为 Flutter 的 Android Library Module,同时还会引入 Flutter 模块所依赖的所有插件。
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')
-
项目结构变化,如下图所示:
在引入 Flutter 模块之前,项目中仅有 app 一个 Module;而在引入之后,可以看到除了原有的 app Module 外,Flutter Gradle 插件自动引入了额外几个子 Module:
-
flutter_module:指代要引入的目标 Flutter Module,不会 apply Android 相关的任何插件,主要是包含 Flutter 相关源码、资源、依赖等。
-
flutter:为 Flutter Gradle 插件引入的 Android Library Module;主要负责编译 flutter_module 及其依赖的第三方 Package、Plugin 的 Dart 代码,以及打包 Flutter 资源等。
-
device_info:为 Flutter Gradle 插件自动引入的 Flutter Android Plugin Library Module,这是因为一开始我在 flutter_module 的 pubspec.yaml 文件中添加了对 device_info 这个插件的依赖。Flutter Gradle 工具会将 flutter_module 依赖到的所有插件其 Android 平台侧的代码、资源作为一个 Library Module 引入到项目中一起参与构建。如果要查看 flutter_module 引入了哪些 Plugin,可以查看其对应目录下的 .flutter-plugins 与 .flutter-plugins-dependencies 文件,这两个文件是执行 flutter pub get 时生成的,记录了插件的本地文件目录、依赖信息等。
注意:一个工程不能包含多个 Flutter Module,最多只能引入一个,这是由 Flutter 的 Gradle 插件决定的。
使用 Flutter
完成 Flutter 模块的引入后,我们再来看看如何使用 Flutter。
添加依赖
首先需要在 App 模块的build.gradle脚本文件中添加对Flutter工程的依赖,只有这样 Flutter 模块才会参与到整个应用的构建中来,我们也才能够在 App 模块中调用到 Flutter 提供的 Java 层 API。如下所示:
dependencies {
implementation project(':flutter')
}
运行 Flutter 页面
我们可以选择使用 Activity、Fragment 或者 View 来承载 Flutter 的 UI,这里主要介绍前面两种方式,并假设flutter_module中已经通过runApp方法渲染了一个widget。
-
运行 Flutter Activity。使用io.flutter.embedding.android.FlutterActivity类可以很方便的启动一个 Flutter Activity,当然我们也可以继承它并扩展自己的逻辑。示例代码如下:
FlutterActivity
.withNewEngine()
.build(context)
.also { startActivity(it) }
-
运行 Flutter Fragment。可以使用FlutterFragmentActivity或者FlutterFragment来添加 Flutter UI 片段:a. 使用FlutterFragmentActivity可以自动创建并添加一个FlutterFragment;b. 手动创建FlutterFragment后添加到目标 Activity 中。示例代码如下:
val flutterFragment = FlutterFragment.withNewEngine()
.dartEntrypoint(getDartEntrypointFunctionName())
.initialRoute(getInitialRoute())
.appBundlePath(getAppBundlePath())
.flutterShellArgs(FlutterShellArgs.fromIntent(intent))
.handleDeeplinking(shouldHandleDeeplinking())
.renderMode(renderMode)
.transparencyMode(transparencyMode)
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
.build<FlutterFragment>()
fragmentManager
.beginTransaction()
.add(
FRAGMENT_CONTAINER_ID,
flutterFragment,
TAG_FLUTTER_FRAGMENT
)
.commit()
-
平台层和 Flutter 层通信。不论是开发 Plugin 还是业务逻辑,平台层与 Flutter 层通信是必不可少的,为此就需要使用到MethodChannel。平台层通过MethodChannel请求调用 Flutter 层 API 时,数据在经过打包编码后,通过 JNI、DartVM 传到 Flutter 层解码后使用;待结果计算完成后,又会重新打包编码,经过 DartVM、JNI 传回到 Native 层;同理,在 Flutter 层请求调用平台层的 API 时,数据处理是一致的,只是流转方向相反。通过这种方式,平台层与 Flutter 层就建立了一个双向的、异步的通信通道。在下面的示例代码中,Native 层使用dev.flutter.example/counter创建一个MethodChannel,并设置 Handler 接收 Dart 的远程方法调用 incrementCounter,并调用 reportCounter 将结果回传。
channel = MethodChannel(flutterEngine.dartExecutor, "dev.flutter.example/counter")
channel.setMethodCallHandler { call, _ ->
when (call.method) {
"incrementCounter" -> {
count++
channel.invokeMethod("reportCounter", count)
}
}
}
Dart 层使用相同的名称创建 MethodChannel,并设置 Handler 处理回调结果,随后调用 incrementCounter 方法请求 counter。示例代码如下:
final _channel = MethodChannel('dev.flutter.example/counter');
_channel.setMethodCallHandler(_handleMessage);
_channel.invokeMethod('incrementCounter');
Future<dynamic> _handleMessage(MethodCall call) async {
if (call.method == 'reportCounter') {
_count = call.arguments as int;
notifyListeners();
}
}
这里我们是通过手动创建 MethodChannel 进行通信的,这在进行简单通信的场景是没问题的,但在通信接口 API 比较复杂的情况就不是很适用了。
一是繁琐,因为我们需要手写大量的打包、拆包代码;二是容易出错。这个时候就轮到 Pigeon 大显身手了。Pigeon 是一个官方推出的代码生成工具,可以生成类型安全的双向通信 API 接口,具体可以参考官方的 Example,这里不再赘述。
Pigeon :https://flutter.dev/docs/development/platform-integration/platform-channels#pigeon
Flutter APK 解析
到这里,我们已经了解了如何在现有 Android 项目中引入并使用 Flutter,接下来我们再来探究一下 Flutter APK 的结构,看看 Flutter Tools 在这个 APK 包内到底打包了哪些东西。下面两图分别为 Debub 模式和 Release 模式下构建出来的 Flutter APK 包结构,忽略了非 Flutter 相关的项。
可以看到两个模式下的 APK 结构大致相同,说明如下:
-
lib/{arch}/libflutter.so:为对应架构的 Flutter Engine 共享库,负责 Flutter 渲染、JNI 通信、DartVM。如果不需要对应架构的版本,通过 abiFilters 可以 Exclude 掉。
-
lib/{arch}/libapp.so:只存在于 Release 模式下,共享库中包含 Dart AOT 生成的二进制指令和数据。在运行时,Flutter Engine 通过 Dynamic Load 的方式,从共享库中读取对应的可执行机器指令以及数据。
-
assets/flutter_assets:Flutter 引用到的相关资源:
-
fonts:包含字体库。
-
FontManifest.json:引用到的字体库清单文件,json 格式,所有使用到的字体、以及字体文件在 flutter_assets 下的路径。
-
AssetManifest.json:其他资源清单文件,json 格式,为所有资源名称到资源路径的映射,Flutter 在加载某一项资源时,会通过这个配置清单找到对应路径的资源进行读取后加载。
-
kernel_blob.bin、isolate_snapshot_data、vm_snapshot_data:只存在于 Debug 模式下,分别为 DartVM 字节码与数据,其作用类似于 libapp.so,只是存在形式、打包方式不同。在 Debug 模式下,Flutter Tools 将指令和数据分别打包,主要是为了热重载(HotReload)服务的,而在 Release 模式下是统一打包成共享库。
-
踩过的坑
这里,也总结了几个我们在应用的时候遇到的问题,供大家参考避坑。
-
路由管理复杂:这里面包括 Flutter 层内部的页面路由管理以及 Flutter 与原生的混合栈管理。前者在 Navigator 2.0 API 中已经得到了很好的完善与支持,但后者仍面临着诸多限制与不足,需要改进。目前项目中还未涉及到后者这种很复杂的业务场景,因此对这一块的研究比较少,感兴趣的同学可以了解一下诸如 flutter_boost 此类的开源解决方案。
-
生命周期不对应:Android 的组件一般都会有自己的生命周期,Flutter 的 Widget State 也有一套自己的生命周期,但这两者其实并不是一一对应的。比如原生的 Activity 页面虽然已经被 Finish 并 Destroy 掉了,但 Flutter 层的页面并不一定会随之而被 Dispose,尤其是在使用 Cache Flutter Engine 的时候。Flutter 页面是可以脱离原生页面而存在的,它们可以被动态地 Attach 和 Detach,Attach 时会触发重新渲染,Detach 时 UI 相关的所有操作都会 Pending 直到重新被 Attach。所以在混合开发中,业务逻辑不应该过度依赖 Widget State 的一些生命周期方法,因为它们可能会被延后执行从而导致一些奇怪的 Bug。
总结
Flutter 混合开发使得开发者可以渐进式地进行 Flutter 开发与迁移,是 Flutter 寄生于原生平台至关重要的一环。
本文主要从一个 Android 开发的视角,介绍了 Flutter 混合开发的入门知识。随着 Flutter 开源项目的不断迭代与演进,混合开发的体验正在变得越来越好、性能也越来越高。但美中不足的是仍然有一些应用场景与问题并未得到很好地完善与解决,比如 Flutter 多实例问题(我们也将在下个月的另一篇文章中跟大家分享介绍我们在实践 Flutter 多实例遇到的问题与解决方案,敬请关注)。
瑕不掩瑜,Flutter 这门技术整体而言还是非常不错的,它如今仍处于快速发展的阶段,相信在 Flutter 团队与开源社区的共同努力下,未来的生态值得期待。
作者介绍
李成达,网易云信资深移动端开发工程师,热衷于研究跨平台开发技术以及工程提效,目前主要负责视频会议组件化 SDK 的相关研发工作。
延伸阅读