腾讯翻译君Android客户端集成Flutter Module的探索

前言

这篇文章介绍翻译君客户端如何在现有的原生App中加入一些flutter页面,把已有的工程改造成flutter和native结合的App。
目前翻译君App中已经应用了大量Flutter页面,性能良好。
分享出来,如果有好的方案可以互相交流。

集成方案

如果需要本地开发,可前往flutter.io下载SDK并配置环境变量。
在配置完Flutter环境之后,如果通过IDE或者CML创建一个Flutter Application,目录结构是这样的:

Project
├── android //原生android app目录
├── assets // dart引用的资源文件,比如图片
├── build
├── gen
├── ios //原生iOS app目录
├── lib //dart代码目录
├── pubspec.lock
└── pubspec.yaml //包管理工具pub

这是一个完全通过Flutter构建的App,入口也是Flutter的lib目录下的main.dart;那么通过在lib目录编写dart代码,就可以通过flutter run命令生成Android和iOS安装包安装运行。对于Android来讲,是通过flutter.gradle来添加flutter.jar和vm/isolate_snapshot_data/instr,用它们编译lib中的dart代码,嵌入App中运行的。

那如果想要在已有的App里增加一些flutter页面,根据上面的目录结构,可以有两种思路:

graph1. dependecy

左边的方案,是在普通的Flutter Application的基础上,把已有的Application替换到Flutter Project的子目录;国内较早使用混合flutter开发较多的团队使用的一般是左边这种方案。后来大家也发现了这种方案的弊端,也就是耦合严重导致的开发效率降低。于是很多团队都有自己的方案进行依赖清理,达到右边这种集成方式。
好处是:
1)Native工程可以独立构建,也可以迅速与flutter模块交互,实现「热插拔」。并且,jenkins打包也不再一定需要flutter环境。

2)Flutter工程可以通过flutter attach在chrome中进行调试,hot reload等等。如果使用Android Studio,并且把flutter模块作为子目录,无需打开两个IDE,可在一个窗口中写native代码和dart代码。

有兴趣的同学,可以参考闲鱼早期的方案

好的集成方案

好消息是,Flutter团队也意识到了这个问题,大家可能一开始只是想在已有原生工程中集成flutter,而不是写一个纯flutter的app。所以,他们也在紧锣密鼓地制定更简单的集成方案

翻译君使用了Flutter提供的创建模块的方式来构造flutter模块,可以参考Add Flutter to existing apps
也就是,使用$ flutter create -t module my_flutter创建flutter模块。
这样就直接实现了上面图1右边的剥离的flutter模块带来的种种好处。

注意,生成的Flutter Module可以放在Android、iOS原生工程的外面,也可以放在原生工程的内部,使用git submodule来管理,两种方案各有优劣,可以自行选择。放在外面的好处是在本地同一台机器开发的时候,Android、iOS工程可以依赖同一个Flutter目录,方便修改和调试;放在目录内部作为git submodule的好处是,可以在同一个IDE窗口中开发调试native和dart代码。

源码依赖和aar依赖,打包

  1. 源码依赖
    源码依赖的优点是开发、调试方便,也就是在settings.gradlebuild.gradle里加入对创建的flutter模块的依赖即可,可参考上面的链接。
// MyApp/settings.gradle
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))                                                                      // new
  1. aar依赖
    那如果需要用jenkins/RDM远程打包,远程的机器上没有flutter环境,就可以把flutter和一些plugin打包成aar文件进行依赖。可以进入Flutter目录和plugin目录,分别运行./gradlew assembleDebug/assembleRelease来生成aar文件,再在主工程里引用,flutter aar中包含了flutter sdk的代码,所以这种方式是不需要flutter环境的,也适合第三方快速接入。

由于这两种方式只是在 settings.gradleapp/build.gradle中有一些差别,所以可以利用脚本实现快速切换两种打包方式,切换完成之后sync即可。

flutter/native混合页面跳转

使用了stack manager实现native/flutter页面跳转,原理简单讲就是使用openurl来路由nativeflutter页面,比如hrd://profile_page代表个人信息页。在flutternative侧各有一个路由中心来控制分发。

这种方式的优势是在混合页面比较多的情况下可以方便地管理跳转,通过截图手段减少了跳转的白屏/黑屏现象;缺点是少量修改了flutter sdk里的源码,以及使用起来比较麻烦,比如参数传递的功能实现得并不是很完善,我fork了一份对它进行了少量修改,它的原工程写得蛮厉害的,我觉得大家可以在原工程的代码上做适合自己的精简或者优化。

另外,我认为如果新增的flutter页面居多,可以尽量多用flutter本身的跳转方式,无需采用Activity代理的方式,这样性能更好也更方便。

调试和hot reload

作为模块导入的flutter工程也是可以调试和hot reload的,但不能直接在IDE中进行,需要借助Observatory在浏览器中进行。
在flutter工程目录下运行CML,输入flutter attach。如果无法attach,可以重启VM或者检查代理。
打开一个flutter页面,CML提示An Observatory debugger and profiler on Android SDK built for x86 is available at: http://127.0.0.1:55428/。
在CML窗口按下r可以hot reload, 按下R可以hot restart。
在chrome地址栏输入http://127.0.0.1:55428/,可以进入Observatory进行debug等操作。

插件(plugin)管理

Plugin Packages插件可以放在flutter模块的子目录比如plugins通过yaml依赖,这样,在源码依赖的时候,主工程的gradle会自动通过flutter.gradle识别插件中包含的原生工程代码。

graph2. plugin packages

如果需要提供桥接,可以通过注册监听器的方式来规避双向依赖的问题;比如插件中需要引用外部工程的代码,这肯定是不合适的,这种情况就可以通过接口回调的方式实现flutter对native功能的调用。

一些issues

  1. ./gradlew build的时候提示找不到product_vm_isolate_snapshot.bin的问题。这个应该是flutter本身的问题。已提issue。可以执行gradlew assembleDebug/assembleRelease规避。
  2. 找不到icudtl.dat的问题。这个也是一个flutter的issue,貌似仍未解决(flutter团队更新0.9+以后以为解决了其实并没有),目前可以通过手动拷贝它到assets/flutter_shared/目录规避。
  3. "flutter/runtime/dart_vm.ccVM snapshot must be valid/ duplicated resources"之类的问题,可通过手动删除app/build目录进行规避。

目前简单总结了这么多,有问题可以多多交流。

https://github.com/chaangliu

推荐阅读更多精彩内容