前言
了解一下如何在 Android 原生项目中集成 Flutter ,通过项目中打开 Flutter 页面。
添加 Flutter Module 并依赖
生成配置
在原生项目根目录执行命令
1 | flutter create -t module --org {package_name} {module_name} |
结果
1 | Creating project sub_flutter... |
最终生成了以上文件,注意这里最后自动执行了 flutter pub get 的命令。关于 flutter pub get 具体做了什么,可以参考后面的描述。
这里在项目根目录创建子 module 只是为了把代码放在一个仓库,方便维护,理论上可以放在硬盘的任何位置。
配置原生项目 settings.gradle
在配置 settings.gradle 之前先来简单回顾一下关于 Gradle 的一些基础知识。
如果你了解过 Gradle 相关的配置的话,一定会看到一个概念,就是约定优于配置,什么意思呢,按照面向对象的思路来理解,每一个工程是一个巨大的 Project 类,整个类里有很多的属性。而我们创建的每一个项目其实就是一个具体的 Project 对象(也就是实例).约定优于配置的意思,就是在 project 实例化的时候,其内部的属性已经有了默认值。那么我们怎么知道有哪些默认值呢?在项目根目录执行
1 | ./gradlew properties |
就可以得到整个 Project 的一些默认配置,比如(此处节选部分结果)
1 | ------------------------------------------------------------ |
这里当前有一些是我们配置的,比如 useAndroidX,但也有一些是约定的,比如 对于整个 project 来说 buildDir 就是项目根目录的 build 文件夹等。
执行
1 | ./gradlew :app:properties |
节选部分结果
1 | buildDir: /Users/username/Documents/mygithub/MinApp/app/build |
就会得到关于 app 整个 module 现阶段的一些配置信息,当然这些配置信息除了约定的,还有你自己配置的,比如 buildToolsVersion ,签名等相关信息。可以看到 buildDir 和整个 project 的是不一样的。
回到主题, 看看如何把我们刚才创建的 sub_flutter 模块集成到项目中。(严格来说并不是集成 sub_flutter 模块,因为他只是一个 flutter 的模块,而在 Android 主项目只能集成子 Android module,那么具体该怎么做呢,下面就来看看其中的奥秘)
按照官方的操作方法,会要求我们添加以下配置到 settings.gradle 中。
1 | // Include the host app project. |
首先看看 这里的 settingsDir 的值。在 settings.gradle 中直接添加
1 | println "settings.dir=" + settingsDir |
sync 之后就会看到输出
1 | settings.dir=/Users/username/Documents/mygithub/MinApp |
所以,上面的配置信息,就是说结合 settings 所在目录的父目录和我们配置的目录结合,找到一个名为 include_flutter.groovy 的文件,然后去执行他。
前面说了,创建子 module 的时候,可以是在项目根目录,也可以是在其他位置,如果是在其他位置,这里的 my_flutter 可以替换为你创建目录的绝对路劲。
这里是在根目录直接创建的,那么以上的配置就可以简化为
1 | setBinding(new Binding([gradle: this])) |
关于 include_flutter.groovy
上面说了,settings.gradle 的配置,其实就是去执行 include_flutter.groovy 这个文件,可以简单看一下这个文件
1 | def scriptFile = getClass().protectionDomain.codeSource.location.toURI() |
.android 其实就是一个 Android 项目,他包含一个 Flutter 文件夹,这 Flutter 是一个 library 类型的 Android module ,这个一点从他的 build.gradle 文件就可以看出。 include_flutter.groovy 所做的事情,就是将当前 library 命名为 flutter 的一个 moudle。然后检查项目中 local.properties 中 sdk 的相关配置,最后去执行 FlutterSDK 的中 gradle 脚本,这里具体的分析就不再展开了。
也就是说,现在有一个名为 flutter 的 Android Library Module 。这个 module 包含 flutter 的所有配置。我们如果依赖了这个 module ,那么就相当于是依赖了 Flutter .
依赖 flutter
最后在原生项目的 application-module 的 build.gradle 的 dependencies 闭包中添加
1 | implementation project(':flutter') |
至此,原生项目已经有了 Flutter 的依赖,可以使用 Flutter 的 UI 了。
至此,现在的原生项目就包含 Flutter SDK 的所有依赖了。 Flutter UI 相关的内容,该怎么写还是用 dart 在 main.dart 中写,然后我们就可以把这个 dart 渲染出来的内容按照 Activity 、Fragment 或 View 的形式添加到已有的项目中了。具体怎么添加,下面来看。
集成 Flutter UI 到原生项目
FlutterEngine 提前初始化并缓存
按照官方的建议以及实际体验,在这种原生项目后集成 Flutter 的场景下,需要提前预热(官方称为 pre-warm) Flutter 引擎,也就是说提前启动 Dart 的虚拟机,否则当你以其他方式添加 Flutter 的内容时再创建 Flutter 引擎,体验会非常糟糕,尤其是首次打开页面的时候。
因此,简单起见我们可以使用 IdleHandler 在应用启动后进行预热操作。(理论上这种后期集成 Flutter 的页面应该不是首页及需要 App 启动后立即显示的页面,大部分情况下至少是个二级甚至三级页面,因此这样做是合理的)
1 | fun onCreate() { |
注释 1 处的实现不是必须的,注释 2 处是默认实现。
按照上面的代码,在这个 FlutterEngine 被启动的时候,会默认执行 main.dart 中的 main 方法。DartExecutor.DartEntrypoint.createDefault()
这个默认的实现结点就是 main 方法,这点从代码了很容易窥探到。当然,我们也可以在 FlutterEngine 启动的时候指定自定义的执行结点,比如上面注释中的 fakeMain()
方法,是定义在 main.dart 中的一个 Dart 方法。(当然,这里有一个坑,后面再说)。
当然,我们也可以按注释 1 出写的那样, FlutterEngine 在启动的时候,执行指定路由对应页面的提前初始化工作。(当然,这里还是有坑的,后面统一说)
打开 FlutterActivity 或 FlutterFragment
有了缓存好的 FlutterEngine,我们就可以用它打开 FlutterActivity 和 FlutterFragment 了。
1 | viewBinding.flutterActivity.setOnClickListener { |
FlutterActivity 和 FlutterFragment 都是 Flutter SDK 内封装好的,便于在原生项目中集成 Flutter 的实现。当然,你也可以使用 FlutterView ,但是比较复杂,具体可以参考官方指导 。
这样上面打开的 Activity 或添加的 Fragment 就会默认展示 main.dart 中配置的 Widget 所对应的 UI。
FlutterFragment 已经被 add 到当前页面的 FrameLayout 中了。
一些坑
这里说一下这种做法的一些坑。
官方是建议提前缓存 FlutterEngine ,但是按照实际体验与其说是建议,不如说是必须做的事情,否则打开页面的体验真的只能用非常卡来形容了。
关于 pre-warmed FlutterEngine 的一些细节。
Note: To warm up a FlutterEngine, you must execute a Dart entrypoint. Keep in mind that the moment executeDartEntrypoint() is invoked, your Dart entrypoint method begins executing. If your Dart entrypoint invokes runApp() to run a Flutter app, then your Flutter app behaves as if it were running in a window of zero size until this FlutterEngine is attached to a FlutterActivity, FlutterFragment, or FlutterView. Make sure that your app behaves appropriately between the time you warm it up and the time you display Flutter content.
按照这段描述的意思,提前创建 FlutterEngine 并不只是创建了一个执行环境,而是在提前执行代码,类似于用一个 1 像素的 Activity 敲敲执行一些后台逻辑。因此,但是我们的 Flutter 代码的执行依赖于一个 Activity 或者是 Window,那么就会失败。
比如,我要做一个显示用户相册的页面,因此需要申请权限,但在 pre-warmed 的时候,Flutter Widget 所要要依赖的 Activity 并没有被 attached. 这个时候,势必会导致代码运行的异常。
前面说了,我们可以在 FlutterEngine 创建的时候,指定特定的执行结点,而不一定要执行 main() 方法,从而规避一些问题,是这样没错。但是,当打出 release 版本的 Apk时, main.dart 中的代码被混淆了,自定义的执行结点找不到了,比如上面代码中注释掉的 fakeMain() 方法,在 Debug 阶段时正常的,但是在 release 的版本就会出现找不到方法的异常日志。这里其实很好奇 main 方法是怎么找到的,难道没有被混淆?
在来说说
flutterEngine.navigationChannel.setInitialRoute
这个似乎很牛逼的配置,意思是我可以在初始化 FlutterEngine 的时候,执行特定路由所对应的 Widget ,而不是默认的 home 配置。比如,main.dart 默认加载的是 A Widget,可以通过路由配置在初始化的时候执行 B Widget 的逻辑。似乎很好,如果 B Widget 恰好很耗时的话,似乎还可以做优化呢?但是我们再看一下打开页面时候的代码
1 | startActivity( |
是的,你没法做任何其他和 FlutterEngine 有关的配置了,现在打开的 FlutterActivity 会默认显示 B Widget ,A Widget 直接被忽略了。是不是很坑?(o(╥﹏╥)o
- 最后这个集成的 Flutter module 会导致整个项目编译变慢,是非常明显的变慢了。
坦白来说,在原生项目中接入 Flutter 现阶段还是会有投入产出比不高的体验,虽然写实际功能或业务的代码可以复用了,似乎是省了人力成本,但是实际上这中间的还是有许多意想不到的坑要去趟,有些问题都是前人没有遇到过的,到最后只能自己去读源码或是自己给官提 issue 找答案,总体来说整体流程还是比较费时的。
flutter pub get
flutter pub get 或者 pub get 是在做 flutter 的时候在使用第三方 lib 或版本更新的时候经常会使用一个命令,通过这个命令会拉取相关的依赖,其实这个命令还会自动生成 Android 和 iOS 的原生项目。比如在我们创建就的 sub_flutter 模块中,均自动生成了 .android 和 .ios 的原生项目目录。同时这两个目录都是点打头的,那么一般情况下就是隐藏文件,同时通过 .gitignore 文件也可以看到,对于 flutter module 形式来说,这两个文件夹都是被忽略的,毕竟 flutter module 的核心,还是为了方便以 module 的形式集成到原生的项目中,内部的两个原生目录,一方面是为了方便集成,另一方面是便于直接运行执行 hot-reload 的调试。
Flutter 一些思考
Flutter 的确很棒,尤其在低端机上的表现,比如我手边的小米 5 上,长列表滑动或者网格图显示图片,流畅度和原生页面几乎是一样的。但是在高端机上就没有什么优势了,总不能比原生实现还牛逼吧。 HotReload 很实用,尤其是客户端还是比较偏 UI 的情况下,对开发效率是一个很大的提升。同时现阶段各类需要和原生通信的插件还是挺丰富的,虽然适配情况及实现质量层次不齐,比如 Android 权限获取的适配,Android Q 沙盒机制下存储空间访问的适配,相册读写的适配等,都没有非常完美的实现。同时这类跨平台的插件实现,需要满足 Android、iOS 双端甚至是未来更多端的需求,所以还有很长的路要走。
至于 Flutter 自身,Dart 语法写 UI 的嵌套问题其实也不是什么完全无法接受的事情,写多了,自然而然就习惯了,或者说实在不行就不断的创建小方法,将局部的嵌套用方法体来包裹掉。
比如上面这样,按照一些都是 Widget 的思路,做一些封装之后,代码看起来也还行,写起来也能习惯。
槽点:
最后,再说一下 Flutter 页面的生命周期简直是过于捡漏了,关键结点太少了,来来去去就是 build 、setState .很多场景下,限制太大了。
好了,暂时这些,Flutter 未来可期。新项目或试水项目值得投入,在试错阶段可以有非常高效的产出。