官方方案
首先我们先看一下官方提供的混合工程接入方案,链接地址flutter.dev/docs/develo… 总共提供两种接入方式:
- 以module方式接入
- 以本地maven方式接入
两种接入方式都比较简单,前提都是先创建Flutter Module(flutter相关代码写在这边)。
以module方式接入
以module方式接入首先在Android工程的settings.gradle加入:
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy' //注意这边flutter module路径的正确性
))
这时候工程中就会很神奇的多出一个flutter module,在工程中引入这个flutter module就可以了。例如在app的build.gradle中添加:
implementation project(':flutter')
分析
setBinding和evaluate都是groovy语法,这里所做的事就是运行 include_flutter.groovy 脚本;而setBinding的作用是把gradle环境传入include_flutter.groovy内(因为里面需要使用到gradle环境)。运行groovy文件,文件运行在一个Script对象中,Script有一个属性binding,内部存储了当前环境的变量(包括当前脚本声明的变量与启动脚本传入的参数),evaluate执行时会把当前脚本的binding传入下一个脚本。下面是include_flutter.groovy中的关键代码:
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中的关键代码
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的插件管理,官方的插件托管平台是pub.dev, flutter是用配置文件pubspec.yaml来管理三方插件的(类似于前端的npm),配置某个三方依赖类似如下:
dependencies:
path_provider: ^1.6.18
然后执行Pub.get,会做两件事情:
- 把插件的源代码下载到本地,具体位置在flutterRoot/.pub-cache目录下
- 更新.flutter-plugins和.flutter-plugins-dependencies文件
.flutter-plugins和.flutter-plugins-dependencies以json的形式存储了插件名字和对应的本地工程地址,上面添加插件module到Android工程有用到,看一眼里面存储的东西:
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中,里面有一句特别关键的代码
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中就不需要做额外的配置了:
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
# 远程maven url和账号
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
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中
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
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 {
// Tell the Artifactory Plugin which artifacts should be published to Artifactory.
publications('aar')
publishArtifacts = true
// Properties to be attached to the published artifacts.
properties = ['qa.level': 'basic', 'dev.team': 'core']
// Publish generated POM files to Artifactory (true by default)
publishPom = true
}
}
}
ext {
appendDependencies = { Set<Dependency> compileTimeDependencies, dependenciesNode ->
compileTimeDependencies.each {
// 过滤library引用
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.自动升级 添加了脚本参数的情况下取消自动升级
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
版本号自动升级,自动升级基于上次版本做加一操作
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工程中
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,构建收集产物上传
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,构建收集产物上传
./gradlew clean assembleRelease
./gradlew flutter:artifactoryPublish
产物上传到私仓之后就能正常使用了,使用方式和本地maven类似;还有一项工作就是源码和二进制切换的配置。开发阶段还是需要以module方式依赖的,因为调试和hot reload更加方便。工程远程分支保持二进制依赖,通过本地配置来打开源码依赖,这种方式侵入最小,这里就想到在local.properties中添加配置
flutterAar=false
settings.gradle中修改为
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的地方修改为
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的方案 github.com/adwiv/andro… ,其实也是蛮适合flutter module打产物的——将plugin module的aar和flutter module的aar合并,但是由于没有想好module内部的三方依赖怎么合并,所以没有实施。实际上fat-aar可以将三方依赖也打进整个aar中,可能会造成gradle依赖冲突默认解决方式失效(默认是使用高版本的依赖)。不知道是不是可以通过合并pom文件解决,能想到的就这些,希望可以给各位提供一些思路。