Android集成Flutter

Android 集成Flutter

Flutter 作為 Google 開源的新一代跨平臺、高性能 UI 框架,旨在幫助開發者高效地構建出跨平臺的、UI 與交互體驗一致的精美應用,推出後一直倍受開發者的青睞。

當需要開發一個全新的應用時,我們可以很方便地從零開始,完全使用 Flutter 進行開發。但如果是針對一個現有的應用,需要引入 Flutter 技術,顯然使用 Flutter 全部重寫一遍是不現實的。幸運的是,Flutter 很好地支持瞭以獨立頁面、甚至是 UI 片段的方式集成到現有的應用中,即所謂的混合開發模式。本文主要從一個 Android 開發的視角,談談 Android 平臺下, Flutter 的混合開發與構建。

1, Hello Flutter

對於這門技術,使用過的應該絕大多數都會說好;沒用過的推薦嘗試一下,跑個 Demo 體驗體驗,有可能它就是你需要學習和掌握的最後一門新技術瞭。回過頭來,Flutter 究竟有什麼獨特的魅力讓它能從一眾技術中脫穎而出呢?總結一下,主要有以下幾點:

  • 跨平臺:可以做到一套代碼完美適配 Android、iOS 平臺,未來還會覆蓋更多平臺,大大節省瞭開發人力與維護成本,同時擁有出色的跨端 UI 表現一致性。
  • 高效開發:SDK 提供瞭豐富的 UI 組件,開箱即用;聲明式的 UI 構建方式,大大減少出錯率;Debug 模式提供熱重載能力,可實時預覽代碼變更,不需要重新編譯安裝。
  • 高性能:采用自建渲染引擎,獨立於系統並可單獨優化;區別於 RN、WEEX,沒有中間層轉換的額外開銷;Release 模式下代碼編譯為 AOT 指令,運行高效。

受益於以上的核心優勢,Flutter 推出後圈瞭很多移動開發者的粉,各互聯網大廠也紛紛將其作為一項基礎技術進行研究。在 Flutter 初期,其應用場景主要是從 0 構建一個全新 App,對混合開發的支持很不友好。但作為一門跨平臺的技術框架,到底還是需要依賴原生平臺提供的諸多系統能力,此外還有眾多現存原生 App 躍躍欲試,因此在這個需求背景下,混合開發的支持與完善至今已發展得越來越好,下面我們就用一個簡單的示例開始 Android 端的 Flutter 混合開發與構建之旅。

2, 引入 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 時生成的,記錄瞭插件的本地文件目錄、依賴信息等。

3,使用Flutter

3.1 添加依賴

首先,需要在 App 模塊的build.gradle腳本文件中添加對Flutter工程的依賴,隻有這樣 Flutter 模塊才會參與到整個應用的構建中來,我們也才能夠在 App 模塊中調用到 Flutter 提供的 Java 層 API。

dependencies { 
  implementation project(':flutter') 
}

3.2 運行Flutter頁面

3.2.1 添加Flutter頁面

我們可以選擇使用Activity、Fragment 或者 View 來承載 Flutter 的 UI,這裡主要介紹前面兩種方式,並假設flutter_module中已經通過runApp方法渲染瞭一個widget。

Flutter Activity

首先,我們介紹下使用 Flutter Activity的方式。使用io.flutter.embedding.android.FlutterActivity類可以很方便的啟動一個 Flutter Activity,當然我們也可以繼承它並擴展自己的邏輯。

FlutterActivity 
  .withNewEngine() 
  .build(context) 
  .also { 
    startActivity(it) 
  }
Flutter Fragment

另外一種就是 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()
 3.2.2 平臺層和 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 接口,具體可以參考官方的 例子,Pigeon官方鏈接。

4,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 模式下是統一打包成共享庫。

5,踩過的坑

Flutter 混合開發使得開發者可以漸進式地進行 Flutter 開發與遷移,是 Flutter 寄生於原生平臺至關重要的一環,不過在接入Flutter的過程中,也出現瞭一些問題:

  • 路由管理復雜:這裡面包括 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。

到此這篇關於Android 集成Flutter的文章就介紹到這瞭,更多相關Android內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: