介紹

#

使用 Flutter,Android 和 Web 應用能夠在應用執行時下載延遲載入的元件(額外的程式碼和資源)。這對於大型應用非常有用,因為您可以只在使用者需要時才安裝這些元件。

雖然 Flutter 支援 Android 和 Web 上的延遲載入,但實現方式有所不同。兩者都需要 Dart 的延遲匯入

  • Android 的 動態功能模組 將延遲載入的元件打包成 Android 模組進行分發。

    構建 Android 應用時,雖然可以延遲載入模組,但必須構建整個應用並將其作為單個 Android App Bundle (AAB) 上傳。Flutter 不支援在不重新上傳整個應用程式的 Android App Bundle 的情況下分發部分更新。

    當您以 釋出或剖析模式 編譯 Android 應用時,Flutter 會執行延遲載入,但在除錯模式下,所有延遲載入的元件都將被視為常規匯入。

  • Web 會將延遲載入的元件建立為單獨的 *.js 檔案。

有關此功能工作原理的更深入的技術細節,請參閱 Flutter wiki 上的 延遲載入元件

如何為 Android 專案設定延遲載入元件

#

以下說明將介紹如何為 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. 透過將 deferred-components 條目新增到應用的 pubspec.yaml 檔案中 flutter 條目下,選擇啟用延遲載入元件

    yaml
    ...
    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
    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 等待 initState 中建立的 loadLibrary Future 完成,並顯示 CircularProgressIndicator 作為佔位符。當 Future 完成時,它會返回 DeferredBox 小部件。然後,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 構建應用,指示其生成單獨的 SO 檔案的分片 AOT 共享庫。首次執行時,驗證器很可能會因檢測到問題而失敗;該工具會提供有關如何設定專案和修復這些問題的建議。

    驗證器分為兩個部分:構建前驗證和 post-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
      包含一個元資料條目,該條目編碼了載入單元與載入單元關聯的元件名稱之間的對映。嵌入器使用此對映來將 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 庫新增到功能模組的庫部分。請牢記以下指南:

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

    • 包含載入單元中的一個 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 庫,那麼元件中的任何資源也會被載入。但是,透過元件名稱進行安裝和 services 實用類將不會載入元件中的任何 Dart 庫。

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

  5. 手動將您在 pubspec.yaml 中定義的所有延遲載入元件新增到 android/settings.gradle 檔案中作為 include。例如,如果 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 檔案解壓縮為 APK 檔案並將其安裝在裝置上。所有可用的 Android 動態功能都將在本地載入到裝置上,並模擬延遲載入元件的安裝。

在再次執行 build-apks 之前,請刪除現有的應用 APK 檔案

rm <your_temp_dir>/app.apks

Dart 程式碼的更改需要增加 Android 構建 ID 或解除安裝並重新安裝應用,因為 Android 在檢測到新版本號之前不會更新功能模組。

釋出到 Google Play 商店

#

構建的 AAB 檔案可以直接上傳到 Play 商店,和往常一樣。當呼叫 loadLibrary() 時,包含 Dart AOT 庫和資源的所需 Android 模組將由 Flutter 引擎使用 Play 商店的交付功能下載。