Flutter混合开发之打包aar并上传到nexus

今天我必须得写篇博客来记录下这“历史性”的时刻,耗时3天终于算是搞出来了。此时此刻只有一张图能形容我现在的状态。

我们的产品已经正式使用一年了,移动端Android和iOS都是采用原生开发,有一个模块使用频率非常高、改动的频率非常高、逻辑非常复杂,当初在做这个模块的时候就在想有没有一个框架能够嵌入到原生的应用中,就像网页一样的嵌入进去,这样只要开发一次就好了,避免了两端人员对需求的理解偏差,导致开发和测试成本增加,接触Flutter也有一个多月时间了,就觉得很契合我当初的设想。

其实项目从上线到现在已经有大半年了,让我去寻找这样一个框架的需求并不迫切,但是!在我看了iOS端代码后,这个需求强烈的不能再强烈了!这就是我说的那个使用频率很高的模块。

看到代码的那一刹那,我感觉我的一只脚已经跨出了公司的大门了。我不知道其他同学接手过的项目怎么样,我也不敢说自己写的代码有多精简和牛逼,但是这么一坨至少在我这里我是没法容忍了。为了挽回程序员那点颜面,也为了证明下自己,主要是想验证下混合方案的可行性,我决定收回那只脚。其实我这里多说一句,在工作中遇到的种种困难,都不要逃避,把这个困难当成一次历练,因为这种困难真的很难得!有这种机会就要把握住,分析困难,解决它,你就离成功又进一步了。

废话不说,进入正题。

Flutter和原生Native项目混合使用官方已经有方案,这里是官方方案,官方的方案就是将flutter作为module,然后native主工程引入进来。这种方式适合1到2人负责的项目,如果有多人协作开发的大型项目就不合适了,因为其他人首先要配置Flutter环境,而且团队里面其他人还要配置module的依赖,熟悉flutter,成本是很高的。我们能不能将flutter写的项目打包成aar然后native项目只要简单的implementation进来就行了呢?答案是肯定的。

这种方案最早提出者是闲鱼,大家可以观摩下这篇文章,其主要思想就是将flutter相关的产物(这让我想到了大便)打包起来,然后传到maven等仓库上。

我们需要解决的问题有以下几点:

1、如何把flutter的产物提取出来;

2、如何打包成aar;

3、如何上传到maven以及如何引入。

一、提取Flutter产物

产物提取的过程大家可以观摩下这篇掘金文章,写的很好,最核心的就是这位作者在最下面提供的那个脚本。我把提取的脚本代码贴出来,有几处我做了修改。

flutter.sh

#!/bin/bash
# This is flutter build

# 初始化记录项目pwd
projectDir=`pwd`
# 获取 flutter sdk
rootFlutter=`/Users/longdw/flutter`
# 提取 flutter skd路径
rootDir=${rootFlutter%/*}
# 获取

# 假如没有引用三方的flutter Plugin 设置false 即可 *************************
isPlugin=true

# targetSDK 设置为 26

echo "扫描 app/build.gradle 中 targetSDK "
if [ `grep -c 'targetSdkVersion 26' .android/app/build.gradle` -eq '1' ]; then
     echo "targetSDK 已经是26 ,不需要修改"
else
     echo "targetSDK 需要修改为 26 "
     sed -i '' 's/targetSdkVersion 28/targetSdkVersion 26/g' .android/app/build.gradle
fi

echo "扫描 Flutter/build.gradle 中 targetSDK "
if [ `grep -c 'targetSdkVersion 26' .android/Flutter/build.gradle` -eq '1' ]; then
     echo "targetSDK 已经是26 ,不需要修改"
else
     echo "targetSDK 需要修改为 26 "
     sed -i '' 's/targetSdkVersion 28/targetSdkVersion 26/g' .android/Flutter/build.gradle
fi


# 版本号 + 1
cd ${projectDir}
v=`grep sonatypeVersion bak/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]}'`
v4=`echo | awk '{split("'$v'",array,"."); print array[4]}'`
y=`expr $v4 + 1`

vv=$v1"."$v2"."$v3"."$y
echo 新版本号$vv
# 更新配置文件
sed -i '' 's/sonatypeVersion='$v'/sonatypeVersion='$vv'/g' bak/gradle.properties
if [ $? -eq 0 ]; then
    echo ''
else
    echo '更新版本号失败...'
    exit
fi

# 删除 fat-aar 引用
function delFatAarConfig() {
    if [  ${isPlugin} == false  ]; then
        echo '删除 fat-aar 引用........未配置三方插件'
    else :
        cd ${projectDir} # 回到项目
        echo '删除 fat-aar 引用 ... '
        sed -i '' '$d
            ' .android/settings.gradle
        sed -i '' '$d
            ' .android/Flutter/build.gradle
        sed -i '' '$d
            ' .android/Flutter/build.gradle
        sed -i '' '11 d
            ' .android/build.gradle
    fi
}

# 引入fat-aar
function addFatAArConfig() {
     if [  ${isPlugin} == false  ]; then
        echo '引入fat-aar 配置........未配置三方插件'
     else :
        cd ${projectDir} # 回到项目

        cp bak/setting_gradle_plugin.gradle .android/config/setting_gradle_plugin.gradle

        if [ `grep -c 'setting_gradle_plugin.gradle' .android/settings.gradle` -eq '1' ]; then
            echo ".android/settings.gradle 中 已存在 !!!"
        else
            echo ".android/settings.gradle 中 不存在,去编辑"
            sed -i '' '$a\
            apply from: "./config/setting_gradle_plugin.gradle"
            ' .android/settings.gradle
        fi

        if [ $? -eq 0 ]; then
            echo '.android/settings.gradle 中 脚本插入 fat-aar 成功 !!!'
        else
            echo '.android/settings.gradle 中 脚本插入 fat-aar 出错 !!!'
            exit 1
        fi

        if [ `grep -c 'com.kezong:fat-aar' .android/build.gradle` -eq '1' ]; then
            echo "com.kezong:fat-aar:1.0.3 已存在 !!!"
        else
            echo "com.kezong:fat-aar:1.0.3 不存在,去添加"
            sed -i '' '10 a\
            classpath "com.kezong:fat-aar:1.0.3"
            ' .android/build.gradle
        fi

        # flutter/build.gradle 中添加fat-aar 依赖 和 dependencies_gradle_plugin
        if [ `grep -c "com.kezong.fat-aar" .android/Flutter/build.gradle` -eq '1' ]; then
            echo "Flutter/build.gradle 中 com.kezong:fat-aar 已存在 !!!"
        else
            echo "Flutter/build.gradle 中 com.kezong:fat-aar 不存在,去添加"
            sed -i '' '$a\
            apply plugin: "com.kezong.fat-aar"
            ' .android/Flutter/build.gradle
        fi

        cp bak/dependencies_gradle_plugin.gradle .android/config/dependencies_gradle_plugin.gradle
        if [ `grep -c 'dependencies_gradle_plugin' .android/Flutter/build.gradle` -eq '1' ]; then
            echo "Flutter/build.gradle 中 dependencies_gradle_plugin.gradle 已存在 !!!"
        else
            echo "Flutter/build.gradle 中 dependencies_gradle_plugin.gradle 不存在,去添加"
            sed -i '' '$a\
            apply from: "../config/dependencies_gradle_plugin.gradle"
            ' .android/Flutter/build.gradle
        fi
      fi
}


# step1 clean
echo 'clean old build'
find . -depth -name "build" | xargs rm -rf
${rootFlutter} clean


# step2 copy so
echo 'copy so'
cd ${rootDir}/cache/artifacts/engine
for arch in android-arm android-arm-profile android-arm-release ; do
    pushd $arch
    cp flutter.jar flutter-armeabi-v7a.jar #备份
    unzip flutter.jar lib/armeabi-v7a/libflutter.so
    mv lib/armeabi-v7a lib/armeabi
    zip -d flutter.jar lib/armeabi-v7a/libflutter.so
    zip flutter.jar lib/armeabi/libflutter.so
    popd
done

# step 3 package get
echo 'packages get'
cd ${projectDir} # 回到项目
${rootFlutter} packages get

# step3.1 脚本补充:因为.android是自动编译的,所以内部的配置文件和脚本不可控,所以需要将bak内的脚本自动复制到 .android 内部
echo 'copy bak/config/uploadArchives.gradle to .android/config/... ,    copy bak/gradle.properties to Flutter/gradle.properties'
if [  -d '.android/config/' ]; then
   echo '.android/config 文件夹已存在'
else :
   mkdir .android/config
fi

if [  -f '.android/config/uploadAndroidJar.gradle' ];then
   echo '.android/config/uploadAndroidJar.gradle 已存在'
else :
   cp bak/config/uploadAndroidJar.gradle .android/config/uploadAndroidJar.gradle
fi

if [  -f "android/config/uploadArchives.gradle" ];then
    echo '.android/config/uploadArchives.gradle 已存在'
else :
    cp bak/config/uploadArchives.gradle .android/config/uploadArchives.gradle
fi

cp bak/gradle.properties .android/Flutter/gradle.properties

# step 3.2  脚本补充:同时在Flutter 的gradle中插入引用  apply from: "../uploadArchives.gradle"
echo '在Flutter 的gradle中插入引用  apply from: "../uploadArchives.gradle"'
if [ `grep -c 'uploadArchives.gradle' .android/Flutter/build.gradle` -eq '1' ]; then
    echo "Found!"
else
    echo "not found , 去修改"
    sed -i '' '2i\
    apply from: "../config/uploadArchives.gradle"' .android/Flutter/build.gradle
fi

# setp 3.3 脚本补充:引入fat-aar 相关脚本
# 在 settings.gradle 中 插入 , 注意 sed 命令换行 在mac下 是 \'

大家把flutter.sh文件放到项目的项目根目录下,如我的是flutterdemo/flutter.sh。

然后在项目根目录下新建bak文件夹,将下面几个文件放进去。

dependencies_gradle_plugin.gradle

dependencies {
    def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
    def plugins = new Properties()
    def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
    if (pluginsFile.exists()) {
        pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
    }
    plugins.each { name, _ ->
        println name
        embed project(path: ":$name", configuration: 'default')
    }
}

setting_gradle_plugin.gradle

def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()

def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}

plugins.each { name, path ->
    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
    include ":$name"
    project(":$name").projectDir = pluginDirectory
}

其实把上面的步骤都做好后,再把flutter.sh文件232到258行注释掉,然后点击AndroidStudio下面的Terminal选项卡来切换到命令行模式,运行sh flutter.sh就能生成产物了,产物就在/Users/longdw/Documents/Flutter/flutterdemo/.android/Flutter/src/main/assets/下面。

二、编译出AAR文件

flutter.sh文件232到240行放开,然后再运行一次sh flutter.sh,然后我们进入/Users/longdw/Documents/Flutter/flutterdemo/.android/Flutter/build/outputs/aar/会看到flutter-release.aar文件,进行到这一步大家可以把这个文件拷贝到一个纯原生的Android项目中,然后把它放到xxx/app/libs下面,然后修改app的build.gradle

android{
...
}
repositories {
    flatDir {
        dir 'libs'
    }
}

dependencies {
    implementation(name: 'flutter-release', ext: 'aar')
}

至此,我们其实就可以引用flutter中所有的相关内容。当然距离我们的目标还差最后一步。

三、上传到nexus并引用

nexus的安装我们看这篇文章。安装很简单,安装好后我们再切换回来。

接下来我们在flutterdemo/bak/下面新建一个config文件夹,然后把下面这个文件拷贝进去

uploadArchives.gradle

apply plugin: 'maven'

// 这里不需要artifacts,uploadArchives命令会自动生成并上传./build/outputs/flutter-release.aar,不然出现下面错误
// A POM cannot have multiple artifacts with the same type and classifier
//artifacts {
//    archives file('./build/outputs/flutter-release.aar')
//}

final def localMaven = "1".equals(CLOUND_MAVEN) //true: 发布到本地maven仓库, false: 发布到maven私服

final def artGroupId = GROUP
final def artVersion = VERSION_NAME
final def artifactId = ARTIFACT_ID

uploadArchives {
    repositories {
        mavenDeployer {
            println "==maven url: ${artGroupId}:${artifactId}:${artVersion}"

            if(localMaven) {
                repository(url: uri(project.rootProject.projectDir.absolutePath + '/repo-local'))
            } else {
                repository(url: MAVEN_URL) {
                    authentication(userName: MAVEN_ACCOUNT_NAME, password: MAVEN_ACCOUNT_PWD)
                }

                snapshotRepository(url: MAVEN_URL_SNAPSHOT) {
                    authentication(userName: MAVEN_ACCOUNT_NAME, password: MAVEN_ACCOUNT_PWD)
                }
            }

            pom.groupId = artGroupId
            pom.artifactId = artifactId
            pom.version = artVersion

            pom.project {
                licenses {
                    license {
                        name 'The Apache Software License, artVersion 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
            }
        }
    }
}

然后在bak下面新建gradle.properties文件如下

# 远程maven url和账号
MAVEN_URL=http://ip:8081/repository/maven-releases/
MAVEN_URL_SNAPSHOT=http://ip:8081/repository/maven-snapshots/
MAVEN_ACCOUNT_NAME=用户名
MAVEN_ACCOUNT_PWD=密码

#0=>maven 1=>source
FLUTTER_SOURCE=0
#0=>clound 1=>local
CLOUND_MAVEN=0

GROUP=com.替换下这里随便你自己,如longdw
VERSION_NAME=0.0.1
ARTIFACT_ID=名字随便取,如test-flutter

然后我们把flutter.sh文件242到258行注释放开,接着再执行sh flutter.sh,耐心等待几十秒,我们会发现如下输出

然后登录我们的nexus看下仓库是否上传成功

最后一步就是在app的build.gradle中引入

implementation 'com.xxx:test-xxx:0.0.1@aar'

修改project的build.gradle

allprojects {
    repositories {
        google()
        jcenter()

        //添加本地仓库地址
        maven { url 'http://ip:8081/repository/maven-public/' }
        
    }
}

同步下gradle,过一会儿,不出意外就完全成功了!

其实每一步都有原理在里面,需要深究的话还有很多东西可以学,接下来我再研究下iOS下怎么打包集成。

\n addFatAArConfig # step 4.1 build products echo '编译出四个中间产物 ... ' flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir build/app/intermediates/flutter/release --release --extra-gen-snapshot-options=--print-snapshot-sizes if [ $? -eq 0 ]; then echo '编译中间产物 succeed !!!' else echo '编译中间产物出错 !!!' exit 1 fi # step 4.2 copy products echo '复制中间产物到项目目录下 ... ' mkdir ${projectDir}/.android/Flutter/src/main/assets cp build/app/intermediates/flutter/release/isolate_snapshot_data ${projectDir}/.android/Flutter/src/main/assets/isolate_snapshot_data cp build/app/intermediates/flutter/release/isolate_snapshot_instr ${projectDir}/.android/Flutter/src/main/assets/isolate_snapshot_instr cp build/app/intermediates/flutter/release/vm_snapshot_data ${projectDir}/.android/Flutter/src/main/assets/vm_snapshot_data cp build/app/intermediates/flutter/release/vm_snapshot_instr ${projectDir}/.android/Flutter/src/main/assets/vm_snapshot_instr # step 5.1 build assets echo '编译出assets资源文件文件 ...' flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --precompiled --asset-dir build/app/intermediates/flutter/release/flutter_assets --release if [ $? -eq 0 ]; then echo '编译出assets资源文件文件 succeed !!!' else echo '编译出assets资源文件文件出错......' exit 1 fi # step 5.2 copy assets echo '复制assets资源文件文件到项目目录 ...' mkdir ${projectDir}/.android/Flutter/src/main/assets/flutter_assets cp -r build/app/intermediates/flutter/release/flutter_assets/ ${projectDir}/.android/Flutter/src/main/assets/flutter_assets/ if [ $? -eq 0 ]; then echo '复制assets资源文件文件到项目目录 succeed ..' else echo '复制assets资源文件文件到项目目录, 出错 !!!' exit 1 fi # step 6 build aar ,生成 aar , 然后上传到maven echo 'build aar' cd ${projectDir}/.android ./gradlew flutter:assembleRelease if [ $? -eq 0 ]; then echo '打包成AAR 成功!!!' else echo '打包成AAR 出错 !!!' exit 1 fi ./gradlew flutter:uploadArchives #gradle clean flutter:assembleRelease uploadArchives --info if [ $? -eq 0 ]; then echo 'uploadArchives 成功!!!' else echo 'uploadArchives 出错 !!!' delFatAarConfig exit 1 fi # step 7 remove unused files echo 'remove assets/lib' cd ${projectDir}/.android/Flutter/src/main/ rm -rf assets rm -rf lib delFatAarConfig echo '<<<<<<<<<<<<<<<<<<<<<<<<<< 结束 >>>>>>>>>>>>>>>>>>>>>>>>>' echo '打包成功 : xxx.aar...................! ' exit

大家把flutter.sh文件放到项目的项目根目录下,如我的是flutterdemo/flutter.sh。

然后在项目根目录下新建bak文件夹,将下面几个文件放进去。

dependencies_gradle_plugin.gradle


setting_gradle_plugin.gradle


其实把上面的步骤都做好后,再把flutter.sh文件232到258行注释掉,然后点击AndroidStudio下面的Terminal选项卡来切换到命令行模式,运行sh flutter.sh就能生成产物了,产物就在/Users/longdw/Documents/Flutter/flutterdemo/.android/Flutter/src/main/assets/下面。

二、编译出AAR文件

flutter.sh文件232到240行放开,然后再运行一次sh flutter.sh,然后我们进入/Users/longdw/Documents/Flutter/flutterdemo/.android/Flutter/build/outputs/aar/会看到flutter-release.aar文件,进行到这一步大家可以把这个文件拷贝到一个纯原生的Android项目中,然后把它放到xxx/app/libs下面,然后修改app的build.gradle


至此,我们其实就可以引用flutter中所有的相关内容。当然距离我们的目标还差最后一步。

三、上传到nexus并引用

nexus的安装我们看这篇文章。安装很简单,安装好后我们再切换回来。

接下来我们在flutterdemo/bak/下面新建一个config文件夹,然后把下面这个文件拷贝进去

uploadArchives.gradle


然后在bak下面新建gradle.properties文件如下


然后我们把flutter.sh文件242到258行注释放开,接着再执行sh flutter.sh,耐心等待几十秒,我们会发现如下输出

然后登录我们的nexus看下仓库是否上传成功

最后一步就是在app的build.gradle中引入


修改project的build.gradle


同步下gradle,过一会儿,不出意外就完全成功了!

其实每一步都有原理在里面,需要深究的话还有很多东西可以学,接下来我再研究下iOS下怎么打包集成。