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工程的两种方式:

  1. 源码集成,在原生项目中Flutter作为lib直接嵌入Flutter代码,编译过程需要依赖Flutter环境,每个开发都需要配置Flutter开发环境,适合全新项目
  2. 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。

19个小时前加入的 https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps/_compare/1f606754ee0b18d9970e5fdd7b14d8a6df8d2d72…d648f6b063a0ab7ad4eaf0546e90b1cc75b9d58b

即使没有这个命令,也可以使用./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,主要做了三件事情:

  1. 选择符合对应架构的Flutter引擎(flutter.so)
  2. 解析 .flutter-plugins文件,把Flutter对应的android module动态添加到Native工程的依赖中,即动态添加implementation依赖,这块后面会详细讲
  3. 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层的双向关联。

未完待续。。