『全网独一份』Flutter混合工程一键打aar上传Artifactory

3,887 阅读6分钟

官方方案

首先我们先看一下官方提供的混合工程接入方案,链接地址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添加配置所做的事:

  1. include FlutterModule中的.android/Flutter工程
  2. include FlutterModule中.flutter-plugins文件中包含的Flutter工程路径下的android module
  3. 配置所有工程的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中的代码太长,我这里就不贴了,但是里面的代码特别重要,记得去看。这里先总结一下内部所做的事:

  1. 选择符合对应架构的Flutter引擎
  2. 插入Flutter Plugin的编译依赖(默认aar依赖)
  3. 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方式接入:

  1. 需要团队成员都安装有flutter开发环境,对于不开发flutter的同学侵入性太大;同时对于新来的同学需要安装的环境变多,加大了负担。
  2. 需要修改ci流程,原先ci流程中肯定是不包含flutter流程的。

以本地maven的方式接入:

  1. 版本不好管理
  2. 需要把本地产物拷贝给其他不开发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文件解决,能想到的就这些,希望可以给各位提供一些思路。