Android 和 Web 的延迟加载组件
介绍
#使用 Flutter,Android 和 Web 应用能够在应用运行时下载延迟加载的组件(额外的代码和资源)。这对于大型应用非常有用,因为您可以只在用户需要时才安装这些组件。
虽然 Flutter 支持 Android 和 Web 上的延迟加载,但实现方式有所不同。两者都需要 Dart 的延迟导入。
Android 的 动态功能模块 将延迟加载的组件打包成 Android 模块进行分发。
构建 Android 应用时,虽然可以延迟加载模块,但必须构建整个应用并将其作为单个 Android App Bundle (AAB) 上传。Flutter 不支持在不重新上传整个应用程序的 Android App Bundle 的情况下分发部分更新。
当您以 发布或剖析模式 编译 Android 应用时,Flutter 会执行延迟加载,但在调试模式下,所有延迟加载的组件都将被视为常规导入。
Web 会将延迟加载的组件创建为单独的
*.js
文件。
有关此功能工作原理的更深入的技术细节,请参阅 Flutter wiki 上的 延迟加载组件。
如何为 Android 项目设置延迟加载组件
#以下说明将介绍如何为 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 文档中所述),任何一种都有效使您的应用程序类继承
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
声明为应用程序的子类,并将FlutterApplication
中的 Flutter 兼容性代码添加到您的应用程序类中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 延迟加载库。在 第 3.3 步 中,一旦gen_snapshot
生成了加载单元,您将在此处填写。
步骤 2:实现延迟加载的 Dart 库
#接下来,在您应用的 Dart 代码中实现延迟加载的 Dart 库。此实现目前不需要功能齐全。本页面其余部分的示例添加了一个新的简单延迟加载小部件作为占位符。您还可以通过修改导入并使用 loadLibrary()
Futures
来保护延迟代码的使用,将现有代码转换为延迟加载。
创建一个新的 Dart 库。例如,创建一个新的
DeferredBox
小部件,它可以在运行时下载。此小部件可以具有任何复杂性,但出于本指南的目的,创建一个简单的框作为占位符。要创建一个简单的蓝色框小部件,请创建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
等待initState
中创建的loadLibrary
Future
完成,并显示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
构建应用,指示其生成单独的 SO 文件的分片 AOT 共享库。首次运行时,验证器很可能会因检测到问题而失败;该工具会提供有关如何设置项目和修复这些问题的建议。验证器分为两个部分:构建前验证和 post-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.gradle
和src/main/AndroidManifest.xml
文件。这仅检查文件是否存在,而不验证其内容。如果文件不存在,则会生成一个默认的推荐文件。<projectDir>/android/app/src/main/res/values/AndroidManifest.xml
包含一个元数据条目,该条目编码了加载单元与加载单元关联的组件名称之间的映射。嵌入器使用此映射来将 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 库添加到功能模块的库部分。请牢记以下指南:
不应将加载单元包含在多个组件中。
包含加载单元中的一个 Dart 库表示整个加载单元都分配给了延迟加载组件。
所有未分配给延迟加载组件的加载单元都包含在基本组件中,该组件始终是隐式存在的。
分配给同一延迟加载组件的加载单元将一起下载、安装和打包。
基本组件是隐式的,无需在 pubspec 中定义。
还可以通过在延迟加载组件配置中添加
assets
部分来包含资源yamldeferred-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 代码打包在同一个组件中。
手动将您在
pubspec.yaml
中定义的所有延迟加载组件添加到android/settings.gradle
文件中作为 include。例如,如果 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
文件来验证生成的加载单元是否按预期描述,从而检查此问题。调整延迟加载组件配置或进行添加、修改或删除加载单元的 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()
时,包含 Dart AOT 库和资源的所需 Android 模块将由 Flutter 引擎使用 Play 商店的交付功能下载。