遞延元件

簡介

Flutter 具有在執行階段下載額外 Dart 程式碼和資源的能力。這讓應用程式可以縮小安裝 APK 大小,並在使用者需要時下載功能和資源。

我們將每個可單獨下載的 Dart 函式庫和資源組合稱為「遞延元件」。若要載入這些元件,請使用 Dart 的遞延匯入。它們可以編譯成分割的 AOT 和 JavaScript 共用函式庫。

雖然您可以遞延載入模組,但您必須建置整個應用程式,並將該應用程式上傳為單一的 Android 應用程式組合 (*.aab)。Flutter 不支援在不重新上傳整個應用程式的 Android 應用程式組合的情況下,傳送部分更新。

當您在 發行或設定檔模式 中編譯應用程式時,Flutter 會執行遞延載入。偵錯模式會將所有遞延元件視為一般匯入。元件會在啟動時存在並立即載入。這讓偵錯建置可以熱重載。

若要深入了解此功能運作方式的技術細節,請參閱 遞延元件,位於 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:name 屬性設定為 io.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 文件 中所述),任何一個方法都是有效的

    • 讓您的應用程式類別延伸 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 作為應用程式子類別,並將 FlutterApplication 的 Flutter 相容性程式碼新增至您的應用程式類別

      <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. 透過在 flutter 項目下的應用程式 pubspec.yaml 中新增 deferred-components 項目,選擇加入遞延元件

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

    flutter 工具會在 pubspec.yaml 中尋找 deferred-components 項目,以判斷應用程式是否應建置為遞延。除非您已知道所需的元件以及對應的 Dart 遞延函式庫,否則現在可以先留空。您會在 步驟 3.3 中,在 gen_snapshot 產生載入單元後填入此區段。

步驟 2:實作遞延 Dart 函式庫

接著,在應用程式的 Dart 程式碼中實作延遲載入的 Dart 函式庫。實作不需要立即具備完整功能。本頁面中其餘範例會新增一個新的簡單延遲小工具作為佔位符。您也可以修改匯入和延遲程式碼使用情況,將現有程式碼轉換為延遲,並在 loadLibrary() Futures 後面加上保護措施。

  1. 建立新的 Dart 函式庫。例如,建立新的 DeferredBox 小工具,可以在執行期間下載。此小工具可以具備任何複雜度,但為了本指南的目的,請建立一個簡單的方塊作為替身。若要建立一個簡單的藍色方塊小工具,請建立 box.dart,其內容如下

    // 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 等待 loadLibrary Future(在 initState 中建立)完成,並顯示 CircularProgressIndicator 作為佔位符。當 Future 完成時,它會傳回 DeferredBox 小工具。然後,SomeWidget 可以像一般一樣在應用程式中使用,而且在成功載入之前,永遠不會嘗試存取延遲的 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() {
        super.initState();
        _libraryFuture = box.loadLibrary();
      }
    
      @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 的 lib/deferred_widget.dart 中找到延遲匯入載入的另一個範例。

步驟 3:建置應用程式

使用下列 flutter 指令建置延遲元件應用程式

$ flutter build appbundle

此指令會協助您驗證專案是否已正確設定,以建置延遲元件應用程式。預設情況下,如果驗證器偵測到任何問題,建置會失敗,並引導您進行建議的變更以修正問題。

  1. flutter build appbundle 指令會執行驗證器,並嘗試使用 gen_snapshot 建置應用程式,並指示產生分割 AOT 共享函式庫,作為個別的 .so 檔案。在第一次執行時,驗證器可能會失敗,因為它偵測到問題;此工具會建議如何設定專案並修正這些問題。

    驗證器分為兩個區段:建置前和建置後 gen_snapshot 驗證。這是因為任何參照載入單元的驗證,都必須等到 gen_snapshot 完成並產生最後一組載入單元後才能執行。

    驗證器會偵測由 gen_snapshot 產生任何新的、已變更或已移除的載入單元。目前產生的載入單元會追蹤在您的 <projectDirectory>/deferred_components_loading_units.yaml 檔案中。此檔案應檢查至原始碼控制中,以確保其他開發人員對載入單元的變更可以被捕獲。

    驗證器也會在 android 目錄中檢查下列項目

    • <projectDir>/android/app/src/main/res/values/strings.xml
      每個遞延元件的項目,將金鑰 ${componentName}Name 對應到 ${componentName}。此字串資源是由每個功能模組的 AndroidManifest.xml 使用,以定義 dist:title 屬性。例如

      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      
    • <projectDir>/android/<componentName>
      每個遞延元件都有一個 Android 動態功能模組,且包含 build.gradlesrc/main/AndroidManifest.xml 檔案。這只會檢查是否存在,而不會驗證這些檔案的內容。如果檔案不存在,它會產生一個建議的預設檔案。

    • <projectDir>/android/app/src/main/res/values/AndroidManifest.xml
      包含一個元資料項目,編碼載入單元與載入單元關聯的元件名稱之間的對應。此對應是由嵌入器使用,以將 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 lib 新增至功能模組的函式庫區段。請記住以下準則

    • 載入單元不應包含在多個元件中。

    • 包含來自載入單元的其中一個 Dart 函式庫表示整個載入單元已指派給延遲元件。

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

    • 載入指定給相同延遲元件的單位會一起下載、安裝和發布。

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

  4. 資產也可以透過在延遲元件組態中新增資產區段來包含

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

    資產可以在多個延遲元件中包含,但安裝兩個元件會導致複製資產。也可以透過省略函式庫區段來定義僅限資產的元件。這些僅限資產的元件必須使用服務中的 DeferredComponent 實用程式類別安裝,而不是 loadLibrary()。由於 Dart 函式庫會與資產一起封裝,如果 Dart 函式庫已使用 loadLibrary() 載入,元件中的任何資產也會載入。不過,透過元件名稱和服務實用程式安裝不會載入元件中的任何 Dart 函式庫。

    您可以自由地在任何元件中包含資產,只要在第一次參照時安裝和載入即可,不過通常,資產和使用這些資產的 Dart 程式碼最好封裝在同一個元件中。

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

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

    執行成功時,此指令會在 build/app/outputs/bundle/release 中輸出 app-release.aab 檔案。

    成功建置並不一定表示已按照預期建置應用程式。您必須確保所有載入單元和 Dart 函式庫都已按照您的預期納入。例如,常見的錯誤是意外匯入 Dart 函式庫,但未加上 deferred 關鍵字,導致遞延函式庫編譯為基本載入單元的一部分。在此情況下,Dart lib 會正確載入,因為它始終存在於基本載入單元中,而且不會拆分 lib。您可以檢查 deferred_components_loading_units.yaml 檔案,以驗證產生的載入單元是否如預期所述。

    調整遞延元件設定,或進行會新增、修改或移除載入單元的 Dart 變更時,您應預期驗證器會失敗。請按照步驟 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 檔案可以像一般一樣直接上傳到 Play 商店。當呼叫 loadLibrary() 時,包含 Dart AOT 函式庫和資源的所需 Android 模組會由 Flutter 引擎使用 Play 商店的傳遞功能下載。