極客們,請收下2021 微軟 x 英特爾黑客松大賽英雄帖!>>>
Flutter 作為 Google 開源的新一代跨平台、高性能 UI 架構,旨在幫助開發者高效地建構出跨平台的、UI 與互動體驗一緻的精美應用,推出後一直倍受開發者的青睐。
當需要開發一個全新的應用時,我們可以很友善地從零開始,完全使用 Flutter 進行開發。但如果是針對一個現有的應用,需要引入 Flutter 技術,顯然使用 Flutter 全部重寫一遍是不現實的。幸運的是,Flutter 很好地支援了以獨立頁面、甚至是 UI 片段的方式內建到現有的應用中,即所謂的混合開發模式。本文主要從一個 Android 開發的視角,談談 Android 平台下, Flutter 的混合開發與建構。
相信現在應該很少會有移動端開發者不知道 Flutter,這裡不再做過多介紹。對于這門技術,使用過的應該絕大多數都會說好;沒用過的推薦嘗試一下,跑個 Demo 體驗體驗,有可能它就是你需要學習和掌握的最後一門新技術了。回過頭來,Flutter 究竟有什麼獨特的魅力讓它能從一衆技術中脫穎而出呢?總結一下,主要有以下幾點:
跨平台:可以做到一套代碼完美适配 Android、iOS 平台,未來還會覆寫更多平台,大大節省了開發人力與維護成本,同時擁有出色的跨端 UI 表現一緻性。
高效開發:SDK 提供了豐富的 UI 元件,開箱即用;聲明式的 UI 建構方式,大大減少出錯率;Debug 模式提供熱重載能力,可實時預覽代碼變更,不需要重新編譯安裝。
高性能:采用自建渲染引擎,獨立于系統并可單獨優化;差別于 RN、WEEX,沒有中間層轉換的額外開銷;Release 模式下代碼編譯為 AOT 指令,運作高效。
受益于以上的核心優勢,Flutter 推出後圈了很多移動開發者的粉,各網際網路大廠也紛紛将其作為一項基礎技術進行研究。在 Flutter 初期,其應用場景主要是從 0 建構一個全新 App,對混合開發的支援很不友好。但作為一門跨平台的技術架構,到底還是需要依賴原生平台提供的諸多系統能力,此外還有衆多現存原生 App 躍躍欲試,是以在這個需求背景下,混合開發的支援與完善至今已發展得越來越好,下面我們就用一個簡單的示例開始 Android 端的 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 子產品所依賴的所有插件。
項目結構變化,如下圖所示:
在引入 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。
首先需要在 App 子產品的build.gradle腳本檔案中添加對Flutter工程的依賴,隻有這樣 Flutter 子產品才會參與到整個應用的建構中來,我們也才能夠在 App 子產品中調用到 Flutter 提供的 Java 層 API。如下所示:
我們可以選擇使用 Activity、Fragment 或者 View 來承載 Flutter 的 UI,這裡主要介紹前面兩種方式,并假設flutter_module中已經通過runApp方法渲染了一個widget。
運作 Flutter Activity。使用io.flutter.embedding.android.FlutterActivity類可以很友善的啟動一個 Flutter Activity,當然我們也可以繼承它并擴充自己的邏輯。示例代碼如下:
運作 Flutter Fragment。可以使用FlutterFragmentActivity或者FlutterFragment來添加 Flutter UI 片段:a. 使用FlutterFragmentActivity可以自動建立并添加一個FlutterFragment;b. 手動建立FlutterFragment後添加到目标 Activity 中。示例代碼如下:
平台層和 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 将結果回傳。
Dart 層使用相同的名稱建立 MethodChannel,并設定 Handler 處理回調結果,随後調用 incrementCounter 方法請求 counter。示例代碼如下:
這裡我們是通過手動建立 MethodChannel 進行通信的,這在進行簡單通信的場景是沒問題的,但在通信接口 API 比較複雜的情況就不是很适用了。
一是繁瑣,因為我們需要手寫大量的打包、拆包代碼;二是容易出錯。這個時候就輪到 Pigeon 大顯身手了。Pigeon 是一個官方推出的代碼生成工具,可以生成類型安全的雙向通信 API 接口,具體可以參考官方的 Example,這裡不再贅述。
Pigeon :https://flutter.dev/docs/development/platform-integration/platform-channels#pigeon
到這裡,我們已經了解了如何在現有 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 的相關研發工作。
關于網易雲信的技術實踐,也歡迎關注網易雲信官網。