延迟加载组件

简介

Flutter 支持构建在运行时下载额外 Dart 代码和静态资源的应用程序。这可以减少安装应用程序 apk 的大小,并在用户需要时下载功能和静态资源。

我们将每个独立的可下载的 Dart 库和静态资源称为「延迟组件」。这是通过使用 Dart 的延迟导入来实现的,可以将其编译到拆分的 AOT 共享库中。

尽管模块可以延迟加载,但整个应用程序必须作为单个 App Bundle 完全构建和上传。不支持在没有重新上传整个新 Android 应用程序包的情况下发送部分更新。

延迟加载仅在应用程序编译为 Release 或 Profile 模式 时可用。在 Debug 模式下,所有延迟组件都被视为常规导入,它们在启动时立即加载。因此,Debug 模式下仍然可以热重载。

关于此功能的技术细节,请查看 Flutter wiki 上的 延迟加载组件

如何让项目支持延迟加载组件

下面的引导将介绍如何设置 Android 应用程序以支持延迟加载。

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

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

    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    
  2. 如果使用 Google Play 商店作为动态功能的分发模型,应用程序必须支持 SplitCompat 并手动提供 PlayStoreDeferredComponentManager 的实例。这两个任务都可以通过设置 android/app/src/main/AndroidManifest.xml 中的 android:nameio.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),其中任何一种都是有效的:

    • Make your application class extend SplitCompatApplication:

      让你的 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());
    
  3. 通过将 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 后面的延迟加载代码的导入和保护用法,将现有代码转换为延迟代码。

  1. Create a new Dart library. For example, create a new DeferredBox widget that can be downloaded at runtime. This widget can be of any complexity but, for the purposes of this guide, create a simple box as a stand-in. To create a simple blue box widget, create box.dart with the following contents:

    <p markdown="1">创建新的 Dart 库。例如,创建一个可以在运行时下载的 `DeferredBox` widget。这个 widget 可以是任意复杂的,本指南使用以下内容创建了一个简单的框。</p>
    
    // 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,
        );
      }
    }
  2. 在应用中使用 deferred 关键字导入新的 Dart 库,并调用 loadLibrary()(请参见 延迟加载库)。下面的示例使用 FutureBuilder 等待 loadLibraryFuture 对象(在 initState 中创建)完成,并将 CircularProgressIndicator 做为占位。当 Future 完成时,会返回 DeferredBoxSomeWidget 便可在应用程序中正常使用,在成功加载之前不会尝试访问延迟的 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() {
        _libraryFuture = box.loadLibrary();
        super.initState();
      }
    
      @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’s lib/deferred_widget.dart 中找到其他延迟加载组件的示例。

步骤 3:构建应用程序

使用以下 flutter 命令构建延迟组件应用:

$ flutter build appbundle

此命令会帮助你检查项目是否正确设置为构建延迟组件应用。默认情况下,验证程序检测到任何问题都会导致构建失败,你可以通过系统建议的更改来修复这些问题。

  1. 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.gradlesrc/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 验证程序在预构建验证通过之前不会运行。

  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 文件将包含:

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

  4. 静态资源也可以通过在延迟组件中配置 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 代码最好打包在同一组件中。

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

    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 文件,验证预期的加载单元是否生成描述。

    当调整延迟组件配置,或者进行添加、修改、删除加载单元的更改时,你应该预料到验证程序会失败。按照步骤 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 模块。