延迟加载组件
简介
#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
中添加以下内容:groovy... 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
应用属性来完成:xml<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
:javapublic 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
实例来处理延迟组件的安装请求。通过在应用程序的初始流程中添加以下代码,将PlayStoreDeferredComponentManager
添加到 Flutter 嵌入层中:javaimport 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
下,并选择延迟组件:yaml... 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
中使用以下内容创建了一个简单的蓝色方框:box.dartdartimport '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 代码。dartimport '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<?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
包含一个 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
验证程序在预构建验证通过之前不会运行。 -
-
对于每个检查,该工具会创建或者修改需要的文件。这些文件放在
<projectDir>/build/android_deferred_components_setup_files
目录下。建议通过复制和覆盖项目android
目录中的相同文件来应用更改。在覆盖之前,当前的项目状态应该被提交到源代码管理中,并检查建议的改动。该工具不会自动更改android
目录。 -
一旦生成可用的加载单元并将其记录到
<projectDirectory>deferred_components_loading_units.yaml
中,便可完善 pubspec 的deferred-components
配置,将加载单元分配给延迟的组件。在上面的案例中,生成的deferred_components_loading_units.yaml
文件将包含:yamlloading-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 中定义。
-
-
静态资源也可以通过在延迟组件中配置 assets 进行添加:
yamldeferred-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
中包含以下内容:groovyinclude ':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 模块。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-07-03。 查看文档源码 或者 为本页面内容提出建议。