跳到主內容

Android 和 Web 的延遲載入元件

如何建立延遲載入元件以提高下載效能。

介紹

#

使用 Flutter,Android 和 Web 應用可以在執行時下載延遲載入元件(額外的程式碼和資源)。如果您的應用體積較大,並且只想在使用者確實需要時才安裝某些元件,這將非常有用。

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

  • Android 的 動態功能模組 (dynamic feature modules) 以 Android 模組的形式交付延遲載入元件。

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

    當您在 release 或 profile 模式下編譯 Android 應用時,Flutter 會執行延遲載入,而 debug 模式會將所有延遲載入元件視為常規匯入。

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

有關此功能技術細節的深入探討,請參閱 Flutter Wiki 上的 Deferred Components

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

#

以下說明解釋瞭如何為 Android 應用設定延遲載入。

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

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

    android/app/build.gradle.kts
    kotlin
    ...
    dependencies {
      ...
      implementation("com.google.android.play:core:1.8.0")
      ...
    }
    
    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 中將 application 的 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 文件),以下任一方法均有效:

    • 使您的 Application 類繼承 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 作為 Application 子類,並將 FlutterApplication 中的 Flutter 相容性程式碼新增到您的 Application 類中

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

    嵌入器依賴於注入的 DeferredComponentManager 例項來處理延遲載入元件的安裝請求。透過在應用初始化時新增以下程式碼,為 Flutter 嵌入器提供一個 PlayStoreDeferredComponentManager

    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 庫。實現方案不需要即刻完成功能。本頁其餘部分的示例添加了一個新的簡單延遲載入小部件作為佔位符。您也可以透過修改匯入並將延遲程式碼的使用限制在 loadLibrary()Future 中,將現有程式碼轉換為延遲載入。

  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 等待 loadLibraryFuture(在 initState 中建立)完成,並顯示一個 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 構建應用,從而生成拆分的 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 部分,以便將載入單元分配給所需的延遲元件。繼續上面的 box 示例,生成的 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 庫,元件中的任何資源也會被載入。但是,按元件名稱和 services 實用程式安裝將不會載入元件中的任何 Dart 庫。

    您可以隨意在任何元件中包含資源,只要它們在首次引用時被安裝和載入即可,儘管通常情況下,資源和使用這些資源的 Dart 程式碼最好打包在同一個元件中。

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

    android/settings.gradle.kts
    kotlin
    include(":app", ":boxComponent", ":circleComponent", ":assetComponent")
    ...
    
    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() 時,Flutter 引擎會使用 Play 商店的分發功能下載包含 Dart AOT 庫和資源的 Android 模組。