flutter-project-manage
flutter-project-manage
本文主要介绍Android视角下在已有 App 中嵌入 Flutter 应用的实践,iOS 的方案思路基本一致,实现细节不详细阐述。
Flutter 工程类型
Flutter工程中,通常有以下几种工程类型,下面分别简单概述下:
1. Flutter Application
纯净的Flutter App工程,包含标准的Dart层与Native平台层(android/&ios/)
2. Flutter Module
Flutter模块工程,仅包含Dart层实现,Native平台子工程的作用是构建Flutter产物,是通过Flutter自动生成的隐藏工程(.android/&.ios/)
3. Flutter Plugin
Flutter平台插件工程,包含Dart层与Native平台层的实现(android/&ios/),往外提供API接口
4. Flutter Package
Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget
接入Flutter工程的两种方式:
- 源码集成,在原生项目中Flutter作为lib直接嵌入Flutter代码,编译过程需要依赖Flutter环境,每个开发都需要配置Flutter开发环境,适合全新项目
- Flutter项目作为子项目module,生成aar后由原生App依赖,对于App来说屏蔽了Flutter开发环境,在原有环境中即可打包,对App开发来说屏蔽了Flutter环境,适合已经App嵌入Flutter应用
本文主要介绍第二种方式,由于Flutter官方并未提供完整的解决方案,所以接入过程中会碰到一些问题,这里给出一些解决思路供参考。
创建一个flutter module:
flutter create -t module –org com.example my_flutter
工程结构:
FlutterModule 构建 Aar
flutter build aar
但是这个命令在Flutter v1.7.8版本中会提示找不到这个命令,估计是dev版本新加入的,还没有stable。
即使没有这个命令,也可以使用./gradlew assembleDebug进行编译,结果是一样的。
如果使用flutter build aar命令,Flutter官方给出的结果是,本地作为localmaven,生成的结构如下:
build/host/outputs/repo └── com └── example └── my_flutter └── flutter_release ├── 1.0 │ ├── flutter_release-1.0.aar │ ├── flutter_release-1.0.aar.md5 │ ├── flutter_release-1.0.aar.sha1 │ ├── flutter_release-1.0.pom │ ├── flutter_release-1.0.pom.md5 │ └── flutter_release-1.0.pom.sha1 ├── maven-metadata.xml ├── maven-metadata.xml.md5 └── maven-metadata.xml.sha1
Flutter v1.2版本之后Flutter产物自动会打包到aar中,具体脚本见 flutter.gradle,主要做了三件事情:
- 选择符合对应架构的Flutter引擎(flutter.so)
- 解析 .flutter-plugins文件,把Flutter对应的android module动态添加到Native工程的依赖中,即动态添加implementation依赖,这块后面会详细讲
- Hook mergeAssets/processResources Task,预先执行FlutterTask,调用flutter命令编译Dart层代码构建出flutter_assets产物,并拷贝到assets目录下
基于Flutter v1.7.8,编译产物 Debug版本
Release版本
在Flutter v1.7之前,Release的产物如下:
也就是说Flutter的编译产物, 从四个二进制文件变成了一个 libapp.so 文件 。
这里也涉及到so兼容arm的问题,之前把libflutter.so拷贝到arm目录下即可,现在编译出的libapp.so拷贝过去是有问题的,解决办法可以参考 这个项目 。
如果在Flutter module 中依赖了Flutter Plugin,那么在App中依赖Flutter module编译出的aar时,会报错,例如
ERROR: Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT. Show Details Affected Modules: app
下面会分析下为什么会报错。
Flutter Plugin依赖原理
以Flutter Module为例,看下Flutter Plugin依赖的原理。
flutter package get
当Flutter Module在pub中依赖了Flutter Plugin,并且在 flutter package get后,会从远程pubserver拉取依赖的Flutter Plugin,并生成一个文件记录依赖了哪些Flutter Plugin,名字为: .flutter-plugins
,内容为 k-v,如:
flutter_webview_plugin=/Users/wanglinglong/Develop/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.5/
被拉取到本地后的Flutter Plugin目录如下
动态依赖
然后在.android/app/settings.gradle 文件中添加配置脚本,使其在Gradle初始化之后执行解析操作:
rootProject.name = 'android_generated' setBinding(new Binding([gradle: this])) evaluate(new File(settingsDir, 'include_flutter.groovy'))
这个脚本的作用是控制配置(evaluate)顺序,操作是解析.flutter-plugins,得到各个插件的android工程,使其在.android/Flutter 配置完成之后再进行配置解析。Gradle配置阶段的目标是解析每个project中的build.gradle。
然后在.android/Flutter/build.gradle中依赖
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
这个文件中同样解析.flutter-plugins文件,遍历后一个一个 implementation 到./android/Flutter module工程里完成依赖。
所以在Native层面来看这种依赖其实是本地依赖,只不过Flutter Plugin工程路径都在Flutter的环境变量下的缓存目录中。
这里的依赖分成两个部分,一部分是原生Native依赖,一部分是Dart依赖。
对于原生部分,Flutter Plugin作为lib被Flutter Module本地依赖,根据Android 编译常识,在打包Aar后,这些 本地依赖的工程lib不能被一起打到Aar中 。
所以当App中依赖Flutter Module的产物Aar时,不能获得Flutter Module 中依赖的Flutter Plugin Android原生依赖,最终会报错。
对于Dart部分,通过pub进行管理依赖,相当于源码依赖,在打包时,这些Plugin的Dart源码部分都参与打包,最终生成Flutter的构建产物。
Flutter Module打包成Aar后,Aar中包含Android原生部分编译产物和Flutter Dart部分编译产物,后面App依赖该Aar后就可以脱离Flutter的编译环境,直接进行apk打包了。
那么如何解决Aar报错的问题?
Flutter module 依赖 Flutter Plugin
已有方案大致原理都是收集Flutter Plugin 的Aar文件,然后进一步处理。
方案一:
使用 fat-aar,大致原理就是将所有的Flutter Plugin打包到同一个Aar中,这个方案比较臃肿,还可能涉及到gradle版本适配,而且可能产生多次依赖的问题。不建议使用。
方案二:
遍历所有依赖的Flutter Plugin,搜集Plugin Android工程下的Aar产物并copy到Flutter Module指定目录下,然后再push maven。
方案三:
遍历所有依赖的Flutter Plugin,根据Plugin版本信息,挨个打包成上传到maven。
由于公司内之前碰到到相关需求场景,即同一工程下多个Module如何统一管理的问题,最后解决方案就是使用的方案三,这里依然采用方案三,相比其他两种方案更稳定。
iOS 思路相同,打包成framework后上传到CDN,通过Pod进行依赖管理。
看下实现:
首先在.andorid/Flutter/build.gradle中依赖脚本 apply from: './publish_flutter_plugin.gradle'
def flutterUpload = gradle.rootProject.project(':flutter').tasks.getByName(uploadTaskName) gradle.rootProject.ext.pluginList.each { name -> project.afterEvaluate { project.apply plugin: 'com.u51.publish' // 修改插件库对插件库的本地依赖为pom依 modifyPom { pom -> pom.dependencies.findAll { dep -> if (rootProject.ext.pluginList.contains(dep.artifactId)) { dep.groupId = rootProject.ext.groupId dep.version = rootProject.ext.pluginMap[dep.artifactId] } } } // 设定插件本地库发布坐标版本号 project.publish { groupId rootProject.ext.groupId version rootProject.ext.pluginMap[name] artifactId name compileEnvCheck false sources true } // flutter库 upload在插件本地库均upload完成后 if (pluginUploadTask != null&&node==null) { flutterUpload.dependsOn(pluginUploadTask) } } }
另外这种方案兼容持续集成环境,对于Native层面来讲,无需做任何改动。
Flutter Plugin 依赖 Flutter Plugin
上面提到的是Flutter module依赖Flutter Plugin的情况,那么如果是Flutter Plugin工程依赖Flutter Plugin工程有没有问题?
先看下Flutter Plugin的项目结构 flutter create --template=plugin -i swift -a kotlin flutter_plugin
在example下运行 flutter run命令,就可以跑起来了。
看下编译产物,其中Aar中只有原生的编译产物,并无Dart编译产物。
所以Flutter Plugin需要被pub依赖,pub将发布到远程的库下载到本地,然后在工程中
- 原生部分,Flutter Module通过上述方案动态添加maven 依赖
- Dart部分,Flutter Module通过pub依赖找到Dart源码,在Flutter Module中import引入,相当于源码依赖,共同参与编译,生成最终的 Dart产物
上面的场景都是Flutter Module(Flutter Application)依赖Flutter Plugin的情况,那么Flutter Plugin能不能依赖Flutter Plugin,会不会有问题?
首先Dart部分由于是pub依赖,相当于源码依赖,是没有问题的。
原生部分,由于Flutter Plugin在原生部分没有引入include_flutter.groovy(Android),所以宿主Flutter Pugin无法动态include到子Flutter Plugin,找不到依赖。
解决办法就是再给它加上,方法同上。
工程结构
总结来说,对于 Flutter 来讲只有 Flutter Module 和 Flutter Application编译会生成Dart编译产物,而Flutter Application我们基本不用考虑。
所以很自然的得出结论,我们可以把Flutter Module作为隔离层,作为Flutter层的聚合统一输出给 App,App 工程只需通过 maven or Pod 依赖Flutter Module即可。
其余的Flutter相关依赖操作都交给Flutter Module。
Flutter Module可以看成是Flutter层到Native层的双向关联。
未完待续。。