跳至主要內容

延遲組件

簡介

#

Flutter 具有建置應用程式的功能,這些應用程式可以在執行時下載額外的 Dart 程式碼和資源。這讓應用程式能夠減少安裝 apk 的大小,並在使用者需要時下載功能和資源。

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

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

當您在發佈或效能分析模式中編譯應用程式時,Flutter 會執行延遲載入。除錯模式會將所有延遲元件視為一般匯入。這些元件會在啟動時出現並立即載入。這讓除錯版本能夠進行熱重載。

如需深入了解此功能如何運作的技術細節,請參閱 延遲元件Flutter 維基上。

如何為延遲元件設定專案

#

以下說明將說明如何為您的 Android 應用程式設定延遲載入。

步驟 1:相依性與初始專案設定

#
  1. 將 Play Core 新增至 Android 應用程式的 build.gradle 相依性。在 android/app/build.gradle 中新增以下內容

    groovy
    ...
    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 來完成

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

    io.flutter.app.FlutterPlayStoreSplitApplication 會為您處理這兩項任務。如果您使用 FlutterPlayStoreSplitApplication,您可以跳至步驟 1.3。

    如果您的 Android 應用程式很大或很複雜,您可能想要個別支援 SplitCompat 並手動提供 PlayStoreDynamicFeatureManager

    為了支援 SplitCompat,有三種方法(如 Android 文件 中詳細說明),任何一種方法都有效

    • 讓您的應用程式類別擴充 SplitCompatApplication

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

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

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

    嵌入器依靠注入的 DeferredComponentManager 執行個體來處理延遲元件的安裝請求。將 PlayStoreDeferredComponentManager 提供給 Flutter 嵌入器,方法是將以下程式碼新增至您的應用程式初始化

    java
    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. 透過在應用程式的 pubspec.yamlflutter 條目下新增 deferred-components 條目,選擇加入延遲元件

    yaml
    ...
    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 可以是任何複雜程度,但為了本指南的目的,請建立一個簡單的方塊作為替代。若要建立一個簡單的藍色方塊 widget,請建立 box.dart 並包含下列內容

    box.dart
    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 widget。然後,SomeWidget 可以在應用程式中正常使用,而且在成功載入之前,永遠不會嘗試存取延遲的 Dart 程式碼。

    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
      <?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
      包含一個 meta-data 條目,用於編碼載入單元與該載入單元相關聯的元件名稱之間的對應關係。嵌入器使用此對應關係將 Dart 的內部載入單元 ID 轉換為要安裝的延遲元件名稱。例如:

      xml
      ...
      <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 檔案會包含:

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

    載入單元 ID(在此例中為「2」)由 Dart 內部使用,可以忽略。基本載入單元(ID 為「1」)未列出,包含未明確包含在另一個載入單元中的所有內容。

    您現在可以將以下內容新增至 pubspec.yaml

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

    若要將載入單元指派給延遲元件,請將載入單元中的任何 Dart 函式庫新增至功能模組的 libraries 區段。請記住以下準則:

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

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

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

    • 指派給同一個延遲元件的載入單元會一起下載、安裝和交付。

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

  4. 也可以透過在延遲元件設定中新增 assets 區段來包含資源。

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

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

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

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

    groovy
    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 檔案來驗證,以確認產生的載入單元是否如預期般描述。

    當調整延遲元件設定,或進行新增、修改或移除載入單元的 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() 時,Flutter 引擎會使用 Play 商店的傳遞功能下載包含 Dart AOT 函式庫和資源的所需 Android 模組。