一、前言
上一篇文章Android 混合Flutter之原始碼整合方式有優點和也有缺點:
優點
- 1.簡單快捷,Google原生支援
- 2.開發除錯方便,和原生互動較多或需要依賴原生資料環境的時候特別能體現出來
缺點
- 1.團隊所有人都可能要會
Flutter
並且都要安裝Flutter
環境 - 2.需要對現有的編譯體系做出修改,也就是要同時編譯
Flutter
專案和Native
專案 - 3.
Flutter
會直接侵入到Native
專案中去 - 4.編譯速度慢
Android
混合Flutter
除了上面所說的原始碼整合方式還有沒有其他方式呢?答案肯定是有的,那就是Flutter
以產物的方式整合到Native
,簡而言之將開發的Flutter
專案單獨編譯成aar檔案,然後以元件的形式被主工程(Native工程)依賴,aar
檔案可以以maven方式(遠端方式)的依賴,本文主要為了體驗產物整合和原始碼整合方案對比,就先用本地依賴的方式來整合。
二、Flutter專案
1.編寫Flutter專案程式碼
這裡和原始碼整合不同的是在New Flutter Project
選擇的是Flutter Application
而不是Flutter Module
:
Flutter
專案的dart
檔案拉到這個專案中,程式碼就不貼出來了,主要是根據路由去跳轉不同的頁面。
2.build.gradle
首先看android
目錄下build.gradle
:
app
模組下也有這個檔案,這個檔案是app
模組的gradle
構建指令碼,一般用來管理app包名、版本號以及新增修改依賴庫,在Flutter
專案中,這個檔案是由Flutter SDK生成的,相比原生安卓工程有些許不同,當然如果你根據gradle
的知識體系來理解就行,下面看看這個檔案:
//取得`local.properties`中的關於Flutter相關屬性
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
//獲取flutter.sdk資訊
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
//獲取flutter.versionCode
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
//獲取flutter.versionName
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
//指定為應用程式模組
apply plugin: 'com.android.application'
//引用flutter.gradle
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
//編譯版本
compileSdkVersion 28
//lint配置
lintOptions {
disable 'InvalidPackage'
}
//基本配置資訊 包名,最低支援版本號 版本號 版本名字等
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutter_app"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
//可以增加簽名資訊
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
複製程式碼
apply from:"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
這句話是引入flutter.gradle
配置模組,可以這麼理解向普通Android工程打包流程插入一些Flutter Task
任務,簡單的話用一下一張圖描述:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
android {
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
apply plugin:FlutterPlugin
class FlutterPlugin implements Plugin<Project>{...}
class FlutterExtension {...}
abstract FlutterTask extends BaseFlutterTask{...}
gradle.useLogger(new FlutterEventLogger)
class FlutterEventLogger extends BuildAdapter implements TaskExecutinListener{...}
複製程式碼
flutter.gradle
配置了一個名為FlutterPlugin
的外掛,這個外掛實現了Plugin<Project>
介面的apply
方法,這是標準的gradle plugin
,那麼它肯定會定義一些task
和必要的依賴,在addFlutterTask
這個方法可以體現:
.....
// We know that the flutter app is a subproject in another Android
// app when these tasks exist.
//我們知道flutter應用程式是另一個安卓系統的子專案
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
dependsOn compileTasks
if (packageAssets && cleanPackageAssets) {
//擋在flutter模組中,存在cleanPackageAssets和packageAssets時
dependsOn packageAssets
dependsOn cleanPackageAssets
into packageAssets.outputDir
} else {
//依賴於mergeAssets任務
dependsOn variant.mergeAssets
//依賴於cleanAssets任務
dependsOn "clean${variant.mergeAssets.name.capitalize()}"
variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}")
into variant.mergeAssets.outputDir
}
compileTasks.each { flutterTask ->
//執行flutterTask的getAssets方法
with flutterTask.assets
}
}
//processResource依賴於copyFlutterAssetsTask
variant.outputs.first().processResources.dependsOn(copyFlutterAssetsTask)
複製程式碼
從上面原始碼可以看出processResource
這個Task依賴於copyFlutterAssetsTask
,意思是要先執行完copyFlutterAssetsTask
才能執行processResource
,看英文意思就把flutter
相關Task加到gradle
的編譯流程中,另外copyFlutterAssetsTask
依賴了mergeAssets
和flutterTask
,也就是當mergeAssets
(Android的assets處理完成後)和flutterTask
(flutter編譯完)和執行完,Flutter
產物就會被copyFlutterAssetsTask
根據debug還是release複製到build/app/intermediates/merged_assets/debug/mergeDebugAssets/out
或者build/app/intermediates/merged_assets/release/mergeReleaseAssets/out
,Flutter
的編譯產物,具體是在flutterTask
的getAssets
方法指定的:
class FlutterTask extends BaseFlutterTask {
@OutputDirectory
File getOutputDirectory() {
return intermediateDir
}
CopySpec getAssets() {
return project.copySpec {
from "${intermediateDir}"
include "flutter_assets/**" // the working dir and its files
}
}
......
}
複製程式碼
也就是說,這些產物就是build/app/intermediates/flutter/xxx
(xxx指debug或者release)下面的flutter_assets/
目錄中的所有內容,那現在在命令列輸入打包命令flutter build apk
,會編譯生成apk檔案,路徑位於build/app/outputs/apk/release/app-release.apk
,注意如果你輸入flutter build apk
,實際預設打release
包,也就是等價於flutter build --release
,如果需要打debug
包,可以輸入flutter build apk --debug
:
Flutter
構建程式碼:
final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
if (buildSharedLibrary || platform == TargetPlatform.ios) {
// Assembly AOT snapshot.
outputPaths.add(assembly);
genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
genSnapshotArgs.add('--assembly=$assembly');
} else {
// Blob AOT snapshot.
final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');
final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]);
genSnapshotArgs.addAll(<String>[
'--snapshot_kind=app-aot-blobs',
'--vm_snapshot_data=$vmSnapshotData',
'--isolate_snapshot_data=$isolateSnapshotData',
'--vm_snapshot_instructions=$vmSnapshotInstructions',
複製程式碼
看看debug
模式下的產物:
isolate_snapshot_instr
和vm_snapshot_instr
,因為在debug
下只會執行一個命令flutter build bundle
,它會生成assets
、vm_snapshot_data
、isolate_snapshot_data
。release
模式下,會執行兩個命令:flutter build aot
,flutter build bundle --precomiled
,Android
預設使用app-aot-blobs
,這種模式會生成isolate_snapshot_data
、isolate_snapshot_instr
、vm_snapshot_data
和vm_snapshot_instr
四個檔案,多生成兩個檔案只要是為了執行速度更快。
3.產物分析
3.1.assets資料夾
assets
資料夾有isolate_snapshot_instr
,flutter_assets
,vm_snapshot_data
,vm_snapshot_instr
。
flutter_assets
:是Flutter
工程產生的assets
檔案,包含字型檔案,協議等。isolate_snapshot_instr
:包含由Dart isolate
執行的AOT程式碼。isolate_snapshot_data
:表示isolates堆儲存區的初始狀態和特定的資訊,和vm_snapshot_data
配合,更快啟動Dart_VM。vm_snapshot_data
:表示isolates之間共享的Dart堆儲存區的初始狀態,用於更快的啟動Dart VM。vm_snapshot_instr
:包含VM中所有的isolates之間的常見例程指令。
3.2.lib資料夾
lib資料夾是特定平臺(arm或者x86)的so檔案,Flutter
在Android
平臺下會預設生成arm-v7
架構的so庫,debug模式下同時生成x86_64、x86和arm64-v8a的so檔案,當然有的專案可能配置了
ndk{
abiFilters 'armeabi'
}
複製程式碼
為了解決so對其問題,需要在Flutter
專案中手動armeabi
的so檔案,這樣的話打包出來就aar包含了armeabi
的so檔案,這個armeabi
的so檔案可以拷貝armeabi-v7
下面的,一般情況下他們兩個是沒什麼區別,在app目錄下建立libs/armeabi,然後將libflutter.so
拷貝到armeabi的目錄下,然後在gradle中配置
android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
複製程式碼
因為Flutter SDK
版本速度很快,每個版本打出的so檔案可能稍有不同,所有隻要升級sdk可能就需要拷貝so檔案,比較麻煩,所以可以監聽打包aar的任務來進行自動拷貝,在gradle檔案中配置以下程式碼
//以下任務為了拷貝so 因為Flutter預設只生成v7的so
task copyFlutterSo(dependsOn: 'transformNativeLibsWithSyncJniLibsForRelease', type: Copy) {
//${buildDir} = /Users/xueshanshan/project/flutter/flutter_debug_library/build/app
def dir = "${buildDir}/intermediates/library_and_local_jars_jni/release"
from "${dir}/armeabi-v7a/libflutter.so"
into "${dir}/armeabi/"
}
複製程式碼
本文暫時還不需要用到這兩步。
4.打包aar檔案
上面通過編譯命令得到apk,那麼如果想打包aar,只要把app/build.gradle
中的apply plugin: 'com.android.application'
改為apply plugin: 'com.android,library'
並且把applicationId "com.example.flutter_app"
註釋
android
目錄下的AndroidManifest.xml
把android:label="xxx"
和android:name="xxxxx"
註釋掉:
在Terminal
執行下面命令,就能得到app-release.aar
檔案
- flutter clean
- cd android
- ./gradlew assembleRelease
三、Android專案
首先建立完Android專案,將上面打包成功的aar檔案
以普通的aar
整合到Android專案中去,首先將aar
檔案拷貝到libs
目錄下:
app
模組下配置build.gradle
,對aar
檔案的依賴
這時候你會發現沒有Flutter
類和FlutterFragment
,在Android 混合Flutter之原始碼整合方式有提過,建立Flutter Module
的時候,在.android
->Flutter
->io.flutter
->facade
會生成兩個java
檔案,分別是Flutter
和FlutterFragment
:
- Flutter:Android應用程式中使用Flutter的主要入口點
- FlutterFragment:Fragment來管理FlutterView
下面把這兩個檔案複製過來:
最後Android
原生呼叫Flutter
方式:
- 繼承FlutterActivity
@Override
public FlutterView createFlutterView(Context context){
getIntentData();
WindowManager.LayoutParams matchParent = new WindowManager.LayoutParams(-1, -1);
//建立FlutterNativeView
FlutterNativeView nativeView = this.createFlutterNativeView();
//建立FlutterView
FlutterView flutterView = new FlutterView(FlutterMainActivity.this,(AttributeSet)null,nativeView);
//給FlutterView傳遞路由引數
flutterView.setInitialRoute(routeStr);
//FlutterView設定佈局引數
flutterView.setLayoutParams(matchParent);
//將FlutterView設定進ContentView中,設定內容檢視
this.setContentView(flutterView);
return flutterView;
}
複製程式碼
- 繼承AppCompatActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceStae){
super.onCreate(savedInstanceStae);
String route = getIntent().getStringExtra("_route_");
String params = getIntent().getStringExtra("_params_");
JSONObject jsonObject = new JSONObject();
try{
jsonObject.put("pageParams",params);
} catch(JSONException e){
e.printStackTrace();
}
//將FlutterView設定進ContentView中,設定內容檢視
//建立FlutterView
flutterView = Flutter.createView(this,getLifecycle(),route + "?" + jsonObject.toString());
//設定顯示檢視
setContentView(flutterView);
//外掛註冊
registerMethodChannel();
}
複製程式碼
- Fragment方式:
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){
Log.d(TAG,"onCreateView-mRoute:"+mRoute);
mFlutterView = Flutter.createView(getActivity(),getLifecycle(),mRoute);
//綜合解決閃屏,佈局覆蓋問題
mFlutterView.setZOrderOnTop(true);
mFlutterView.setZOrderMediaOverlay(false);
mFlutterView.getHolder().setFormat(Color.parseColor("#00000000"));
//註冊channel
// GeneratedPluginRegistrant.registerWith(mFlutterView.getPluginRegistry());
//返回FlutterView
return mFlutterView;
}
複製程式碼
實際效果如下圖:
注意
這裡會牽扯到如果Flutter
工程依賴了第三方的Flutter plugin
那麼打包aar
檔案的時候是無法把Plugin
內容打進去的,網上有文章說可以用fataar-gradle-plugin或者fat-aar-android,找遍gradle
沒找到修改的地方,可以採用這兩篇文章把flutter專案作為aar新增到已有的Android工程上和Flutter混編一鍵打包並上傳maven的方法來實現。
四、總結
如果想要以混編的方式來開發專案,可以自行根據這兩種方案的特點來選擇,下面附上兩種方案的優缺點: