延迟组件
简介
Flutter 具有构建可在运行时下载其他 Dart 代码和资源的应用的能力。这允许应用减小安装 apk 大小,并在用户需要时下载功能和资源。
我们将每个可单独下载的 Dart 库和资源包称为“延迟组件”。要加载这些组件,请使用 Dart 的延迟导入。它们可以编译成拆分 AOT 和 JavaScript 共享库。
虽然你可以延迟加载模块,但你必须构建整个应用并将该应用上传为单个 Android 应用包 (*.aab
)。Flutter 不支持分发部分更新,而无需为整个应用程序重新上传新的 Android 应用包。
当您在发布或概要模式下编译应用程序时,Flutter 会执行延迟加载。调试模式将所有延迟组件视为常规导入。这些组件在启动时存在并立即加载。这允许调试版本进行热重载。
如需深入了解此功能的工作原理的技术细节,请参阅Flutter Wiki上的延迟组件。
如何为延迟组件设置项目
以下说明解释了如何为延迟加载设置您的 Android 应用程序。
步骤 1:依赖关系和初始项目设置
-
将 Play 核心添加到 Android 应用程序的 build.gradle 依赖关系中。在
android/app/build.gradle
中添加以下内容... dependencies { ... implementation "com.google.android.play:core:1.8.0" ... }
-
如果使用 Google Play 商店作为动态功能的发行模型,则应用程序必须支持
SplitCompat
并提供PlayStoreDeferredComponentManager
的实例。这两项任务都可以通过将应用程序的android:name
属性设置为android/app/src/main/AndroidManifest.xml
中的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 文档中所述),任何一种方法都是有效的-
使您的应用程序类扩展
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
声明为应用程序子类,并将FlutterApplication
中的 Flutter 兼容性代码添加到应用程序类<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());
-
-
通过在
flutter
条目下的pubspec.yaml
中添加deferred-components
条目来选择延迟组件... flutter: ... deferred-components: ...
flutter
工具在pubspec.yaml
中查找deferred-components
条目,以确定是否将应用程序构建为延迟组件。现在可以将其留空,除非您已经知道所需的组件和进入每个组件的 Dart 延迟库。您将在 步骤 3.3 中gen_snapshot
生成加载单元后填写此部分。
步骤 2:实现延迟 Dart 库
接下来,在应用程序的 Dart 代码中实现延迟加载的 Dart 库。该实现目前无需功能齐全。本页其余部分中的示例添加了一个新的简单延迟小组件作为占位符。您还可以通过修改导入并保护 loadLibrary()
Futures
之后延迟代码的使用,将现有代码转换为延迟代码。
-
创建新的 Dart 库。例如,创建一个新的
DeferredBox
小组件,该小组件可以在运行时下载。此小组件可以具有任何复杂性,但出于本指南的目的,创建一个简单的框作为替身。要创建一个简单的蓝色框小组件,请使用以下内容创建box.dart
// 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
目录中检查以下内容-
<projectDir>/android/app/src/main/res/values/strings.xml
一个映射键${componentName}Name
到${componentName}
的每个延迟组件的条目。此字符串资源由每个功能模块的AndroidManifest.xml
用于定义dist:title property
。例如<?xml version="1.0" encoding="utf-8"?> <resources> ... <string name="boxComponentName">boxComponent</string> </resources>
-
<projectDir>/android/<componentName>
存在每个延迟组件的 Android 动态功能模块,并包含一个build.gradle
和src/main/AndroidManifest.xml
文件。这仅检查是否存在,而不验证这些文件的内容。如果文件不存在,它会生成一个默认推荐文件。 -
<projectDir>/android/app/src/main/res/values/AndroidManifest.xml
包含一个元数据条目,该条目对加载单元和加载单元关联的组件名称进行编码。此映射由嵌入器用于将 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 库添加到功能模块的库部分。记住以下准则
-
加载单元不应包含在多个组件中。
-
包含来自加载单元的一个 Dart 库表示整个加载单元已分配给延迟组件。
-
未分配给延迟组件的所有加载单元都包含在始终隐式存在的基组件中。
-
分配给同一延迟组件的加载单元一起下载、安装和交付。
-
基组件是隐式的,无需在 pubspec 中定义。
-
-
还可以通过在延迟组件配置中添加资产部分来包含资产
deferred-components: - name: boxComponent libraries: - package:MyAppName/box.Dart assets: - assets/image.jpg - assets/picture.png # wildcard directory - assets/gallery/
资产可以包含在多个延迟组件中,但安装这两个组件会导致资产重复。还可以通过省略库部分来定义仅资产组件。这些仅资产组件必须使用
DeferredComponent
服务中的实用程序类而不是loadLibrary()
进行安装。由于 Dart 库与资产打包在一起,因此如果使用loadLibrary()
加载 Dart 库,组件中的任何资产也会被加载。但是,按组件名称和服务实用程序进行安装不会加载组件中的任何 dart 库。你可以将资产包含在任何组件中,只要它们在首次引用时安装并加载即可,尽管通常情况下,资产和使用这些资产的 Dart 代码最好打包在同一个组件中。
-
将
pubspec.yaml
中定义的所有延迟组件手动添加到android/settings.gradle
文件中作为包含项。例如,如果 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
文件来验证生成的加载单元是否按预期描述来进行检查。在调整延迟组件配置或进行添加、修改或移除加载单元的 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()
时,包含 Dart AOT 库和资源的所需 Android 模块将由 Flutter 引擎使用 Play 商店的交付功能下载。