延遲載入元件
簡介
Flutter 支援建構在執行時下載額外 Dart 程式碼和靜態資源的應用程式。這可以減少安裝應用程式 apk 的大小,並在使用者需要時下載功能和靜態資源。
我們將每個獨立的可下載的 Dart 庫和靜態資源稱為「延遲元件」。這是透過使用 Dart 的延遲匯入來實現的,可以將其編譯到拆分的 AOT 共享庫中。
儘管模組可以延遲載入,但整個應用程式必須作為單個 App Bundle 完全建構和上傳。不支援在沒有重新上傳整個新 Android 應用程式套件的情況下發送部分更新。
延遲載入僅在應用程式編譯為 Release 或 Profile 模式 時可用。在 Debug 模式下,所有延遲元件都被視為常規匯入,它們在啟動時立即載入。因此,Debug 模式下仍然可以熱重載。
關於此功能的技術細節,請檢視 Flutter wiki 上的 延遲載入元件。
如何讓專案支援延遲載入元件
下面的引導將介紹如何設定 Android 應用程式以支援延遲載入。
步驟 1:依賴項和初始專案設定
-
將 Play Core 新增到 Android 應用程式的 build.gradle 依賴項中。在
android/app/build.gradle
中新增以下內容:... dependencies { ... implementation "com.google.android.play:core:1.8.0" ... }
-
如果使用 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 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());
-
-
透過將
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
後面的延遲載入程式碼的匯入和保護用法,將現有程式碼轉換為延遲程式碼。
-
建立新的 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, ); } }
-
在應用中使用
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() { _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
此命令會幫助你檢查專案是否正確設定為建構延遲元件應用。預設情況下,驗證程式檢測到任何問題都會導致建構失敗,你可以透過系統建議的更改來修復這些問題。
-
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.gradle
和src/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
驗證程式在預建構驗證透過之前不會執行。 -
-
對於每個檢查,該工具會建立或者修改需要的檔案。這些檔案放在
<projectDir>/build/android_deferred_components_setup_files
目錄下。建議透過複製和覆蓋專案android
目錄中的相同檔案來應用更改。在覆蓋之前,當前的專案狀態應該被提交到原始碼管理中,並檢查建議的改動。該工具不會自動更改android
目錄。 -
一旦產生可用的載入單元並將其記錄到
<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 中定義。
-
-
靜態資源也可以透過在延遲元件中配置 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 程式碼最好打包在同一組件中。
-
將在
pubspec.yaml
中定義的所有延遲元件手動新增到android/settings.gradle
檔案中的 includes 部分。例如,如果 pubspec 中定義了三個名為boxComponent
、circleComponent
和assetComponent
的延遲元件,請確保android/settings.gradle
中包含以下內容:include ':app', ':boxComponent', ':circleComponent', ':assetComponent' ...
-
重複步驟 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 模組。