引言
Flutter 作為 Google 開源的新一代跨平台、高性能 UI 框架,旨在幫助開發者高效地構建出跨平台的、UI 與交互體驗一致的精美應用,推出后一直倍受開發者的青睞。
當需要開發一個全新的應用時,我們可以很方便地從零開始,完全使用 Flutter 進行開發。但如果是針對一個現有的應用,需要引入 Flutter 技術,顯然使用 Flutter 全部重寫一遍是不現實的。幸運的是,Flutter 很好地支持了以獨立頁面、甚至是 UI 片段的方式集成到現有的應用中,即所謂的混合開發模式。本文主要從一個 Android 開發的視角,談談 Android 平台下, Flutter 的混合開發與構建。
Hello Flutter
相信現在應該很少會有移動端開發者不知道 Flutter,這里不再做過多介紹。對於這門技術,使用過的應該絕大多數都會說好;沒用過的推薦嘗試一下,跑個 Demo 體驗體驗,有可能它就是你需要學習和掌握的最后一門新技術了。回過頭來,Flutter 究竟有什么獨特的魅力讓它能從一眾技術中脫穎而出呢?總結一下,主要有以下幾點:
- 跨平台:可以做到一套代碼完美適配 Android、iOS 平台,未來還會覆蓋更多平台,大大節省了開發人力與維護成本,同時擁有出色的跨端 UI 表現一致性。
- 高效開發:SDK 提供了豐富的 UI 組件,開箱即用;聲明式的 UI 構建方式,大大減少出錯率;Debug 模式提供熱重載能力,可實時預覽代碼變更,不需要重新編譯安裝。
- 高性能:采用自建渲染引擎,獨立於系統並可單獨優化;區別於 RN、WEEX,沒有中間層轉換的額外開銷;Release 模式下代碼編譯為 AOT 指令,運行高效。
受益於以上的核心優勢,Flutter 推出后圈了很多移動開發者的粉,各互聯網大廠也紛紛將其作為一項基礎技術進行研究。在 Flutter 初期,其應用場景主要是從 0 構建一個全新 App,對混合開發的支持很不友好。但作為一門跨平台的技術框架,到底還是需要依賴原生平台提供的諸多系統能力,此外還有眾多現存原生 App 躍躍欲試,因此在這個需求背景下,混合開發的支持與完善至今已發展得越來越好,下面我們就用一個簡單的示例開始 Android 端的 Flutter 混合開發與構建之旅。
引入 Flutter 模塊
要在一個已有的 Android Project 中使用 Flutter,需要引入一個 Flutter Module。在 Android Studio(需要確保 Flutter 插件已經成功安裝並啟用)中打開現有 Android 工程,通過使用 File > New > New Module… 菜單,我們可以新創建一個 Flutter 模塊或是導入一個外部的 Flutter 模塊。

這里以最簡單的 Android App 項目為例,導入 Flutter 模塊。在 Flutter 模塊導入成功之后,原工程文件、結構都會發生一些變化,主要有:
- settings.gradle 文件新增了以下內容。其實就是執行對應 Flutter 模塊下 .android/include_flutter.groovy 腳本文件,該步驟會引入一個名為 Flutter 的 Android Library Module,同時還會引入 Flutter 模塊所依賴的所有插件。
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')
- 項目結構變化,如下圖所示:

在引入 Flutter 模塊之前,項目中僅有 app 一個 Module;而在引入之后,可以看到除了原有的 app Module 外,Flutter Gradle 插件自動引入了額外幾個子 Module:
- flutter_module:指代要引入的目標 Flutter Module,不會 apply Android 相關的任何插件,主要是包含 Flutter 相關源碼、資源、依賴等。
- flutter:為 Flutter Gradle 插件引入的 Android Library Module;主要負責編譯 flutter_module 及其依賴的第三方 Package、Plugin 的 Dart 代碼,以及打包 Flutter 資源等。
- device_info:為 Flutter Gradle 插件自動引入的 Flutter Android Plugin Library Module,這是因為一開始我在 flutter_module 的 pubspec.yaml 文件中添加了對 device_info 這個插件的依賴。Flutter Gradle 工具會將 flutter_module 依賴到的所有插件其 Android 平台側的代碼、資源作為一個 Library Module 引入到項目中一起參與構建。如果要查看 flutter_module 引入了哪些 Plugin,可以查看其對應目錄下的 .flutter-plugins 與 .flutter-plugins-dependencies 文件,這兩個文件是執行 flutter pub get 時生成的,記錄了插件的本地文件目錄、依賴信息等。
注意:一個工程不能包含多個 Flutter Module,最多只能引入一個,這是由 Flutter 的 Gradle 插件決定的。
使用 Flutter
完成 Flutter 模塊的引入后,我們再來看看如何使用 Flutter。
添加依賴
首先需要在 App 模塊的build.gradle腳本文件中添加對Flutter工程的依賴,只有這樣 Flutter 模塊才會參與到整個應用的構建中來,我們也才能夠在 App 模塊中調用到 Flutter 提供的 Java 層 API。如下所示:
dependencies {
implementation project(':flutter')
}
運行 Flutter 頁面
我們可以選擇使用 Activity、Fragment 或者 View 來承載 Flutter 的 UI,這里主要介紹前面兩種方式,並假設flutter_module中已經通過runApp方法渲染了一個widget。
- 運行 Flutter Activity。使用io.flutter.embedding.android.FlutterActivity類可以很方便的啟動一個 Flutter Activity,當然我們也可以繼承它並擴展自己的邏輯。示例代碼如下:
FlutterActivity
.withNewEngine()
.build(context)
.also { startActivity(it) }
- 運行 Flutter Fragment。可以使用FlutterFragmentActivity或者FlutterFragment來添加 Flutter UI 片段:a. 使用FlutterFragmentActivity可以自動創建並添加一個FlutterFragment;b. 手動創建FlutterFragment后添加到目標 Activity 中。示例代碼如下:
val flutterFragment = FlutterFragment.withNewEngine()
.dartEntrypoint(getDartEntrypointFunctionName())
.initialRoute(getInitialRoute())
.appBundlePath(getAppBundlePath())
.flutterShellArgs(FlutterShellArgs.fromIntent(intent))
.handleDeeplinking(shouldHandleDeeplinking())
.renderMode(renderMode)
.transparencyMode(transparencyMode)
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
.build<FlutterFragment>()
fragmentManager
.beginTransaction()
.add(
FRAGMENT_CONTAINER_ID,
flutterFragment,
TAG_FLUTTER_FRAGMENT
)
.commit()
- 平台層和 Flutter 層通信。不論是開發 Plugin 還是業務邏輯,平台層與 Flutter 層通信是必不可少的,為此就需要使用到MethodChannel。平台層通過MethodChannel請求調用 Flutter 層 API 時,數據在經過打包編碼后,通過 JNI、DartVM 傳到 Flutter 層解碼后使用;待結果計算完成后,又會重新打包編碼,經過 DartVM、JNI 傳回到 Native 層;同理,在 Flutter 層請求調用平台層的 API 時,數據處理是一致的,只是流轉方向相反。通過這種方式,平台層與 Flutter 層就建立了一個雙向的、異步的通信通道。在下面的示例代碼中,Native 層使用dev.flutter.example/counter創建一個MethodChannel,並設置 Handler 接收 Dart 的遠程方法調用 incrementCounter,並調用 reportCounter 將結果回傳。
channel = MethodChannel(flutterEngine.dartExecutor, "dev.flutter.example/counter")
channel.setMethodCallHandler { call, _ ->
when (call.method) {
"incrementCounter" -> {
count++
channel.invokeMethod("reportCounter", count)
}
}
}
Dart 層使用相同的名稱創建 MethodChannel,並設置 Handler 處理回調結果,隨后調用 incrementCounter 方法請求 counter。示例代碼如下:
final _channel = MethodChannel('dev.flutter.example/counter');
_channel.setMethodCallHandler(_handleMessage);
_channel.invokeMethod('incrementCounter');
Future<dynamic> _handleMessage(MethodCall call) async {
if (call.method == 'reportCounter') {
_count = call.arguments as int;
notifyListeners();
}
}
這里我們是通過手動創建 MethodChannel 進行通信的,這在進行簡單通信的場景是沒問題的,但在通信接口 API 比較復雜的情況就不是很適用了。
一是繁瑣,因為我們需要手寫大量的打包、拆包代碼;二是容易出錯。這個時候就輪到 Pigeon 大顯身手了。Pigeon 是一個官方推出的代碼生成工具,可以生成類型安全的雙向通信 API 接口,具體可以參考官方的 Example,這里不再贅述。
Pigeon :https://flutter.dev/docs/development/platform-integration/platform-channels#pigeon
Flutter APK 解析
到這里,我們已經了解了如何在現有 Android 項目中引入並使用 Flutter,接下來我們再來探究一下 Flutter APK 的結構,看看 Flutter Tools 在這個 APK 包內到底打包了哪些東西。下面兩圖分別為 Debub 模式和 Release 模式下構建出來的 Flutter APK 包結構,忽略了非 Flutter 相關的項。

可以看到兩個模式下的 APK 結構大致相同,說明如下:
- lib/{arch}/libflutter.so:為對應架構的 Flutter Engine 共享庫,負責 Flutter 渲染、JNI 通信、DartVM。如果不需要對應架構的版本,通過 abiFilters 可以 Exclude 掉。
- lib/{arch}/libapp.so:只存在於 Release 模式下,共享庫中包含 Dart AOT 生成的二進制指令和數據。在運行時,Flutter Engine 通過 Dynamic Load 的方式,從共享庫中讀取對應的可執行機器指令以及數據。
- assets/flutter_assets:Flutter 引用到的相關資源
- fonts:包含字體庫。
- FontManifest.json:引用到的字體庫清單文件,json 格式,所有使用到的字體、以及字體文件在 flutter_assets 下的路徑。
- AssetManifest.json:其他資源清單文件,json 格式,為所有資源名稱到資源路徑的映射,Flutter 在加載某一項資源時,會通過這個配置清單找到對應路徑的資源進行讀取后加載。
- kernel_blob.bin、isolate_snapshot_data、vm_snapshot_data:只存在於 Debug 模式下,分別為 DartVM 字節碼與數據,其作用類似於 libapp.so,只是存在形式、打包方式不同。在 Debug 模式下,Flutter Tools 將指令和數據分別打包,主要是為了熱重載(HotReload)服務的,而在 Release 模式下是統一打包成共享庫。
踩過的坑
這里,也總結了幾個我們在應用的時候遇到的問題,供大家參考避坑。
- 路由管理復雜:這里面包括 Flutter 層內部的頁面路由管理以及 Flutter 與原生的混合棧管理。前者在 Navigator 2.0 API 中已經得到了很好的完善與支持,但后者仍面臨着諸多限制與不足,需要改進。目前項目中還未涉及到后者這種很復雜的業務場景,因此對這一塊的研究比較少,感興趣的同學可以了解一下諸如 flutter_boost 此類的開源解決方案。
- 生命周期不對應:Android 的組件一般都會有自己的生命周期,Flutter 的 Widget State 也有一套自己的生命周期,但這兩者其實並不是一一對應的。比如原生的 Activity 頁面雖然已經被 Finish 並 Destroy 掉了,但 Flutter 層的頁面並不一定會隨之而被 Dispose,尤其是在使用 Cache Flutter Engine 的時候。Flutter 頁面是可以脫離原生頁面而存在的,它們可以被動態地 Attach 和 Detach,Attach 時會觸發重新渲染,Detach 時 UI 相關的所有操作都會 Pending 直到重新被 Attach。所以在混合開發中,業務邏輯不應該過度依賴 Widget State 的一些生命周期方法,因為它們可能會被延后執行從而導致一些奇怪的 Bug。
總結
Flutter 混合開發使得開發者可以漸進式地進行 Flutter 開發與遷移,是 Flutter 寄生於原生平台至關重要的一環。
本文主要從一個 Android 開發的視角,介紹了 Flutter 混合開發的入門知識。隨着 Flutter 開源項目的不斷迭代與演進,混合開發的體驗正在變得越來越好、性能也越來越高。但美中不足的是仍然有一些應用場景與問題並未得到很好地完善與解決,比如 Flutter 多實例問題(我們也將在本月的另一篇文章中跟大家分享介紹我們在實踐 Flutter 多實例遇到的問題與解決方案,敬請關注)。
瑕不掩瑜,Flutter 這門技術整體而言還是非常不錯的,它如今仍處於快速發展的階段,相信在 Flutter 團隊與開源社區的共同努力下,未來的生態值得期待。
作者簡介
李成達,網易雲信資深移動端開發工程師,熱衷於研究跨平台開發技術以及工程提效,目前主要負責視頻會議組件化 SDK 的相關研發工作。
