官方方案
首先我们先看一下官方提供的混合工程接入方案,链接地址https://flutter.dev/docs/development/add-to-app/android/project-setup 总共提供两种接入方式:
两种接入方式都比较简单,前提都是先创建Flutter Module(flutter相关代码写在这边)。

以module方式接入
以module方式接入首先在Android工程的settings.gradle加入:
1 2 3 4 5
| setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'my_flutter/.android/include_flutter.groovy' ))
|
这时候工程中就会很神奇的多出一个flutter module,在工程中引入这个flutter module就可以了。例如在app的build.gradle中添加:
1
| implementation project(':flutter')
|
分析
setBinding和evaluate都是groovy语法,这里所做的事就是运行 include_flutter.groovy 脚本;而setBinding的作用是把gradle环境传入include_flutter.groovy内(因为里面需要使用到gradle环境)。运行groovy文件,文件运行在一个Script对象中,Script有一个属性binding,内部存储了当前环境的变量(包括当前脚本声明的变量与启动脚本传入的参数),evaluate执行时会把当前脚本的binding传入下一个脚本。下面是include_flutter.groovy中的关键代码:
1 2 3 4
| gradle.include ":flutter" gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter") def flutterSdkPath = properties.getProperty("flutter.sdk") gradle.apply from: "$flutterSdkPath/packages/flutter_tools/gradle/module_plugin_loader.gradle"
|
添加flutter module到Android工程中,导入module_plugin_loader.gradle脚本片段。简单看一下module_plugin_loader.gradle中的关键代码
1 2 3 4 5 6 7 8 9
| def pluginsFile = new File(moduleProjectRoot, '.flutter-plugins-dependencies') if (pluginsFile.exists()) { def object = new JsonSlurper().parseText(pluginsFile.text) object.plugins.android.each { androidPlugin -> def pluginDirectory = new File(androidPlugin.path, 'android') include ":${androidPlugin.name}" project(":${androidPlugin.name}").projectDir = pluginDirectory } }
|
简单的说就是添加所有插件module到Android工程中,那么这些插件module从哪里来。插个题外话讲一下flutter的插件管理,官方的插件托管平台是https://pub.dev,
flutter是用配置文件pubspec.yaml来管理三方插件的(类似于前端的npm),配置某个三方依赖类似如下:
1 2
| dependencies: path_provider: ^1.6.18
|
然后执行Pub.get,会做两件事情:
- 把插件的源代码下载到本地,具体位置在flutterRoot/.pub-cache目录下
- 更新.flutter-plugins和.flutter-plugins-dependencies文件
.flutter-plugins和.flutter-plugins-dependencies以json的形式存储了插件名字和对应的本地工程地址,上面添加插件module到Android工程有用到,看一眼里面存储的东西:
1
| path_provider_macos=/Users/liuxiaoshuai/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-0.0.4+3/
|
pub管理不像gradle依赖管理那么智能,一定要注意冲突的处理!
总结一下settings.gradle添加配置所做的事:
- include FlutterModule中的.android/Flutter工程
- include FlutterModule中.flutter-plugins文件中包含的Flutter工程路径下的android module
- 配置所有工程的build.gradle配置执行阶段都依赖于:flutter工程,也即它最先执行配置阶段
所有的module都加进来了,总感觉差点什么:我们在FlutterModule中写的dart代码以及引擎是怎么加入到Android工程中的呢?答案藏在flutter module的build.gradle中,里面有一句特别关键的代码
1 2
| def flutterRoot = localProperties.getProperty('flutter.sdk') apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
剩余的所有工作在flutter.gradle中做,flutter.gradle中的代码太长,我这里就不贴了,但是里面的代码特别重要,记得去看。这里先总结一下内部所做的事:
- 选择符合对应架构的Flutter引擎
- 插入Flutter Plugin的编译依赖(默认aar依赖)
- Hook mergeAssets/processResources Task,预先执行FlutterTask,调用flutter命令编译Dart层代码构建出flutter_assets产物,并拷贝到assets目录下(dart代码产物)
到这里插件和dart代码都添加到了Android工程中
以本地maven方式接入
首先通过flutter shell脚本打出对应的产物,常用命令如下:
- flutter build aar
- flutter build aar –no-debug –no-profile //只打release产物
- flutter build aar–build-number=2.0 //版本控制
执行命令之后在下图所示位置查看产物:

接下来使用产物,settings.gradle中就不需要做额外的配置了:
1 2 3 4 5 6 7 8
| repositories { maven { url '../FlutterModule/build/host/outputs/repo' //注意路径的正确性 } } dependencies { implementation 'com.example.flutter_module:flutter_release:1.0' }
|
官方方案缺陷
以module方式接入:
- 需要团队成员都安装有flutter开发环境,对于不开发flutter的同学侵入性太大;同时对于新来的同学需要安装的环境变多,加大了负担。
- 需要修改ci流程,原先ci流程中肯定是不包含flutter流程的。
以本地maven的方式接入:
- 版本不好管理
- 需要把本地产物拷贝给其他不开发flutter的同学,否则本质上还是需要安装flutter开发环境。
那么考虑能不能利用flutter build aar打出的产物,将产物提交到公司的私仓,这样所有同学就都能正常下载使用了。理论上这个方案是可行的,遍历build/host/outputs/repo下所有的文件夹,然后将产物依次提交。方案没有实践过,大家可以试试。
全新的方案
我的方案是自己插手产物的构建和上传,整套操作是一个shell脚本,同时会依赖于外部的配置。

第一个配置文件gradle.properties
1 2 3 4 5 6 7
| MAVEN_URL=http://172.16.9.30:8081/artifactory/ MAVEN_ACCOUNT_NAME=*** MAVEN_ACCOUNT_PWD=***
GROUP=com.lxs.flutter VERSION_NAME=0.0.8
|
用于设置私仓的地址、用户名、密码。产物的group、版本
第二个配置文件build.gradle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| buildscript { repositories { google() jcenter() }
dependencies { classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.8.1" } }
allprojects { repositories { google() jcenter() } apply plugin: 'com.jfrog.artifactory' apply plugin: 'maven-publish' }
task clean(type: Delete) { delete rootProject.buildDir }
subprojects { project.afterEvaluate { project.plugins.withId('com.android.library') { project.group = GROUP project.version = VERSION_NAME def mavenScriptPath = project.rootProject.file('./config/flutter_jfrog.gradle') project.apply from: mavenScriptPath } } }
|
注意.android目录在每次pub get之后会重新构建,所以不能直接在.android工程中修改,这里直接用外部配置文件覆盖的方式。因为产物的上传需要依赖jfrog插件,所以build.gradle中做了jfrog的相应配置。还有一点就是group和version的矫正,因为flutter module中添加插件二进制会使用到group和version。具体代码在flutter.gradle中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private void configurePluginAar(String pluginName, String pluginPath, Project project) { File pluginBuildFile = project.file(Paths.get(pluginPath, "android", "build.gradle")); if (!pluginBuildFile.exists()) { throw new GradleException("Plugin $pluginName doesn't have the required file $pluginBuildFile.") } Matcher groupParts = GROUP_PATTERN.matcher(pluginBuildFile.text) String groupId = groupParts[0][1] File pluginSettings = project.file(Paths.get(pluginPath, "android", "settings.gradle")); if (!pluginSettings.exists()) { throw new GradleException("Plugin $pluginName doesn't have the required file $pluginSettings.") } Matcher projectNameParts = PROJECT_NAME_PATTERN.matcher(pluginSettings.text) String artifactId = "${projectNameParts[0][1]}_release" project.dependencies.add("api", "$groupId:$artifactId:+") }
|
第三个配置文件flutter_jfrog.gradle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import com.sun.tools.classfile.Dependency
task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.srcDirs }
assemble.dependsOn androidSourcesJar
publishing { publications { aar(MavenPublication) { groupId = GROUP version = VERSION_NAME artifactId = project.name artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") artifact androidSourcesJar
pom.withXml { def dependenciesNode = asNode().appendNode('dependencies')
def compileTimeDependencies = configurations.implementation.allDependencies.withType(ModuleDependency) + configurations.releaseImplementation.allDependencies.withType(ModuleDependency)
appendDependencies(compileTimeDependencies, dependenciesNode) } } } } artifactory { contextUrl = MAVEN_URL publish { repository { repoKey = 'gradle-dev-local' username = MAVEN_ACCOUNT_NAME password = MAVEN_ACCOUNT_PWD } defaults { publications('aar') publishArtifacts = true properties = ['qa.level': 'basic', 'dev.team': 'core'] publishPom = true } } }
ext { appendDependencies = { Set<Dependency> compileTimeDependencies, dependenciesNode ->
compileTimeDependencies.each { if (it.version != "unspecified") { def dependencyNode = dependenciesNode.appendNode('dependency') dependencyNode.appendNode('groupId', it.group) dependencyNode.appendNode('artifactId', it.name) dependencyNode.appendNode('version', it.version)
if (!it.excludeRules.isEmpty()) { def exclusionsNode = dependencyNode.appendNode('exclusions') it.excludeRules.each { rule -> def exclusionNode = exclusionsNode.appendNode('exclusion') exclusionNode.appendNode('groupId', rule.group) exclusionNode.appendNode('artifactId', rule.module ?: '*') } } } } } }
|
主要是产物的收集和上传,implementation依赖中不包含releaseImplementation,需要注意聚合implementation和releaseImplementation。(因为flutter module中引擎的依赖方式是releaseImplementation)
接下来分析脚本,第一步是版本号的更新。提供了两种方式:1.脚本参数 2.自动升级 添加了脚本参数的情况下取消自动升级
1 2 3 4 5 6 7 8
| num=$# if [ $num -eq 0 ];then updateVersion else v=$(grep VERSION_NAME configs/gradle.properties|cut -d'=' -f2) sed -i '' 's/VERSION_NAME='$v'/VERSION_NAME='$1'/g' configs/gradle.properties echo '更新版本号成功...' fi
|
版本号自动升级,自动升级基于上次版本做加一操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function updateVersion() { v=$(grep VERSION_NAME configs/gradle.properties|cut -d'=' -f2) echo 旧版本号$v v1=$(echo | awk '{split("'$v'",array,"."); print array[1]}') v2=$(echo | awk '{split("'$v'",array,"."); print array[2]}') v3=$(echo | awk '{split("'$v'",array,"."); print array[3]}') y=$(expr $v3 + 1)
if [ $y -ge 10 ];then y=$(expr $y % 10) v2=$(expr $v2 + 1) fi
if [ $v2 -ge 10 ];then v2=$(expr $v2 % 10) v1=$(expr $v1 + 1) fi
vv=$v1"."$v2"."$y echo 新版本号$vv # 更新配置文件 sed -i '' 's/VERSION_NAME='$v'/VERSION_NAME='$vv'/g' configs/gradle.properties if [ $? -eq 0 ]; then echo '' else echo '更新版本号失败...' exit fi }
|
第二步把配置copy到.android工程中
1 2 3 4 5 6 7 8 9
| if [ -d '.android/config/' ]; then echo '.android/config 文件夹已存在' else : mkdir .android/config fi
cp configs/gradle.properties .android/gradle.properties cp configs/flutter_jfrog.gradle .android/config/flutter_jfrog.gradle cp configs/build.gradle .android/build.gradle
|
第三步各个plugin module单独打aar,构建收集产物上传
1 2 3 4 5 6 7 8 9 10 11
| for line in $(cat .flutter-plugins | grep -v '^ *#') do plugin_name=${line%%=*} plugin_path=${line res=$(doesSupportAndroidPlatform ${plugin_path}) if [ $res -eq 0 ];then ./gradlew "${plugin_name}":clean ./gradlew "${plugin_name}":assembleRelease ./gradlew "${plugin_name}":artifactoryPublish fi done
|
第四步flutter module打aar,构建收集产物上传
1 2
| ./gradlew clean assembleRelease ./gradlew flutter:artifactoryPublish
|
产物上传到私仓之后就能正常使用了,使用方式和本地maven类似;还有一项工作就是源码和二进制切换的配置。开发阶段还是需要以module方式依赖的,因为调试和hot reload更加方便。工程远程分支保持二进制依赖,通过本地配置来打开源码依赖,这种方式侵入最小,这里就想到在local.properties中添加配置
settings.gradle中修改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def localProperties = readPropertiesIfExist(new File("local.properties"))
if (!localProperties.getProperty("flutterAar", "true").toBoolean()) { def flutterFile = new File(settingsDir.parentFile, 'flutter_module/.android/include_flutter.groovy') if (flutterFile.exists()) { setBinding(new Binding([gradle: this])) evaluate(flutterFile) } else { throw new GradleException("flutter module does not exit,please check the path") } }
private static Properties readPropertiesIfExist(File propertiesFile) { Properties result = new Properties() if (propertiesFile.exists()) { propertiesFile.withReader('UTF-8') { reader -> result.load(reader) } } return result }
|
添加module的地方修改为
1 2 3 4 5 6 7
| def localProperties = readPropertiesIfExist(new File("local.properties")) def flutterAar = localProperties.getProperty("flutterAar", "true").toBoolean() if(flutterAar){ api com.lxs.flutter:flutter:0.0.8 }else{ api project(':flutter') }
|
小优化
现在每次执行脚本都是所有插件都一股脑打aar、版本升级、上传,这显得非常耗时又没有意义。其实并不是每次所有插件module都需要做这部分操作的,插件内容没有更新的情况下完全没必要。我们可以用一个文件记录plugin和它都应的版本,然后和.flutter-plugins中的作比较,更新了的做打aar、升级、上传操作,没有更新的保持原版本(或者各个插件单独上传maven,根据.flutter-plugins做版本收拢)。git依赖是这种判断方式的软肋,需要做更深一步的检查(可能可以通过文件的摘要信息来对比)。
更优的方案
github上有一种多module合并aar的方案 https://github.com/adwiv/android-fat-aar ,其实也是蛮适合flutter module打产物的——将plugin module的aar和flutter module的aar合并,但是由于没有想好module内部的三方依赖怎么合并,所以没有实施。实际上fat-aar可以将三方依赖也打进整个aar中,可能会造成gradle依赖冲突默认解决方式失效(默认是使用高版本的依赖)。不知道是不是可以通过合并pom文件解决,能想到的就这些,希望可以给各位提供一些思路。