一、環境搭建
首先,需要開發者按照原生Android、iOS的搭建流程搭建好開發環境。然後,去Flutter官網下載最新的SDK,下載完畢後解壓到自定義目錄即可。如果出現下載問題,可以使用Flutter官方為中國開發者搭建的臨時映象。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
為了方便使用命令列,還需要額外配置下環境變數。首先,使用vim命令開啟終端。
vim ~/.bash_profile
然後,將如下程式碼新增到.bash_profile檔案中,並使用source ~/.bash_profile
命令使檔案更改生效。
export PATH=/Users/mac/Flutter/flutter/bin:$PATH
//重新整理.bash_profile
source ~/.bash_profile
完成上述操作之後,接下來使用flutter doctor
命令檢查環境是否正確,成功會輸出如下資訊。
二、建立Flutter aar包
原生Android整合Flutter主要有兩種方式,一種是建立flutter module,然後以原生module那樣依賴;另一種方式是將flutter module打包成aar,然後在原生工程中依賴aar包,官方推薦aar的方式接入。
建立flutter aar有兩種方式,一種是使用Android Studio進行生成,另一種是直接使用命令列。使用命令列建立flutter module如下:
flutter create -t module flutter_module
然後,進入到flutter_module,執行flutter build aar
命令生成aar包,如果沒有任何出錯,會在/flutter_module/.android/Flutter/build/outputs
目錄下生成對應的aar包,如下圖。
build/host/outputs/repo
└── com
└── example
└── my_flutter
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── ...
└── flutter_debug
└── ...
當然,我們也可以使用Android Studio來生成aar包。依次選擇File -> New -> New Flutter Project -> Flutter Module生成Flutter module工程。
然後我們依次選擇build ->Flutter ->Build AAR即可生成aar包。
接下來,就是在原生Android工程中整合aar即可。
三、新增Flutter依賴
3.1 新增aar依賴
官方推薦方式
整合aar包的方式和整合普通的aar包的方式是一樣大的。首先,在app的目錄下新建libs資料夾 並在build.gradle中新增如下配置。
android {
...
buildTypes {
profile {
initWith debug
}
}
String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?:
"https://storage.googleapis.com"
repositories {
maven {
url '/Users/mac/Flutter/module_flutter/build/host/outputs/repo'
}
maven {
url "$storageUrl/download.flutter.io"
}
}
}
dependencies {
debugImplementation 'com.xzh.module_flutter:flutter_debug:1.0'
profileImplementation 'com.xzh.module_flutter:flutter_profile:1.0'
releaseImplementation 'com.xzh.module_flutter:flutter_release:1.0'
}
本地Libs方式
當然,我們也可以把生成的aar包拷貝到本地libs中,然後開啟app/build.grade新增本地依賴,如下所示。
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
...
//新增本地依賴
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(name: 'flutter_debug-1.0', ext: 'aar')
implementation 'io.flutter:flutter_embedding_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:armeabi_v7a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:arm64_v8a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:x86_64_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
}
io.flutter:flutter_embedding_debug來自哪裡呢,其實是build/host/outputs/repo生成的時候flutter_release-1.0.pom檔案中,
<groupId>com.example.flutter_library</groupId>
<artifactId>flutter_release</artifactId>
<version>1.0</version>
<packaging>aar</packaging>
<dependencies>
<dependency>
<groupId>io.flutter.plugins.sharedpreferences</groupId>
<artifactId>shared_preferences_release</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.flutter</groupId>
<artifactId>flutter_embedding_release</artifactId>
<version>1.0.0-626244a72c5d53cc6d00c840987f9059faed511a</version>
<scope>compile</scope>
</dependency>
在拷貝的時候,注意我們本地aar包的環境,它們是一一對應的。接下來,為了能夠正確依賴,還需要在外層的build.gradle中新增如下依賴。
buildscript {
repositories {
google()
jcenter()
maven {
url "http://download.flutter.io" //flutter依賴
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}
如果,原生Android工程使用的是元件化開發思路,通常是在某個module/lib下依賴,比如module_flutter進行新增。
在module_flutter build.gradle下配置
repositories {
flatDir {
dirs 'libs' // aar目錄
}
}
在主App 下配置
repositories {
// 詳細路徑
flatDir {
dirs 'libs', '../module_flutter/libs'
}
}
3.2 原始碼依賴
除了使用aar方式外, 我們還可以使用flutter模組原始碼的方式進行依賴。首先,我們在原生Android工程中建立一個module,如下圖。
新增成功後,系統會預設在settings.gradle檔案中生成如下程式碼。
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
然後,在app/build.gradle檔案中新增原始碼依賴。
dependencies {
implementation project(':flutter')
}
3.3 使用 fat-aar 編譯 aar
如果flutter 中引入了第三方的一些庫,那麼多個專案在使用flutter的時候就需要使用 fat-aar。首先,在 .android/build.gradle 中新增fat-aar 依賴。
dependencies {
...
com.github.kezong:fat-aar:1.3.6
}
然後,在 .android/Flutter/build.gradle 中新增如下 plugin 和依賴。
dependencies {
testImplementation 'junit:junit:4.12'
// 新增 flutter_embedding.jar debug
embed "io.flutter:flutter_embedding_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
// 新增 flutter_embedding.jar release
embed "io.flutter:flutter_embedding_release:1.0.0-e1e6ced81d029258d449bdec2ba3cddca9c2ca0c"
// 新增各個 cpu 版本 flutter.so
embed "io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:x86_64_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:x86_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
此時,如果我們執行專案,可能會報一個Cannot fit requested classes in a single dex file
的錯誤。這是一個很古老的分包問題,意思是dex超過65k方法一個dex已經裝不下了需要個多個dex。解決的方法是,只需要在 app/build.gradle 新增multidex即可。
android {
defaultConfig {
···
multiDexEnabled true
}
}
dependencies {
//androidx支援庫的multidex庫
implementation 'androidx.multidex:multidex:2.0.1'
}
五、跳轉Flutter
5.1 啟動FlutterActivity
整合Flutter之後,接下來我們在AndroidManifest.xml中註冊FlutterActivity實現一個簡單的跳轉。
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true" />
然後在任何頁面新增一個跳轉程式碼,比如。
myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(this)
);
}
});
不過當我執行專案,執行跳轉的時候還是報錯了,錯誤的資訊如下。
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.snbc.honey_app/io.flutter.embedding.android.FlutterActivity}: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2946)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
看報錯應該是初始化的問題,但是官方文件沒有提到任何初始化步驟相關的程式碼,查查Flutter 官方的issue,表示要加一行初始化程式碼:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
}
然後,我再次執行,發現報瞭如下錯誤。
java.lang.NoClassDefFoundError: Failed resolution of: Landroid/arch/lifecycle/DefaultLifecycleObserver;
at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:152)
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.setupFlutterEngine(FlutterActivityAndFragmentDelegate.java:221)
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onAttach(FlutterActivityAndFragmentDelegate.java:145)
at io.flutter.embedding.android.FlutterActivity.onCreate(FlutterActivity.java:399)
at android.app.Activity.performCreate(Activity.java:7224)
at android.app.Activity.performCreate(Activity.java:7213)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2926)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Caused by: java.lang.ClassNotFoundException: Didn't find class "android.arch.lifecycle.DefaultLifecycleObserver" on path: DexPathList[[zip file "/data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/lib/arm64, /data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]
最後的日誌給出的提示是lifecycle缺失,所以新增lifecycle的依賴即可,如下。
implementation 'android.arch.lifecycle:common-java8:1.1.0'
然後再次執行就沒啥問題了。
5.2 使用FlutterEngine啟動
預設情況下,每個FlutterActivity被建立時都會建立一個FlutterEngine,每個FlutterEngine都有一個初始化操作。這意味著在啟動一個標準的FlutterActivity時會有一定的延遲。為了減少此延遲,我們可以在啟動FlutterActivity之前預先建立一個FlutterEngine,然後在跳轉FlutterActivity時使用FlutterEngine即可。最常見的做法是在Application中先初始化FlutterEngine,比如。
class MyApplication : Application() {
lateinit var flutterEngine : FlutterEngine
override fun onCreate() {
super.onCreate()
flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine)
}
}
然後,我們在跳轉FlutterActivity時使用這個緩衝的FlutterEngine即可,由於FlutterEngine初始化的時候已經新增了engine_id,所以啟動的時候需要使用這個engine_id進行啟動。
myButton.setOnClickListener {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
)
}
當然,在啟動的時候,我們也可以跳轉一個預設的路由,只需要在啟動的時候呼叫setInitialRoute方法即可。
class MyApplication : Application() {
lateinit var flutterEngine : FlutterEngine
override fun onCreate() {
super.onCreate()
// Instantiate a FlutterEngine.
flutterEngine = FlutterEngine(this)
// Configure an initial route.
flutterEngine.navigationChannel.setInitialRoute("your/route/here");
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
// Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine)
}
}
六、與Flutter通訊
經過上面的操作,我們已經能夠完成原生Android 跳轉Flutter,那如何實現Flutter跳轉原生Activity或者Flutter如何銷燬自己返回原生頁面呢?此時就用到了Flutter和原生Android的通迅機制,即Channel,分別是MethodChannel、EventChannel和BasicMessageChannel。
- MethodChannel:用於傳遞方法呼叫,是比較常用的PlatformChannel。
- EventChannel: 用於傳遞事件。
- BasicMessageChannel:用於傳遞資料。
對於這種簡單的跳轉操作,直接使用MethodChannel即可完成。首先,我們在flutter_module中新建一個PluginManager的類,然後新增如下程式碼。
import 'package:flutter/services.dart';
class PluginManager {
static const MethodChannel _channel = MethodChannel('plugin_demo');
static Future<String> pushFirstActivity(Map params) async {
String resultStr = await _channel.invokeMethod('jumpToMain', params);
return resultStr;
}
}
然後,當我們點選Flutter入口頁面的返回按鈕時,新增一個返回的方法,主要是呼叫PluginManager傳送訊息,如下。
Future<void> backToNative() async {
String result;
try {
result = await PluginManager.pushFirstActivity({'key': 'value'});
} on PlatformException {
result = '失敗';
}
print('backToNative: '+result);
}
接下來,重新使用flutter build aar
重新編譯aar包,並在原生Android的Flutter入口頁面的configureFlutterEngine方法中新增如下程式碼。
class FlutterContainerActivity : FlutterActivity() {
private val CHANNEL = "plugin_demo"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "jumpToMain") {
val params = call.argument<String>("key")
Toast.makeText(this,"返回原生頁面",Toast.LENGTH_SHORT).show()
finish()
result.success(params)
} else {
result.notImplemented()
}
}
}
}
重新執行原生專案時,點選Flutter左上角的返回按鈕就可以返回到原生頁面,其他的混合跳轉也可以使用這種方式進行解決。
關於混合開發中混合路由和FlutterEngine多例項的問題,可以參考FlutterBoost。