延遲載入元件

簡介

Flutter 支援建構在執行時下載額外 Dart 程式碼和靜態資源的應用程式。這可以減少安裝應用程式 apk 的大小,並在使用者需要時下載功能和靜態資源。

我們將每個獨立的可下載的 Dart 庫和靜態資源稱為「延遲元件」。這是透過使用 Dart 的延遲匯入來實現的,可以將其編譯到拆分的 AOT 共享庫中。

儘管模組可以延遲載入,但整個應用程式必須作為單個 App Bundle 完全建構和上傳。不支援在沒有重新上傳整個新 Android 應用程式套件的情況下發送部分更新。

延遲載入僅在應用程式編譯為 Release 或 Profile 模式 時可用。在 Debug 模式下,所有延遲元件都被視為常規匯入,它們在啟動時立即載入。因此,Debug 模式下仍然可以熱重載。

關於此功能的技術細節,請檢視 Flutter wiki 上的 延遲載入元件

如何讓專案支援延遲載入元件

下面的引導將介紹如何設定 Android 應用程式以支援延遲載入。

步驟 1:依賴項和初始專案設定

  1. 將 Play Core 新增到 Android 應用程式的 build.gradle 依賴項中。在 android/app/build.gradle 中新增以下內容:

    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    
  2. 如果使用 Google Play 商店作為動態功能的分發模型,應用程式必須支援 SplitCompat 並手動提供 PlayStoreDeferredComponentManager 的例項。這兩個任務都可以透過設定 android/app/src/main/AndroidManifest.xml 中的 android:nameio.flutter.embedding.android.FlutterPlayStoreSplitApplication 應用屬性來完成:

    <manifest ...
      <application
         android:name="io.flutter.embedding.android.FlutterPlayStoreSplitApplication"
            ...
      </application>
    </manifest>
    

    io.flutter.app.FlutterPlayStoreSplitApplication 已經為你完成了這兩項任務。如果你使用了 FlutterPlayStoreSplitApplication,可以跳轉至步驟 1.3。

    如果你的 Android 應用程式很大或很複雜,你可能需要單獨支援 SplitCompat 並提供 PlayStoreDynamicFeatureManager

    要支援 SplitCompat,有三種方法(詳見 Android docs),其中任何一種都是有效的:

    • 讓你的 application 類繼承 SplitCompatApplication

      public class MyApplication extends SplitCompatApplication {
          ...
      }
      
    • attachBaseContext() 中呼叫 SplitCompat.install(this);

      @Override
      protected void attachBaseContext(Context base) {
          super.attachBaseContext(base);
          // Emulates installation of future on demand modules using SplitCompat.
          SplitCompat.install(this);
      }
      
    • SplitCompatApplication 宣告為 application 的子類別,並將 FlutterApplication 中的 flutter 相容性程式碼新增到你的 application 類中:

      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      

    嵌入層依賴注入的 DeferredComponentManager 例項來處理延遲元件的安裝請求。透過在應用程式的初始流程中新增以下程式碼,將 PlayStoreDeferredComponentManager 新增到 Flutter 嵌入層中:

    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    import io.flutter.FlutterInjector;
    ... 
    PlayStoreDeferredComponentManager deferredComponentManager = new
      PlayStoreDeferredComponentManager(this, null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    
  3. 透過將 deferred-components 依賴新增到應用程式的 pubspec.yaml 中的 flutter 下,並選擇延遲元件:

      ...
      flutter:
        ...
        deferred-components:
        ...
    

    flutter 工具會在 pubspec.yaml 中查詢 deferred-components,來確定是否應將應用程式建構為延遲載入。除非你已經知道所需的元件和每個元件中的 Dart 延遲庫,否則可以暫時將其留空。當 gen_snapshot 產生載入單元后,你可以在後面的 步驟 3.3 中完善這部分內容。

步驟 2:實現延遲載入的 Dart 庫

接下來,在 Dart 程式碼中實現延遲載入的 Dart 庫。實現並非立刻需要的功能。文章剩餘部分中的範例添加了一個簡單的延遲 widget 作為佔位。你還可以透過修改 loadLibrary()Futures 後面的延遲載入程式碼的匯入和保護用法,將現有程式碼轉換為延遲程式碼。

  1. 建立新的 Dart 庫。例如,建立一個可以在執行時下載的 DeferredBox widget。這個 widget 可以是任意複雜的,本指南使用以下內容建立了一個簡單的框。

    // box.dart
    import 'package:flutter/material.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      const DeferredBox({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 30,
          width: 30,
          color: Colors.blue,
        );
      }
    }
  2. 在應用中使用 deferred 關鍵字匯入新的 Dart 庫,並呼叫 loadLibrary()(請參見 延遲載入庫)。下面的範例使用 FutureBuilder 等待 loadLibraryFuture 物件(在 initState 中建立)完成,並將 CircularProgressIndicator 做為佔位。當 Future 完成時,會返回 DeferredBoxSomeWidget 便可在應用程式中正常使用,在成功載入之前不會嘗試存取延遲的 Dart 程式碼。

    import 'package:flutter/material.dart';
    import 'box.dart' deferred as box;
    
    class SomeWidget extends StatefulWidget {
      const SomeWidget({super.key});
    
      @override
      State<SomeWidget> createState() => _SomeWidgetState();
    }
    
    class _SomeWidgetState extends State<SomeWidget> {
      late Future<void> _libraryFuture;
    
      @override
      void initState() {
        _libraryFuture = box.loadLibrary();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<void>(
          future: _libraryFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              }
              return box.DeferredBox();
            }
            return const CircularProgressIndicator();
          },
        );
      }
    }

    loadLibrary() 函式返回一個 Future<void> 物件,該物件會在延遲庫中的程式碼可用時成功返回,否則返回一個錯誤。延遲庫中所有的符號在使用之前都應確保 loadLibrary() 已經完成。所有匯入的函式庫都必須透過 deferred 標記,以便對其進行適當的編譯以及在延遲元件中使用。如果元件已經被載入,再次呼叫 loadLibrary 將快速返回(但不是同步完成)。也可以提前呼叫 loadLibrary() 函式進行預載入,以幫助遮蔽載入時間。

    你可以在 Flutter Gallery’s lib/deferred_widget.dart 中找到其他延遲載入元件的範例。

步驟 3:建構應用程式

使用以下 flutter 命令建構延遲元件應用:

$ flutter build appbundle

此命令會幫助你檢查專案是否正確設定為建構延遲元件應用。預設情況下,驗證程式檢測到任何問題都會導致建構失敗,你可以透過系統建議的更改來修復這些問題。

  1. flutter build appbundle 命令會嘗試建構應用,透過 gen_snapshot 將應用中拆分的 AOT 共享庫分割為單獨的 .so 檔案。第一次執行時,驗證程式可能會在檢測到問題時失敗,該工具會為如何設定專案和解決這些問題提供建議。

    驗證程式分為兩個部分:預建構和產生快照後的驗證。這是因為在 gen_snapshot 完成並產生最後一組載入單元之前,無法執行任何參考載入單元的驗證。

    驗證程式會檢測 gen_snapshot 產生的所有新增、修改或者刪除的載入單元。當前產生的載入單元記錄在 <projectDirectory>/deferred_components_loading_units.yaml 檔案中。這個檔案應該加入到版本管理中,以確保其他開發人員對載入單元所做的更改可被追蹤。

    驗證程式還會檢查 android 目錄中的以下內容:

    • 每個延遲元件名稱的鍵值對對映 ${componentName}Name${componentName}。每個功能模組的 AndroidManifest.xml 使用此字串資源來定義 dist:title property。例如:

      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      
    • 每個延遲元件都有一個 Android 動態功能模組,它包含一個 build.gradlesrc/main/AndroidManifest.xml 檔案。驗證程式只檢查檔案是否存在,不驗證檔案內容。如果檔案不存在,它將產生一個預設的推薦檔案。

    • 包含一個 meta-data 鍵值對,對載入單元與其關聯的元件名稱之間的對映進行編碼。嵌入程式使用此對映將 Dart 的內部載入單元 id 轉換為要安裝的延遲元件的名稱。例如:

          ...
          <application
              android:label="MyApp"
              android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
              android:icon="@mipmap/ic_launcher">
              ...
              <meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="2:boxComponent"/>
          </application>
          ...
      

    gen_snapshot 驗證程式在預建構驗證透過之前不會執行。

  2. 對於每個檢查,該工具會建立或者修改需要的檔案。這些檔案放在 <projectDir>/build/android_deferred_components_setup_files 目錄下。建議透過複製和覆蓋專案 android 目錄中的相同檔案來應用更改。在覆蓋之前,當前的專案狀態應該被提交到原始碼管理中,並檢查建議的改動。該工具不會自動更改 android 目錄。

  3. 一旦產生可用的載入單元並將其記錄到 <projectDirectory>deferred_components_loading_units.yaml 中,便可完善 pubspec 的 deferred-components 配置,將載入單元分配給延遲的元件。在上面的案例中,產生的 deferred_components_loading_units.yaml 檔案將包含:

    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    

    載入單元 id(在本例中為「2」)由 Dart 內部使用,可以忽略。基本載入單元(id 為「1」)包含了其他載入單元中未顯式列出的所有內容,在這裡沒有列出。

    現在可以將以下內容新增到 pubspec.yaml 中:

    ...
    flutter:
      ...
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      ...
    

    將載入單元分配到延遲元件,把載入單元中的任何 Dart 庫新增到功能模組的 libraries 部分。請記住以下準則:

    • 一個載入單元只能包含在一個延遲元件中

    • 參考載入單元中的一個 Dart 庫意味著整個載入單元都被包含在延遲元件中。

    • 所有未被分配給延遲元件的載入單元都包含在基本元件中,基本元件始終隱含存在。

    • 分配給同一延遲元件的載入單元將一起下載、安裝和執行。

    • 基本元件是隱含的,不需要在 pubspec 中定義。

  4. 靜態資源也可以透過在延遲元件中配置 assets 進行新增:

      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
          assets:
            - assets/image.jpg
            - assets/picture.png
              # wildcard directory
            - assets/gallery/
    

    一個靜態資源可以包含在多個延遲元件中,但是安裝這兩個元件會導致資源的重複。也可以透過省略 libraries 來定義純靜態資源的延遲元件。這些靜態資源的元件必須與服務中的 DeferredComponent 實用程式類一起安裝,而不是 loadLibrary()。由於 Dart 函式庫是與靜態資源打包在一起的,因此如果用 loadLibrary() 載入 Dart 庫,則也會載入元件中的所有資源。但是,按元件名稱和服務實用程式來安裝不會載入元件中的任何 Dart 庫。

    你可以自由選擇將資源包含在任何元件中,只要它們是在首次參考時安裝和載入的,但通常情況下,靜態資源和使用這些資源的 Dart 程式碼最好打包在同一組件中。

  5. 將在 pubspec.yaml 中定義的所有延遲元件手動新增到 android/settings.gradle 檔案中的 includes 部分。例如,如果 pubspec 中定義了三個名為 boxComponentcircleComponentassetComponent 的延遲元件,請確保 android/settings.gradle 中包含以下內容:

    include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
    ...
    
  6. 重複步驟 3.1 到 3.6(此步驟),直到處理了所有驗證程式的建議,並且該工具在沒有更多建議的情況下執行。

    成功時,此命令將在 build/app/outputs/bundle/release 目錄下輸出 app-release.aab 檔案。

    建構成功並非總是意味著應用是按預期建構的。你需要確保所有的載入單元和 Dart 庫都以你想要的方式包含在內。例如,一個常見的錯誤是不小心匯入了一個沒有 deferred 關鍵字的 Dart 庫,導致一個延遲載入庫被編譯為基本載入單元的一部分。在這種情況下,Dart 庫將正確載入,因為它始終存在於基本元件中,並且庫不會被拆分。可以透過檢查 deferred_components_loading_units.yaml 檔案,驗證預期的載入單元是否產生描述。

    當調整延遲元件配置,或者進行新增、修改、刪除載入單元的更改時,你應該預料到驗證程式會失敗。按照步驟 3.1 到 3.6(此步驟)中的所有建議繼續建構。

在本地執行應用

一旦你的應用程式成功建構了一個 .aab 檔案,就可以使用 Android 的 bundletool 來執行帶有 --local testing 標誌的本地測試。

要在測試裝置上執行 .aab 檔案,請從 github.com/google/bundletool/releases 下載 bundletool jar 可執行檔案,然後執行:

$ java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing

$ java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks

<your_app_project_dir> 是應用程式對應專案的目錄位置, <your_temp_dir> 用於儲存 bundletool 輸出的所有臨時目錄。這會將你的 .aab 檔案解壓為 .apks 檔案並將其安裝到裝置上。所有可用的 Android 動態特性都已在本地裝置上載入,並模擬了延遲元件的安裝。

再次執行 build-apks 之前,請刪除已存在的 .apks 檔案:

$ rm <your_temp_dir>/app.apks

對 Dart 程式碼庫的更改需要增加 Android 建構 ID,或者解除安裝並重新安裝應用程式。因為只有檢測到新的版本號,Android 才會去更新功能模組。

釋出到 Google Play 商店

產生的 .aab 檔案可以像平常一樣直接上傳到 Google Play 商店。呼叫 loadLibrary() 時,Flutter 引擎將會使用從商店下載的包含 Dart AOT 庫和資源的 Android 模組。