前言
这篇文章介绍翻译君客户端如何在现有的原生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页面,根据上面的目录结构,可以有两种思路:
左边的方案,是在普通的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依赖,打包
- 源码依赖
源码依赖的优点是开发、调试方便,也就是在settings.gradle
和build.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
- aar依赖
那如果需要用jenkins
/RDM
远程打包,远程的机器上没有flutter环境,就可以把flutter和一些plugin打包成aar
文件进行依赖。可以进入Flutter目录和plugin目录,分别运行./gradlew assembleDebug/assembleRelease来生成aar文件,再在主工程里引用,flutter aar中包含了flutter sdk的代码,所以这种方式是不需要flutter环境的,也适合第三方快速接入。
由于这两种方式只是在 settings.gradle
和app/build.gradle
中有一些差别,所以可以利用脚本实现快速切换两种打包方式,切换完成之后sync
即可。
flutter/native混合页面跳转
使用了stack manager实现native/flutter
页面跳转,原理简单讲就是使用openurl
来路由native
和flutter
页面,比如hrd://profile_page
代表个人信息页。在flutter
和native
侧各有一个路由中心来控制分发。
这种方式的优势是在混合页面比较多的情况下可以方便地管理跳转,通过截图手段减少了跳转的白屏/黑屏现象;缺点是少量修改了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识别插件中包含的原生工程代码。
如果需要提供桥接,可以通过注册监听器的方式来规避双向依赖的问题;比如插件中需要引用外部工程的代码,这肯定是不合适的,这种情况就可以通过接口回调的方式实现flutter对native功能的调用。
一些issues
-
./gradlew build
的时候提示找不到product_vm_isolate_snapshot.bin
的问题。这个应该是flutter
本身的问题。已提issue。可以执行gradlew assembleDebug/assembleRelease
规避。 - 找不到
icudtl.dat
的问题。这个也是一个flutter的issue,貌似仍未解决(flutter团队更新0.9+以后以为解决了其实并没有),目前可以通过手动拷贝它到assets/flutter_shared/
目录规避。 - "flutter/runtime/dart_vm.ccVM snapshot must be valid/ duplicated resources"之类的问题,可通过手动删除
app/build
目录进行规避。
目前简单总结了这么多,有问题可以多多交流。