Flutter 2.2 brings many new features, but one that interests me most is the “Deferred Components” lazy loading component. This feature allows us to break up Flutter products into multiple components and download them as needed. With the help of this feature, the official Gallery demonstrates that the installation size of the program is reduced by 46% (200KB of code and 43MB of resources reduced)

This article has been published in Flutter Chinese documentation -> Performance Optimization -> Lazy-loading Components

Original article: Google engineer Gary Qian

Introduction to the

Flutter supports building applications that download additional Dart code and static resources at runtime. This reduces the size of the apK installed application and downloads functionality and static resources when the user needs them.

We refer to each individual downloadable Dart library and static resource as a “deferred component.” This feature is currently only available on Android, and the code in the deferred component does not affect other platforms, which normally build applications with all the deferred components and resources during the initial installation.

Lazy loading is only available if the application is compiled into Release or Profile mode. In Debug mode, all delayed components are treated as regular imports and are loaded immediately upon startup. Therefore, hot overloading is still possible in Debug mode.

For technical details about this feature, see the Flutter delay Component Principle and Customization on the Flutter Wiki.


How can a project support lazy-loading components

The following bootstrap shows you how to set up an Android application to support lazy loading.

Note that we can’t use Google Play for product distribution in China. We need to implement custom download module for DeferredComponentManager.

Step 1: Dependencies and initial project setup

  1. Add Play Core to the Build. gradle dependency of your Android application. Add the following to ‘Android /app/build.gradle’ :

    .dependencies{... implementation"Com. Google. Android. Play: core: 1.8.0 comes with". }Copy the code
  2. If you use the Google Play store as a distribution model of dynamic capabilities, the application must support ` SplitCompat ` and manually provide ` PlayStoreDeferredComponentManager ` instance. Both of these tasks can be set by ` android/app/SRC/main/AndroidManifest. XML ` ` android: name ` for ` IO. Flatter. App. FlatterPlayStoreSplitApplication ` application properties to complete:

    <manifest .
      <application
         android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
            .
      </application>
    </manifest>
    Copy the code

    IO. Flutter. App. FlutterPlayStoreSplitApplication has completed the tasks for you. If you use the FlutterPlayStoreSplitApplication, you can skip step 1.3.

    If your Android applications very big or very complicated, you may need to separate support SplitCompat and provide PlayStoreDynamicFeatureManager.

    There are three ways to support SplitCompat (see Android Docs for details), any one of which is valid:

    • Make your application class inherit ‘SplitCompatApplication’ :

      public class MyApplication extends SplitCompatApplication {... }Copy the code
    • Call ‘splitCompat.install (this)’ from ‘attachBaseContext()’; ` :

      @Override
      protected void attachBaseContext(Context base) {
          super.attachBaseContext(base);
          // Emulates installation of future on demand modules using SplitCompat.
          SplitCompat.install(this);
      }
      Copy the code
    • Declare ‘SplitCompatApplication’ as a subclass of Application and add flutter compatibility code from ‘FlutterApplication’ to your application class:

      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      Copy the code

    The embedded layer relies on injected instances of DeferredComponentManager to handle installation requests for deferred components. Through the application, the following code is added to the initial process of embedding PlayStoreDeferredComponentManager added to Flutter in the layer:

    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    importio.flutter.FlutterInjector; . layStoreDeferredComponentManager deferredComponentManager =new
      PlayStoreDeferredComponentManager(this.null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    Copy the code
  3. By adding the ‘deferred-Components’ dependency to’ flutter ‘in your application’s’ pubspec.yaml ‘and selecting the delay component:

    .
    flutter:
      .
      deferred-components:
      .
    Copy the code

    The Flutter tool looks for deferred- Components in pubspec.yaml to determine whether the application should be built as lazy loading. Unless you already know the required components and the Dart delay library in each component, leave them blank for now. After gen_snapshot generates the load unit, you can improve this section in step 3.3.

Step 2: Implement the lazy-loaded Dart library

Next, implement the lazy-loaded Dart library in the Dart code. Implement functionality that is not immediately required. The example in the rest of the article adds a simple delay widget as a placeholder. You can also convert existing code to deferred code by modifying the import and protect usage of lazy-loaded code after loadLibrary() and Futures.

  1. Create a new Dart library. For example, create a ‘DeferredBox’ widget that can be downloaded at run time. This widget can be arbitrarily complex, and this tutorial creates a simple box using the following.

    // box.dart
    
    import 'package:flutter/widgets.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      DeferredBox() {}
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 30,
          width: 30, color: Colors.blue, ); }}Copy the code
  2. Import the new Dart library in the application using the ‘deferred’ keyword and call ‘loadLibrary()’. This example USES the ` FutureBuilder ` waiting ` loadLibrary ` ` of Future ` object (in ` initState ` created) is completed, and will be ` CircularProgressIndicator ` as a placeholder. When ‘Future’ is complete, ‘DeferredBox’ is returned. The ‘SomeWidget’ can then be used normally in the application without attempting to access the delayed Dart code until successfully loaded.

    import 'box.dart' deferred as box;
    
    // ...
    
    class SomeWidget extends StatefulWidget {
      @override
      _SomeWidgetState createState() => _SomeWidgetState();
    }
    
    class _SomeWidgetState extends State<SomeWidget> {
      Future<void> _libraryFuture;
    
      @override
      void initState() {
        _libraryFuture = box.loadLibrary();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<void>(
          future: _libraryFuture,
          builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              }
              return box.DeferredBox();
            }
            returnCircularProgressIndicator(); }); }}// ...
    Copy the code

    The loadLibrary() function returns a Future

    object that returns successfully when the code in the deferred library is available, or an error otherwise. All symbols in the delay library should ensure that loadLibrary() is complete before being used. All imported libraries must pass the Deferred flag so that they can be compiled properly and used in deferred components. If the component is already loaded, calling loadLibrary again will return quickly (but not synchronously complete). It is also possible to preload by calling the loadLibrary() function ahead of time to help mask load times.

    You can find other examples of lazy loading components in Flutter Gallery’s lib/ deferred_Widget.dart.

Step 3: Build the application

Build the delay component application with the following flutter command:

$ flutter build appbundle
Copy the code

This command will help you check that the project is properly set up to build deferred component applications. By default, any problems detected by the validator will cause the build to fail, and you can fix them with system-suggested changes.

You can disable building deferred components with the — no-deferred-Components flag. This flag causes all deferred components defined in Pubspec.yaml to be treated as normal components defined in assets. All Dart code is compiled into a shared library, and the loadLibrary() call is completed in the next event loop (as soon as possible in asynchronous cases). This flag is also equivalent to removing deferred-Components: from pubspec.yaml.

  1. The ‘flutter build appbundle’ command attempts to build the application by splitting the AOT shared libraries into separate ‘. So ‘files via’ gen_snapshot ‘. The first time you run it, the validator may fail when it detects problems, and the tool provides advice on how to set up the project and resolve them.

    The validator is divided into two parts: pre-build and validation after snapshot generation. This is because no validation of references to load units can be performed until gen_Snapshot completes and generates the last set of load units.

    You can use the — no-validate-deferred-Components flag to let tools try to build applications without performing validators. This can lead to failures caused by unexpected and incorrect instructions. This flag should only be used for custom implementations of the default Play-store-based that do not rely on validation.

    The validator checks all new, modified, or deleted load units generated by gen_Snapshot. The currently generated load units are recorded in the / deferred_COMPONentS_loading_units.yaml file. This file should be added to versioning to ensure that changes made to the load unit by other developers can be tracked.

    The validator also checks the android directory for the following:

    • The key-value pair mapping for each deferred componentName is’ ${componentName}Name ‘:’ ${componentName} ‘. The ‘androidmanifest.xml’ of each function module uses this string resource to define ‘dist:title Property’. Such as:

      
                
      <resources>.<string name="boxComponentName">boxComponent</string>
      </resources>
      Copy the code
    • Every delay components has an Android dynamic function module, it contains a ` build. Gradle ` and ` SRC/main/AndroidManifest. ` XML file. The verifier only checks for the existence of the file, not its contents. If the file does not exist, it will generate a default recommendation file.

    • Contains a meta-data key-value pair that encodes the mapping between the load unit and its associated component name. The embeder uses this mapping to convert the Dart’s internal load unit ID to the name of the delay component to install. Such as:

      . <applicationandroid: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>
          ...
      Copy the code

    The gen_SNAPSHOT validation program will not run until pre-build validation passes.

  2. For each check, the tool creates or modifies the required files. These files are placed in the directory ‘/build/ Android_deferred_COMPONents_setup_files’. It is recommended that changes be applied by copying and overwriting the same files in the project’s ‘Android’ directory. Before overwriting, the current status of the project should be submitted to source control and reviewed for proposed changes. The tool does not automatically change the ‘Android’ directory.

  3. Once the available load units are generated and recorded in ‘deferred_COMPONentS_loading_units. Yaml’, pubSpec’s ‘deferred-Components’ configuration can be perfected to assign load units to delayed components. In the above case, the generated ‘deferred_COMPONentS_loading_units. yaml’ file will contain:

    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    Copy the code

    The load unit ID (” 2 “in this case) is used internally by Dart and can be ignored. The base load unit (id “1”) contains everything that is not explicitly listed in the other load units and is not listed here.

    Now you can add the following to pubspec.yaml:

    .
    flutter:
      .
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      .
    Copy the code

    Assign the load unit to the delay component and add any Dart libraries from the load unit to the libraries section of the function module. Please remember the following guidelines:

    • A load unit can be contained in only one delay component

    • Referencing a Dart library in the load unit means that the entire load unit is contained within the delay component.

    • All load units that are not assigned to the deferred component are contained in the base component, which always exists implicitly.

    • Load units assigned to the same delay component are downloaded, installed, and run together.

    • The base components are implicit and do not need to be defined in PubSpec.

  4. Static assets can also be added by configuring assets in the deferred component:

      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
          assets:
            - assets/image.jpg
            - assets/picture.png
              # wildcard directory
            - assets/gallery/
    Copy the code

    A static resource can be contained in more than one deferred component, but installing both components results in duplication of resources. Libraries can also be omitted to define the delay component of a purely static resource. The components of these static resources must be installed with the DeferredComponent utility class in the service, not loadLibrary(). Because the Dart library is packaged with static resources, if you load the Dart library with loadLibrary(), all resources in the component will also be loaded. However, the install by component name and service utility does not load any Dart libraries in the component.

    You are free to include resources in any component you choose, as long as they are installed and loaded when first referenced, but in general, static resources and the Dart code that uses them are best packaged in the same component.

  5. Manually add all delayed components defined in ‘pubspec.yaml’ to the includes section of the ‘Android /settings.gradle’ file. For example, if pubSpec defines three delay components named ‘boxComponent’, ‘circleComponent’, and ‘assetComponent’, Make sure ‘Android /settings.gradle’ contains the following:

    include ':app'.':boxComponent'.':circleComponent'.':assetComponent'.Copy the code
  6. Repeat steps’ 3.1 ‘through’ 3.6 ‘(this step) until all validator recommendations are processed and the tool runs without more recommendations.

    Success, this command will be in the build/app/outputs/bundle/release output directory app – the aab file.

    A successful build does not always mean that the application is built as expected. You need to make sure that all the loading units and Dart libraries are included the way you want them to be. For example, a common mistake is to accidentally import a Dart library without the Deferred keyword, resulting in a lazy loading library being compiled as part of the base loading unit. In this case, the Dart library will load correctly because it always exists in the base component, and the library will not be split. You can verify that the expected load unit generates a description by examining the deferred_COMPONentS_loading_units. yaml file.

    You should expect the validator to fail when adjusting the delay component configuration, or when making changes to add, modify, or remove load units. Follow all the recommendations in Steps 3.1 through 3.6 (this step) to continue building.

Run the application locally

Once your application has successfully built a.aab file, you can use Bundletool for Android to perform local tests with the — Local Testing flag.

To run the.aab file on the test device, go to github.com/google/bund… Download the Bundletool JAR executable and run:

$ 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
Copy the code


is the directory location for your application’s corresponding project, and

is used to store all the temporary directories for Bundletool output. This will unzip your.aab file into an.apks file and install it on the device. All of the dynamic features available for Android are locally loaded onto the device and simulate the installation of delayed components.

Before running build-apks again, remove the existing app .apks file:

Please delete existing app.apks files before running build-apks again:

$ rm <your_temp_dir>/app.apks
Copy the code

Changes to the Dart code base require either adding an Android build ID or uninstalling and reinstalling the application. Because Android does not update feature modules unless a new version number is detected.

Publish to the Google Play store

The resulting.aab file can be uploaded directly to the Google Play store as usual. When loadLibrary() is called, the Flutter engine will use Android modules containing the Dart AOT libraries and resources downloaded from the store.


The last

Dynamism has always been a sore point on the mobile end, and one of the criticisms of Flutter. After 2.2, the official government directly provides this capability, which makes it easier for us to implement. Based on this feature, I personally believe that there should also be related solutions for thermal repair as well as volume optimization. Although Google Play is not available in China, the overall process can still be referred to this article. We can customize the download and decompression of delayed components according to our business. I will also translate more details about delay components in the next article, including how to customize the hypervisor for delay components. If you’re interested, please follow, like, and leave your thoughts in the comments.

Nayuta is a senior Android engineer at Shell House

Thank you:

By Gary Qian

Vadaski, community member of Flutter.cn, works for Didi Chuxing, and Caijinglong, Alex, MeandNi and Chenglu from the community

Past quality columns:

How to design and implement a high-performance Flutter list

The Flutter core rendering mechanism

Flutter routing design and source code analysis

Flutter event distribution

The most detailed guide to the advancement and optimization of Flutter has been collected in Advance of Flutter or RunFlutter.