likes
comments
collection
share

一招解决Flutter离线化嵌入安卓项目的问题(2)

作者站长头像
站长
· 阅读数 20

概述

但是,如果我们有 flutter工程代码或者配置的改动,或者更改了flutterSDK,那么我们必须手动将 flutter的最终产物一个一个拷贝到 安卓工程中,这个显然不是我们想要的结果。

作为一个有追求的程序员,要把能自动化的操作尽可能由程序或者脚本来完成,减少人为操作导致的差错。

谁适合阅读

如果你是安卓开发+flutter开发,你的团队中有安卓,iOSLeader要求你在项目中嵌入flutter模块来实现混合开发,但是不能对不会flutter的那些人有影响,那么 请放心阅读此文,一定有帮助。

Flutter产物回顾

flutter产物 aar

产自 flutter build aar命令,命令执行完成且无错误之后,检查 \build\host\outputs\repo目录,从中找出所有release名字的aar包。aar数量的多少,与flutter引用 插件依赖的数量有关,引入的插件越多,我们需要同步的aar越多。

多种CPU架构下的 libflutter.so

产自 flutter build apk --release ,命令执行完毕且正常无误之后,检查 build/host/outputs/apk目录,找到其中的 app-release.apk文件,解压它,找到 libs 目录,然后提炼出每个cpu架构下的 libflutter.so 文件,保持原有的目录结构,删掉其中的 libapp.so ,最后组成一个 jnilibs目录,放置到安卓工程中,记得配置 sourceSetsjniLibs.srcDirs 指向 src/main/jniLibs

flutter框架代码的jar

产物来自 本机的flutterSDK缓存文件,与当前的flutterSDK版本有关,通常在 C:\Users\xxx.gradle\caches\modules-2\files-2.1\io.flutter\flutter_embedding_debug目录下,处理方式为:清空该目录,然后 重新 flutter run 一个工程,则可以生成新的缓存文件,在你本地存在多个版本flutterSDK的时候,可以选择这种方式。原本以为这个文件是存在于flutter SDK内部,但是搜索以后发现并没有,所以猜测,此jar包也是flutterSDK在运行项目时临时从网络上下载下来存放在 本机缓存目录下的,属于远程依赖的性质。


自动化流程设计

首先明确我们设计自动化流程的目的:

在Flutter侧发生变化时,要能够一键发布到 原生工程。

就相当于 离线H5应用那种概念,将离线H5包文件整个从本地或者云端,直接拷贝到 原生工程内的正确位置,让原生打包时直接使用最新的离线包资源。

关键词

  1. 变化

    所谓变化,就是flutter侧的代码,资源,依赖,配置文件等,这些发生变化,都会导致flutter的产物发生变化。

  2. 一键发布

    要降低人为操作的难度,最好是降低到双击某个按钮就能实现的程度。

角色

除了两个关键词之外,还有两个角色我们要注意:

  1. Flutter开发者

    这个角色会自己安装flutter开发环境,并且能够进行fluttter程序开发,还要承担将 flutter离线产物 发布到工程内的职责。

  2. 普通安卓原生开发者

    一个研发团队,不能强制要求每个人都接纳flutter。在他们的概念里,flutter离线产物就是一个普通的 依赖包,随着编译打包,成为apk的一个部分。所以,flutter对他们而言实际上是无感的。

    他们也不会参与到flutter模块的开发工作中来。

任务拆分

分析到这里,我们要做的事情就很明确了。

  • 在 原生安卓工程内部,设计 gradle task:flutter_publish,它的功能是:

    1. 生成flutter最新产物,并将它们拷贝到原生工程中的 正确位置

      每次flutter侧有任何改动,都要将最新产物 发布到原生工程, 以便原生功能在下次打包时能带上最新的flutter模块。

    2. 将最新flutter产物,通过 git 上传到 独立于原生工程之外c的代码仓库

      之所以放到另外的代码仓库,而不是 原生安卓工程仓库,两个原因:

      首先,flutter产物毕竟是独立与原生安卓代码之外的插件,它的版本变化与原生安卓的版本变化没有必然联系。

      其次,虽然本文只是提到了 安卓侧的flutter产物,而flutter离线化嵌入方案要完整的话,iOS必须跟上,iOS的flutter产物也隶属同一个flutter工程,最好是放到同一个 git仓库 进行统一管理。

      PS: 这里使用git,而不是某些网盘或者文件托管平台,实际上也是因为git纯天然支持文件差异对比,发布新的flutter产物时,哪些文件有变化一目了然

  • 在 原生安卓工程内部,设计 gradle task:flutter_sync,它的功能是:

    最新的flutter产物git仓库上 下载下来,并拷贝到原生工程的正确位置。

这样就行了吗?

不,以上两个只是 核心工作,我们还需要做一些辅助工作来帮助提升工作效率。

  • 设计 gradle全局开关,isFlutterDebugging

    通常,在我们作为flutter开发者的角色来对flutter工程做修改时,让它为 true,此时为 flutter的开发模式,此时,原生工程依赖的是 flutter工程本身。而当flutter开发者完成了工作时,就可以使用 flutter_publish 任务来发布flutter最新产物,然后将 它改为false,在其他人打包时,直接就能使用 flutter最新产物,此时为 flutter的发布模式。而由于 flutter的开发模式与发布模式的依赖引用的细微差别,我们要将 isFlutterDebugging 运用在 原生gradle配置的关键位置,来让 flutter的开发模式与发布模式 自由切换,且不引起编译问题。

  • flutter_sync 任务与 assemble 任务产生依赖

    熟悉安卓打包命令的应该都知道,我们生成安卓的最终产物,apk,或者aar,aba等,都是通过 assemble 来完成的。我们要让 普通的,不熟悉flutter的开发者对flutter产物的到来产生无感的效果的话,就不能要求他们手动去调用 flutter_sync 任务之后再去assemble。所以,让这两个任务建立联系,每次assemble之前,都去自动 flutter_sync 是最好的方式,当然,为了避免无用功,我们可以在本地保存一个 flutter产物仓库的 最新提交记录的编码,每次去 flutter_sync 时,都先对比 是否有必要更新,没必要,则可以不用 执行 flutter_sync 任务(当然,本地完全没有 flutter_sync 过 的情况除外)

图形化解释

上面的说法比较模糊,我用一张图来解释:

一招解决Flutter离线化嵌入安卓项目的问题(2)

此图展示了一个普通的安卓项目 在 嵌入了Flutter模块前后的主要区别。

其中有两种角色。

一个 是普通开发者,他们只需要关注 原生工程的开发内容,对于Flutter 内容的存在完全是零感知。

另一种就是 Flutter 兼原生开发者,他们关注两侧的代码内容,对Flutter有开发权限,并负责 执行 flutter_publish 发布最新的flutter产物到 git仓库。

普通开发者无论是在 AndroidStudio中点击绿色三角形Run项目,或者是 手动执行 assemble任务来生成apk包,都会默认执行 flutter_sync 任务来同步flutter最新产物。这样,对于对Flutter陌生的普通开发者来说,flutter的存在都不会对他们有负面影响(开发环境也不需要有任何变动)。

必会的技术

要完成以上这些流程,我们得对如下技术点有一定程度的了解:

  1. Flutter 开发环境搭建以及基本开发
  2. Flutter 混合开发的主流框架,主要是FlutterBoost的 集成流程
  3. Android Gradle 基础以及 gradle 脚本开发
  4. Andrioid Apk 文件的主要结构

主要脚本代码

全局配置 flutter_config.gradle

ext {
    // flutter模块是否处于调试状态
    // - true  调试状态直接依赖   flutter工程,
    // - false 非调试状态则是依赖 flutter工程生成的aar包
    flutterModuleTestMode = false
    // flutter项目的名称
    flutterModuleName = "flutter_module"

    // 产物仓库地址
    gitRepoUrl = "ssh://git@codehub-dg-g.huawei.com:2222/zWX1245985/flutter_repo_just_for_test.git"
    // 仓库名称
    repoName = "flutter_repo_just_for_test"
    // 一个仓库可能存放不同的flutter项目的产物,故 使用proName来区分
    proName = "kbz_b"
}

文件(夹)的拷贝

def copyFile(sourceFilePath, destinationFilePath) {
    def sourceFile = file(sourceFilePath)
    def destinationFile = file(destinationFilePath)
    ant.copy(todir: destinationFile.parent) {
        fileset(file: sourceFile)
    }
}

void copyFiles(String sourceDirPath, String targetDirPath) {
    File sourceDir = file(sourceDirPath)
    File destinationDir = file(targetDirPath)

    if (!sourceDir.exists()) {
        println("原始目录不存在")
        return
    }

    if (!destinationDir.exists()) {
        destinationDir.mkdirs()
    }

    FileTree sourceFiles = fileTree(dir: sourceDir)

    sourceFiles.each { sourceFile ->
        File destinationFile = new File(destinationDir, sourceDir.relativePath(sourceFile).toString())
        copyFile(sourceFile, destinationFile)
    }
}

生成aar

void buildAar(String flutterRoot) {
    def commandCd = "cd ${flutterRoot}"
    def commandFlutterBuildAar = "flutter build aar --no-profile"
    def result = project.exec {
        commandLine "cmd", "/c", "$commandCd && $commandFlutterBuildAar"
    }
    println "打aar包的命令执行完成,exitValue 是: ${result.exitValue}"
    if (result.exitValue != 0) {
        println "aar打包失败,尝试在 $flutterRoot\.android\gradle.properties中添加 org.gradle.java.home=D://env//androidStudio//jbr "
        println "打开笔记本的热点功能有可能也会引起打aar失败,请关闭热点再试 "
    }
}

生成apk

void buildApk(String flutterRoot) {
    def commandCd = "cd ${flutterRoot}"
    def commandFlutterBuildApk = "flutter build apk --release"
    def result = project.exec {
        commandLine "cmd", "/c", "$commandCd && $commandFlutterBuildApk"
    }
    println "打apk包的命令执行完成,exitValue 是: ${result.exitValue}"
}

克隆flutter产物的 git仓库

/**
 * 将 flutter离线依赖资源下载到我项目本地
 *
 */
boolean cloneFlutterOfflineRelay() {

    println("cloneFlutterOfflineRelay->$repoName")

    // 检查 repoName 目录是否存在,如果存在,先删除
    def flutterProductRepo = file("$repoName")
    if (flutterProductRepo.exists()) {
        def deleteRes = deleteRepo()
        if (!deleteRes) {
            println("$repoName 目录删除失败,请手动删除后重试")
            return false
        }
    }

    println("gitRepoUrl-> $gitRepoUrl")

    def result = project.exec {
        commandLine "cmd", "/c", "git clone $gitRepoUrl"
    }
    println "clone 命令执行完成,exitValue 是: ${result.exitValue}"
    println "从云端下载flutter模块离线资源成功"

    return true
}

推送本地改动到git远端

void pushFlutterRepoCommit() {
    def commandCd = "cd $repoName"
    def cmdAdd = "git add --all"
    def cmdCommit = "git commit -m "sync""
    def cmdPush = "git push origin"
    def result = project.exec {
        commandLine "cmd", "/c", "$commandCd && $cmdAdd && $cmdCommit && $cmdPush"
    }
    println "提交的命令执行完成,exitValue 是: ${result.exitValue}"
}
将 libs和so拷贝到原生工程的对应位置
/**
 * 将所有的aar包放置到boost_core模块的libs中
 */
void applyAar() {
    def localAar = "libs" // so将要拷贝到的目录
    def remoteAar = "$repoName/$proName/libs" // apk解压之后的so存放目录

    copyFiles(localAar, remoteAar)
    println "同步aar资源成功"
}

/**
 * 将所有的aar包放置到boost_core模块的libs中
 */
void applySo() {
    def localSo = "src/main/jniLibs/" // so将要拷贝到的目录
    def remoteSo = "$repoName/$proName/jniLibs" // apk解压之后的so存放目录
    if (file(remoteSo).exists()) {
        delete remoteSo
    }
    copyFiles(localSo, remoteSo)
    println "同步so资源成功"
}

完整Demo

了解一项架构,最重要的就是要有 实验环境,此Demo包含了完整的Flutter混合架构的可执行项目。

github地址如下:github.com/18598925736…

  • 注意阅读README.md
  • 注意阅读README.md
  • 注意阅读README.md

如运行有问题,欢迎留言!

转载自:https://juejin.cn/post/7269042529249116218
评论
请登录