延遲載入元件
簡介
Flutter 支援建立在執行時下載額外 Dart 程式碼和靜態資源的應用程式。這可以減少安裝應用程式 apk 的大小,並在使用者需要時下載功能和靜態資源。
我們將每個獨立的可下載的 Dart 函式庫和靜態資源稱為「延遲元件」。請使用 Dart 的延遲匯入 載入這些元件。這些元件可以編譯到拆分的 AOT 和 JavaScript 共享庫中。
儘管模組可以延遲載入 module,但整個應用程式必須作為單個 Android App Bundle (*.aab
)
完全建立和上傳。不支援在沒有重新上傳整個新 Android App Bundle 的情況下傳送部分更新。
在 Release 或 Profile 模式 下編譯應用程式時, Flutter 會執行延遲載入。在 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() { 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
此指令會幫助你檢查專案是否正確設定為建立延遲元件應用。預設情況下,驗證程式檢測到任何問題都會導致建立失敗,你可以透過系統建議的更改來修復這些問題。
-
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 模組。