對於已有工程想要嘗鮮 Flutter, 很多公司給出了最佳實踐方案, android 中是使用 aar 加入項目中, 這樣原生開發對於 flutter 環境就沒有要求了, 只要 flutter 打包后上傳 maven 即可, 但是這部分的過程坑很多, 后面我會再補充這種方案

我也摸索了一個實踐方案, 將所有項目的 aar 由 flutter 方打包 aar 后將 aar 置入某一個固定位置 ,並置入一個 git 庫管理, 然后 android 原生方直接 pull 后引入項目即可

高能預警: 本篇會結合 flutter, android, aar, gradle, maven, docker 的知識來完成所有的步驟

並不是每一個都會詳細說明, 如果有不明白的可以在 https://www.kikt.top 的本文下面留言, 我會更新文章或給予解答, 其他渠道的可能不會有時間看

開發環境

本人設備環境

MacOS 10.13.6 (17G65)
flutter: Flutter 1.5.4-hotfix.2 • channel stable

2019-10-25 更新說明: 這篇文章因為發布時效的原因, 當時還沒有 `$ flutter build aar` 這個命令 所以本人並沒有實測兩個東西的優劣性 
Bash

預計需要的環境

xcode
android sdk
gradle
android studio
flutter sdk
docker # 這個 
Bash

這些環境我默認你都有, 沒有的話本篇不講

windows 用戶? 對不住, 自己找尋其中的差別吧…

flutter

創建 flutter module

使用命令行創建:

$ flutter create -t module flutter_module

cd flutter_module flutter build apk 
Bash

這里理論上會生成一個 aar

tree .android/Flutter/build/outputs
.android/Flutter/build/outputs
├── aar
│   └── flutter-release.aar
└── logs
    └── manifest-merger-release-report.txt
Bash

嗯,就這個東西

我們其實可以直接把這個 aar 放在宿主中,然后通過配置 aar 本地引用來直接使用這個工程, 但是這樣可能並不利於持續集成

所以我們要用到 maven 這個利器

ps: 這里有個坑, 就是純 flutter 項目可以, 但是如果你的 flutter 項目包含了對於第三方項目的依賴, 則 aar 可能不會包含其他的內容, 我們放在最后面再想辦法解決

maven 的處理方式(看看就行,作為錯誤嘗試的步驟)

本篇主要講的是 maven 的方式, 沒有原生 plugin 的很簡單, 但是有原生 plugin 的 flutter 步驟過於復雜, 最終沒實現, 當然理論上肯定是可以實現的

因為本篇講解的是本人解決 flutter 附着到已有工程的嘗試,所以將放棄的過程也記錄下來, 如果你只是想看最終的實現方案可以跳過本篇和后續所有涉及到 maven 的步驟

maven 是一個包管理工具

如果你公司有自己的私服, 則跳過這一章直接看下一章, 我這里只是使用 docker 創建一個 maven 私服環境

使用的鏡像是 sonatype/nexus3

配置

可選: $ docker pull sonatype/nexus3

我比較熟悉的有兩種方式:

命令行直接運行

docker run --name test_nexus -d -p 8099:8081 -v /Volumes/Evo512/docker/nexus/nexus-data:/nexus-data sonatype/nexus3 
Bash

使用 docker-compose

version: '2' services: my-nexus: image: sonatype/nexus3 ports: - 8099:8081 networks: - nexus-net volumes: - /Volumes/Evo512/docker/nexus/nexus-data:/nexus-data networks: nexus-net: driver: bridge 
YAML
docker-compose -d up
Bash

使用 docker-compose 就是類似於配置文件的方式

運行

在瀏覽器打開 http://localhost:8099

登錄的用戶名密碼,默認是 admin admin123

點開 maven, 毛也沒有

20190614095229.png

上傳 aar

使用 gradle 上傳 aar

使用 android studio 打開 flutter_module 下的.android 目錄, 經過一頓同步得到的可能是這樣的:20190614111354.png一片空白毛都沒有…

這時候請 close, 重新打開, 現在是這個鬼樣子的

20190614111457.png

采用 project 視圖模式

20190614111535.png

在.android 下增加一個 gradle 文件,名字自取

比如我的就叫 update_aar.gradle

apply plugin: 'maven' def GROUP = 'top.kikt.flutter_lib' def ARTIFACT_ID = 'module_example' def VERSION_NAME = "1.0.0" def SNAPSHOT_REPOSITORY_URL = 'http://localhost:8099/repository/maven-snapshots/' def RELEASE_REPOSITORY_URL = 'http://localhost:8099/repository/maven-releases/' def REPOSITORY_URL = VERSION_NAME.toUpperCase().endsWith("-SNAPSHOT") ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL def NEXUS_USERNAME = 'admin' def NEXUS_PASSWORD = 'admin123' afterEvaluate { project -> uploadArchives { repositories { mavenDeployer { pom.groupId = GROUP pom.artifactId = ARTIFACT_ID pom.version = VERSION_NAME repository(url: REPOSITORY_URL) { authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD) } } } } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } //解決 JavaDoc 中文注釋生成失敗的問題 tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') options.addStringOption('encoding', 'UTF-8') options.addStringOption('charSet', 'UTF-8') } artifacts { archives androidSourcesJar archives androidJavadocsJar } } 
Groovy

這個文件呢, 就是上傳用的 gradle 文件, 來源於網絡

前幾個 def 要根據你的 maven 來修改, 包名, 端口, 用戶名,密碼

接着引入 gradle 文件到項目中

修改: Flutter/build.gradle

android{ /// .... } apply from: "${rootDir.path}/update_aar.gradle" 
Groovy

按照下圖點擊img

可能會報錯

11:58:23: Executing task 'uploadArchives'... Executing tasks: [uploadArchives] FAILURE: Build failed with an exception. * Where: Settings file '/Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/settings.gradle' line: 7 * What went wrong: A problem occurred evaluating settings 'android_generated'. > /Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/Flutter/include_flutter.groovy (/Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/Flutter/include_flutter.groovy) * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 0s 11:58:23: Task execution finished 'uploadArchives'. 
Bash

似乎是由於路徑不對的原因, 請使用如下的方式修改 setting.gradle:

// Generated file. Do not edit. include ':app' rootProject.name = 'android_generated' setBinding(new Binding([gradle: this])) //evaluate(new File('include_flutter.groovy')) evaluate(new File("$rootDir.path/include_flutter.groovy")) 
Groovy

同步 gradle 后

接着雙擊20190614120303.png

就可以上傳成功了

然后打開 nexus 查看: http://localhost:8099/#browse/search/maven

20190614130245.png

20190614130414.png有顯示, 說明這個 aar 上傳是成功的

后面再上傳更改版本號即可

Android 項目(host)

新建項目

20190614105539.png

引入 maven 依賴

添加倉庫

根目錄 build.gradle, 根據節點增加一個 maven 倉庫:

allprojects { repositories { google() jcenter() maven { url 'http://localhost:8099/repository/maven-releases/' } } } 
Groovy

引入庫, 在 nexus 的管理界面里可以查看引用方式:

20190614130617.png

接着在app/build.gradle中修改


dependencies { // ... implementation 'top.kikt.flutter_lib:module_example:1.0.0' } 
Groovy

經過 sync 以后,使用 project 視圖, 可以找到這個庫:

20190614131424.png

編碼

新建 MyFlutterActivity.java

package top.kikit.androidhost; import android.os.Bundle; import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; /// create 2019-06-14 by cai public class MyFlutterActivity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); } } 
Java

添加到清單文件

<application> <activity android:name=".MyFlutterActivity" /> </application> 
XML

修改 MainActivity.java

package top.kikit.androidhost; import android.content.Intent; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { 
Dart

這里模擬一進來直接進 FlutterActivity 的場景

建議你的 Android 同事在合適的時機調用 Flutter.startInitialization(this.getApplicationContext()); 這個是官方給出的初始化 flutter 引擎的代碼, 否則首屏可能會慢

運行項目

初次運行可能會報錯 提示一個 androidO 什么的玩意

兩種方案

  1. minSDK 修改為 26, 這個簡直不科學
  2. 在 app/build.gradle 下的 android 節點下增加這個代碼
android{ compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } } 
Groovy

將源碼和目標代碼等級都設置為 1.8

嗯 這里插一句, 我的 host 使用的是 androidX, 而 flutter 使用的是 android.support, 所以需要按照 androidX 的遷移流程修改一下, 如果你新建項目的時候勾選了 androidX, 則這里應該不用修改

androidX 的問題可以查看我的另一篇文章, 雖然是 flutter 分類下的,但是對於普通 android 工程也適用

運行結果如下:20190614133601.png

在 flutter 中添加帶有原生功能的庫

這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件
這里注意!!!!!!, 請先備份前面幾個文件

因為一旦 flutter packages get, 則 前面的文件就木有了

在 flutter 中添加庫

這里簡單舉例一下, 使用一個比較常用的shared_preferences

修改 flutter 的 yaml 文件

dependencies: shared_preferences: ^0.5.3+1 
YAML

$ flutter packages get

這一步后, 之前的那幾個文件沒有了…

建議: 把 build.gradle 和 setting.gradle 復制到 module 級別的某個目錄下, 比如叫 template

然后用腳本來做這個上傳的事情

  1. 復制模板到對應目錄
  2. 通過環境變量設置 aar 的版本號
  3. 使用 gradle 命令來完成插件的調用

上傳新版本的 aar

修改版本號為 1.0.1

這里上傳成功了

到 android host 中用了一下, 果不其然和網上的朋友們說的一樣報錯了

ERROR: Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT. Show Details Affected Modules: app ERROR: Unable to resolve dependency for ':app@debugAndroidTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT. Show Details Affected Modules: app ERROR: Unable to resolve dependency for ':app@debugUnitTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT. Show Details Affected Modules: app 
Bash

查看對應的 pom.xml(我這里是 1.0.2),道理是一樣的

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>top.kikt.flutter_lib</groupId> <artifactId>module_example</artifactId> <version>1.0.2</version> <packaging>aar</packaging> <dependencies> <dependency> <groupId>io.flutter.plugins.sharedpreferences</groupId> <artifactId>shared_preferences</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.android.support</groupId> <artifactId>support-v13</artifactId> <version>27.1.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.android.support</groupId> <artifactId>support-annotations</artifactId> <version>27.1.1</version> <scope>compile</scope> </dependency> </dependencies> </project> 
XML

這里有一個 io.flutter.plugins.sharedpreferences 就是報錯的元凶了

思考解決方案

看到這里我感覺有如下的方案

  1. 將所有文件打包到同一個 aar 庫中, 然后再上傳(也就是網上那個 fat-aar 的方案)
  2. 修改 flutter 打包腳本, 然后將中間的三方庫產物(sp 插件)上傳至私服 maven, flutter 項目使用 api 的方式依賴這些庫, 完成 host=>flutter=>other plugin 的目的
  3. 不用 maven, 只用 aar

個人第一感覺, 覺得第一個實施起來可能會簡單一些, 先嘗試一下

fat-aar

這個找到了兩個項目:

一個 gradle 文件的方式: https://github.com/adwiv/android-fat-aar

一個是 plugin 的方式: https://github.com/Vigi0303/fat-aar-plugin

但是都要用到一個類似embed這樣的關鍵字來替換 compile(api/implementation), 無奈找遍 gradle 沒找到修改的地方, 只能暫時放棄

flutter 的插件庫上傳至 maven

這個初始來看很可行.. 但仔細一想, 因為那個版本號的作祟, 需要改動的地方不算很少

每個插件包內的 gradle 文件都需要修改:

  1. 修改 version 版本號,這個應該是可以通過 環境變量/gradle 命令 來指定為佳, 不能指定的話理論上和 pub 的版本號相同也可以, 如果是 git 依賴, 就用 ref, path 依賴就很比較難自動取了
  2. 上傳腳本,這個要讀取上面的版本號, 還要讀取一個

為什么要修改版本號呢? flutter 依賴的插件的版本號會被帶到 aar 對應的 maven 庫中的 pom.xml 文件中

這里要插一句: pom.xml 中依賴的版本號是定義在每個插件自己的 build.gradle 中的,如下面的連接那樣

如下所示: https://github.com/OpenFlutter/flutter_image_compress/blob/e841181d16df44b94c45e77ee1dcd36ebdc27905/android/build.gradle#L1-L2

https://github.com/flutter/plugins/blob/e9766e668b4a84ac526414e26981a23c661aff18/packages/shared_preferences/android/build.gradle#L14-L15

我這里說需要修改的就是這個版本號,否則你上傳 maven 的 flutter 庫的版本號和插件的 maven 版本號沒對上的話,依然會報錯

修改版本號並上傳需要遵循如下的步驟:

  1. 讀取本地.flutter-plugins文件的內容,將其中的版本號字段取出來
  2. 找到插件文件夾,替換掉版本號字段的內容
  3. 將上傳插件的腳本復制至對應文件夾,並將版本號,group 名與插件統一
  4. 啟動上傳腳本
  5. 將對原生文件的修改內容還原

為什么要做最后一步呢? 這種"從遠端"鏡像下來的東西,修改回去是一個好習慣, 因為修改了會破壞倉庫本身版本的完整性

解決方案-使用 aar 和 git 管理

這個就是我開篇說的解決方案, 不使用 maven, 只是打包出 aar, 集中起來, 置入 git 倉庫,如果有必要就打 tag 后 push 到遠端, 方便根據版本來引用

然后作為 android 原生方, 在 project 的 gradle 中引入 aar 庫即可, 當然如果你是大公司有自己的要求, 還是用上一種比較好

git 和 aar 引入也是很成熟的使用方案了, 無非就是如何拼接而已的問題, 何況這一步還可以通過 gradle 自動完成

處理 flutter 端

這次使用 dart 來作為腳本, 畢竟 dart 語言對於 flutter 開發者來說會很熟悉, 當然這一步可以用任何你熟悉的方式,比如: shell/python 等等, 這一步的執行需要將 dart 放入環境變量中

build_module.dart:

import 'dart:io'; var outputDir = Directory("../output"); var targetDir = Directory("../../flutter-aar"); Future main() async { List<AAR> list = []; outputDir.deleteSync(recursive: true); outputDir.createSync(recursive: true); var file = File("../.flutter-plugins"); var plugins = file.readAsLinesSync(); for (var value in plugins) { if (value.trim().isEmpty) { continue; } var splitArr = value.split("="); var name = splitArr[0]; var path = splitArr[1]; var aar = handlePlugin(name, path); list.add(aar); } var aar = await handleFlutter(); list.add(aar); handleAAR(list); } void handleAAR(List<AAR> list) { targetDir.deleteSync(recursive: true); targetDir.createSync(); list.forEach((aar) { var targetPath = "${targetDir.path}/${aar.aarName}"; var targetFile = aar.file.copySync(targetPath); print( '\ncopy "${aar.file.absolute.path}" to "${targetFile.absolute.path}"'); }); } AAR handlePlugin(String name, String path) { var result = Process.runSync("./gradlew", ["$name:assRel"], workingDirectory: "../.android"); print(result.stdout); var aarFile = File("$path/android/build/outputs/aar/$name-release.aar"); var aarName = aarFile.path.split("/").last; var pathName = "${outputDir.path}/$aarName"; var targetFile = aarFile.copySync(pathName); return AAR() ..file = targetFile ..aarName = aarName; } Future<AAR> handleFlutter() async { var processResult = await Process.run( "flutter", ["build", "apk"], workingDirectory: "..", runInShell: true, ); print(processResult.stdout); var name = "flutter-release.aar"; var file = File("../.android/Flutter/build/outputs/aar/flutter-release.aar"); var target = file.copySync("${outputDir.path}/$name"); return AAR() ..file = target ..aarName = name; } class AAR { String aarName; File file; String get noExtensionAarName => aarName.split(".").first; 
Dart

大概解釋下腳本的功能:

  1. 處理.flutter-plugins文件,獲取 android 所在目錄
  2. 執行flutter/.android下的 gradle 命令來生成 aar
  3. 根據插件所在目錄來獲取 aar 文件
  4. 打包 flutter 本身的 aar, 這一步因為一些資源的原因, 直接使用 flutter build apk, 會完成所有的中間產物的生成
  5. 將 插件和 flutter 的 aar 文件復制到 output/flutter-aar 文件夾下

output 文件夾就是我們作為 git 依賴使用的文件夾, 這個文件夾

命令: $ dart build_aar.dart

新建一個目錄用於存放 aar

因為 git submodule 的管理方式對於新手不友好, 所以使用更簡單一點的方案管理

新建一個目錄,把所有的 aar 文件都放在一起 (我的示例代碼是放在一個倉庫里的, 不過是同級目錄)

當前的目錄結構是這樣的:

tree -L 2 . ├── README.md ├── android-host │ ├── android-host.iml │ ├── app │ ├── build │ ├── build.gradle │ ├── gradle │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── local.properties │ └── settings.gradle ├── flutter-aar │ ├── flutter-release.aar │ └── shared_preferences-release.aar └── flutter_module ├── README.md ├── build ├── flutter_module.iml ├── flutter_module_android.iml ├── lib ├── output ├── pubspec.lock ├── pubspec.yaml ├── shell ├── template └── test 
Bash

這樣分級的好處是倉庫權限的分級:

android 組允許訪問 android-host 和 flutter-aar

flutter 組允許訪問 flutter_module 和 flutter-aar

我示例代碼是一個倉庫, 但實際上對於項目來說應該是 3 個倉庫為佳

修改 android 主工程

build.gradle:


def aarDir = "${rootProject.projectDir.path}/../flutter-aar" repositories { flatDir { dirs aarDir } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' def file = new File(aarDir) file.listFiles(new FilenameFilter() { @Override boolean accept(File dir, String name) { return name.endsWith("aar") } }).each { f -> def aar = f.name.split("\\.").first() println("f.name = ${f.name} , aar = $aar") api(name: f.name.split("\\.").first(), ext: 'aar') } } 
Groovy

這樣的情況下這個目錄就完成了對於所有 aar 文件的引用

總結一下所有修改

dart 腳本

  1. 復制我提供的倉庫下flutter_module/shell/build_module.dart到你的 flutter 下的 shell 目錄
  2. 修改這個 dart 腳本中的 targetDir 目錄到任何你想要的目錄(無論是直接到原生還是到單獨倉庫內)

原生部分修改

修改 build.gradle 加入對於 aar 的引用

這里使用倉庫還是直接在原生工程里看你們項目管理的要求

這一步可以從原生項目的 app/build.gradle 看到所有修改

運行腳本

總結一下我的運行步驟:

  1. 命令行在根目錄下執行 cd flutter_module/shell && dart build_module.dart
  2. 運行 android 項目

建議的步驟如下:

對於 flutter 開發者來說:

  1. cd flutter_project/shell && dart build_module.dart
  2. cd android-aar
  3. 操作 git 倉庫,上傳 aar

對於安卓原生來說:

  1. $ cd android-aar
  2. $ git pull
  3. 運行項目

后記

本篇詳細介紹了我是如何解決 flutter 添加到已有工程的方案, 雖然字數多, 但是實際引入並不復雜

可能有遺漏, 有不清楚的請在官方 blog 下評論留言, csdn 僅作為文章的同步發布平台, 評論可能沒有時間看

嗯,倉庫在這里: gitee

以上