Deferred components
Introduction
Flutter has the capability to build apps that can download additional Dart code and assets at runtime. This allows apps to reduce install apk size and download features and assets when needed by the user.
We refer to each uniquely downloadable bundle of Dart libraries and assets as a “deferred component”. This is achieved by using Dart’s deferred imports, which can be compiled into split AOT shared libraries.
Though modules can be defer loaded, the entire application must be completely built and uploaded as a single Android App Bundle. Dispatching partial updates without re-uploading new Android App Bundles for the entire application is not supported.
Deferred loading is only performed when the app is compiled to release or profile mode. In debug mode, all deferred components are treated as regular imports, so they are present at launch and load immediately. Therefore, debug builds can still hot reload.
For a deeper dive into the technical details of how this feature works, see Deferred Components on the Flutter wiki.
How to set your project up for deferred components
The following instructions explain how to set up your Android app for deferred loading.
Step 1: Dependencies and initial project setup
-
Add Play Core to the Android app’s build.gradle dependencies. In
android/app/build.gradle
add the following:... dependencies { ... implementation "com.google.android.play:core:1.8.0" ... }
-
If using the Google Play Store as the distribution model for dynamic features, the app must support
SplitCompat
and provide an instance of aPlayStoreDeferredComponentManager
. Both of these tasks can be accomplished by setting theandroid:name
property on the application inandroid/app/src/main/AndroidManifest.xml
toio.flutter.app.FlutterPlayStoreSplitApplication
:<manifest ... <application android:name="io.flutter.app.FlutterPlayStoreSplitApplication" ... </application> </manifest>
io.flutter.app.FlutterPlayStoreSplitApplication
handles both of these tasks for you. If you useFlutterPlayStoreSplitApplication
, you can skip to step 1.3.If your Android application is large or complex, you might want to separately support
SplitCompat
and provide thePlayStoreDynamicFeatureManager
manually.To support
SplitCompat
, there are three methods (as detailed in the Android docs), any of which are valid:-
Make your application class extend
SplitCompatApplication
:public class MyApplication extends SplitCompatApplication { ... }
-
Call
SplitCompat.install(this);
in theattachBaseContext()
method:@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Emulates installation of future on demand modules using SplitCompat. SplitCompat.install(this); }
-
Declare
SplitCompatApplication
as the application subclass and add the Flutter compatibility code fromFlutterApplication
to your application class:<application ... android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"> </application>
The embedder relies on an injected
DeferredComponentManager
instance to handle install requests for deferred components. Provide aPlayStoreDeferredComponentManager
into the Flutter embedder by adding the following code to your app initialization: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());
-
-
Opt into deferred components by adding the
deferred-components
entry to the app’spubspec.yaml
under theflutter
entry:... flutter: ... deferred-components: ...
The
flutter
tool looks for thedeferred-components
entry in thepubspec.yaml
to determine whether the app should be built as deferred or not. This can be left empty for now unless you already know the components desired and the Dart deferred libraries that go into each. You will fill in this section later in step 3.3 oncegen_snapshot
produces the loading units.
Step 2: Implementing deferred Dart libraries
Next, implement deferred loaded Dart libraries in your
app’s Dart code. The implementation does not need
to be feature complete yet. The example in the
rest of this page adds a new simple deferred widget
as a placeholder. You can also convert existing code
to be deferred by modifying the imports and
guarding usages of deferred code behind loadLibrary()
Futures
.
-
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, createbox.dart
with the following contents:// 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, ); } }
-
Import the new Dart library with the
deferred
keyword in your app and callloadLibrary()
(see lazily loading a library). The following example usesFutureBuilder
to wait for theloadLibrary
Future
(created ininitState
) to complete and display aCircularProgressIndicator
as a placeholder. When theFuture
completes, it returns theDeferredBox
widget.SomeWidget
can then be used in the app as normal and won’t ever attempt to access the deferred Dart code until it has 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(); } return CircularProgressIndicator(); }, ); } } // ...
The
loadLibrary()
function returns aFuture<void>
that completes successfully when the code in the library is available for use and completes with an error otherwise. All usage of symbols from the deferred library should be guarded behind a completedloadLibrary()
call. All imports of the library must be marked asdeferred
for it to be compiled appropriately to be used in a deferred component. If a component has already been loaded, additional calls toloadLibrary()
complete quickly (but not synchronously). TheloadLibrary()
function can also be called early to trigger a pre-load to help mask the loading time.You can find another example of deferred import loading in Flutter Gallery’s lib/deferred_widget.dart.
Step 3: Building the app
Use the following flutter
command to build a deferred
components app:
$ flutter build appbundle
This command assists you by validating that your project is properly set up to build deferred components apps. By default, the build fails if the validator detects any issues and guides you through suggested changes to fix them.
-
The
flutter build appbundle
command runs the validator and attempts to build the app withgen_snapshot
instructed to produce split AOT shared libraries as separate.so
files. On the first run, the validator will likely fail as it detects issues; the tool makes recommendations for how to set up the project and fix these issues.The validator is split into two sections: prebuild and post-gen_snapshot validation. This is because any validation referencing loading units cannot be performed until
gen_snapshot
completes and produces a final set of loading units.The validator detects any new, changed, or removed loading units generated by
gen_snapshot
. The current generated loading units are tracked in your<projectDirectory>/deferred_components_loading_units.yaml
file. This file should be checked into source control to ensure that changes to the loading units by other developers can be caught.The validator also checks for the following in the
android
directory:-
<projectDir>/android/app/src/main/res/values/strings.xml
An entry for every deferred component mapping the key${componentName}Name
to${componentName}
. This string resource is used by theAndroidManifest.xml
of each feature module to define thedist:title property
. For example:<?xml version="1.0" encoding="utf-8"?> <resources> ... <string name="boxComponentName">boxComponent</string> </resources>
-
<projectDir>/android/<componentName>
An Android dynamic feature module for each deferred component exists and contains abuild.gradle
andsrc/main/AndroidManifest.xml
file. This only checks for existence and does not validate the contents of these files. If a file does not exist, it generates a default recommended one. -
<projectDir>/android/app/src/main/res/values/AndroidManifest.xml
Contains a meta-data entry that encodes the mapping between loading units and component name the loading unit is associated with. This mapping is used by the embedder to convert Dart’s internal loading unit id to the name of a deferred component to install. For example:... <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> ...
The
gen_snapshot
validator won’t run until the prebuild validator passes. -
-
For each of these checks, the tool produces the modified or new files needed to pass the check. These files are placed in the
<projectDir>/build/android_deferred_components_setup_files
directory. It is recommended that the changes be applied by copying and overwriting the same files in the project’sandroid
directory. Before overwriting, the current project state should be committed to source control and the recommended changes should be reviewed to be appropriate. The tool won’t make any changes to yourandroid/
directory automatically. -
Once the available loading units are generated and logged in
<projectDirectory>/deferred_components_loading_units.yaml
, it is possible to fully configure the pubspec’sdeferred-components
section so that the loading units are assigned to deferred components as desired. To continue with the box example, the generateddeferred_components_loading_units.yaml
file would contain:loading-units: - id: 2 libraries: - package:MyAppName/box.Dart
The loading unit id (‘2’ in this case) is used internally by Dart, and can be ignored. The base loading unit (id ‘1’) is not listed and contains everything not explicitly contained in another loading unit.
You can now add the following to
pubspec.yaml
:... flutter: ... deferred-components: - name: boxComponent libraries: - package:MyAppName/box.Dart ...
To assign a loading unit to a deferred component, add any Dart lib in the loading unit into the libraries section of the feature module. Keep the following guidelines in mind:
-
Loading units should not be included in more than one component.
-
Including one Dart library from a loading unit indicates that the entire loading unit is assigned to the deferred component.
-
All loading units not assigned to a deferred component are included in the base component, which always exists implicitly.
-
Loading units assigned to the same deferred component are downloaded, installed, and shipped together.
-
The base component is implicit and need not be defined in the pubspec.
-
-
Assets can also be included by adding an assets section in the deferred component configuration:
deferred-components: - name: boxComponent libraries: - package:MyAppName/box.Dart assets: - assets/image.jpg - assets/picture.png # wildcard directory - assets/gallery/
An asset can be included in multiple deferred components, but installing both components results in a replicated asset. Assets-only components can also be defined by omitting the libraries section. These assets-only components must be installed with the
DeferredComponent
utility class in services rather thanloadLibrary()
. Since Dart libs are packaged together with assets, if a Dart library is loaded withloadLibrary()
, any assets in the component are loaded as well. However, installing by component name and the services utility won’t load any dart libraries in the component.You are free to include assets in any component, as long as they are installed and loaded when they are first referenced, though typically, assets and the Dart code that uses those assets are best packed in the same component.
-
Manually add all deferred components that you defined in
pubspec.yaml
into theandroid/settings.gradle
file as includes. For example, if there are three deferred components defined in the pubspec named,boxComponent
,circleComponent
, andassetComponent
, ensure thatandroid/settings.gradle
contains the following:include ':app', ':boxComponent', ':circleComponent', ':assetComponent' ...
-
Repeat steps 3.1 through 3.6 (this step) until all validator recommendations are handled and the tool runs without further recommendations.
When successful, this command outputs an
app-release.aab
file inbuild/app/outputs/bundle/release
.A successful build does not always mean the app was built as intended. It is up to you to ensure that all loading units and Dart libraries are included in the way you intended. For example, a common mistake is accidentally importing a Dart library without the
deferred
keyword, resulting in a deferred library being compiled as part of the base loading unit. In this case, the Dart lib would load properly because it is always present in the base, and the lib would not be split off. This can be checked by examining thedeferred_components_loading_units.yaml
file to verify that the generated loading units are described as intended.When adjusting the deferred components configurations, or making Dart changes that add, modify, or remove loading units, you should expect the validator to fail. Follow steps 3.1 through 3.6 (this step) to apply any recommended changes to continue the build.
Running the app locally
Once your app has successfully built an .aab
file,
use Android’s bundletool
to perform
local testing with the --local-testing
flag.
To run the .aab
file on a test device,
download the bundletool jar executable from
github.com/google/bundletool/releases 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
Where <your_app_project_dir>
is the path to your app’s
project directory and <your_temp_dir>
is any temporary
directory used to store the outputs of bundletool.
This unpacks your .aab
file into an .apks
file and
installs it on the device. All available Android dynamic
features are loaded onto the device locally and
installation of deferred components is emulated.
Before running build-apks
again,
remove the existing app .apks file:
$ rm <your_temp_dir>/app.apks
Changes to the Dart codebase require either incrementing the Android build ID or uninstalling and reinstalling the app, as Android won’t update the feature modules unless it detects a new version number.
Releasing to the Google Play store
The built .aab
file can be uploaded directly to
the Play store as normal. When loadLibrary()
is called,
the needed Android module containing the Dart AOT lib and
assets is downloaded by the Flutter engine using the
Play store’s delivery feature.