跳至主要内容

延迟加载组件

简介

#

Flutter 能够构建可在运行时下载其他 Dart 代码和资源的应用。这使得应用能够减少安装 apk 大小,并在用户需要时下载功能和资源。

我们将每个可单独下载的 Dart 库和资源捆绑包称为“延迟组件”。要加载这些组件,请使用Dart 的延迟导入。它们可以编译成拆分的 AOT 和 JavaScript 共享库。

虽然您可以延迟加载模块,但必须构建整个应用并将其作为单个Android 应用包 (*.aab) 上传。Flutter 不支持在不重新上传整个应用的新 Android 应用包的情况下分发部分更新。

当您在发布或概要文件模式下编译应用时,Flutter 会执行延迟加载。调试模式将所有延迟组件视为常规导入。这些组件在启动时存在并立即加载。这允许调试版本进行热重载。

要深入了解此功能的工作原理的技术细节,请参阅 Flutter wiki 上的延迟组件

如何为您的项目设置延迟组件

#

以下说明解释了如何为您的 Android 应用设置延迟加载。

步骤 1:依赖项和初始项目设置

#
  1. 将 Play Core 添加到 Android 应用的 build.gradle 依赖项中。在android/app/build.gradle中添加以下内容

    groovy
    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
  2. 如果使用 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

      java
      public 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 嵌入器。

    java
    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());
  3. 通过在应用的pubspec.yaml中(在flutter条目下)添加deferred-components条目来选择加入延迟组件。

    yaml
    ...
    flutter:
      ...
      deferred-components:
      ...

    flutter工具会在pubspec.yaml中查找deferred-components条目,以确定应用是否应作为延迟组件构建。除非您已经知道所需的组件和每个组件中包含的 Dart 延迟库,否则现在可以将其保留为空。您将在步骤 3.3中完成此部分,届时gen_snapshot将生成加载单元。

步骤 2:实现延迟 Dart 库

#

接下来,在应用的 Dart 代码中实现延迟加载的 Dart 库。实现不必是功能完整的。此页面其余部分中的示例添加了一个新的简单延迟小部件作为占位符。您还可以通过修改导入并在loadLibrary() Futures后面保护延迟代码的使用来将现有代码转换为延迟代码。

  1. 创建一个新的 Dart 库。例如,创建一个新的DeferredBox小部件,可以在运行时下载。此小部件可以具有任何复杂性,但出于本指南的目的,我们将创建一个简单的框作为替代。要创建一个简单的蓝色框小部件,请使用以下内容创建box.dart

    box.dart
    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,
        );
      }
    }
  2. 在您的应用中使用deferred关键字导入新的 Dart 库,并调用loadLibrary()(请参阅延迟加载库)。以下示例使用FutureBuilder等待loadLibrary Future(在initState中创建)完成,并显示CircularProgressIndicator作为占位符。当Future完成时,它会返回DeferredBox小部件。然后可以在应用中像往常一样使用SomeWidget,并且在成功加载之前永远不会尝试访问延迟的 Dart 代码。

    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

此命令通过验证您的项目是否已正确设置为构建延迟组件应用来为您提供帮助。默认情况下,如果验证程序检测到任何问题,则构建会失败,并指导您进行建议的更改以修复它们。

  1. flutter build appbundle命令运行验证程序并尝试使用gen_snapshot构建应用,该应用指示生成拆分的 AOT 共享库作为单独的.so文件。在第一次运行时,验证程序可能会失败,因为它检测到问题;该工具会提供有关如何设置项目并修复这些问题的建议。

    验证程序分为两个部分:预构建和后 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.gradlesrc/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验证程序不会运行。

  2. 对于这些检查中的每一个,该工具都会生成通过检查所需的修改或新文件。这些文件放置在<projectDir>/build/android_deferred_components_setup_files目录中。建议通过复制和覆盖项目android目录中的相同文件来应用这些更改。在覆盖之前,应将当前项目状态提交到源代码控制,并应审查推荐的更改以确保其适用。该工具不会自动对您的android/目录进行任何更改。

  3. 一旦生成并记录了可用的加载单元<projectDirectory>/deferred_components_loading_units.yaml,就可以完全配置 pubspec 的deferred-components部分,以便根据需要将加载单元分配给延迟组件。要继续使用框示例,生成的deferred_components_loading_units.yaml文件将包含

    yaml
    loading-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 中定义。

  4. 还可以通过在延迟组件配置中添加资源部分来包含资源

    yaml
      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 代码最好打包在同一个组件中。

  5. 手动将您在pubspec.yaml中定义的所有延迟组件作为包含项添加到android/settings.gradle文件中。例如,如果在 pubspec 中定义了三个名为boxComponentcircleComponentassetComponent的延迟组件,请确保android/settings.gradle包含以下内容

    groovy
    include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
    ...
  6. 重复步骤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()时,Flutter 引擎使用 Play 商店的交付功能下载包含 Dart AOT 库和资源的所需 Android 模块。