The Android Developer Challenge is back! Submit your idea before December 2.

Navigate with dynamic feature modules

The Dynamic Navigator library extends the functionality of the Jetpack Navigation component to work with destinations that are defined in dynamic feature modules. This library also provides seamless installation of on-demand dynamic feature modules when navigating to these destinations.

Prerequisites

You need to add the AndroidX snapshot repository to access the Dynamic Navigator library while it is in development.

Add the following repository to your root build.gradle:

repositories {
    maven {
        url "https://ci.android.com/builds/submitted/5956592/androidx_snapshot/latest/repository/"
    }
    ...
}

Basic usage

Add the following dependency to your Navigation module's build.gradle file:

dependencies {
    implementation "androidx.navigation:navigation-dynamic-features-fragment:2.3.0-SNAPSHOT"
}

To support dynamic feature modules, first change all instances of NavHostFragment in your app to androidx.navigation.dynamicfeature.DynamicNavHostFragment:

<fragment
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.dynamicfeature.DynamicNavHostFragment"
    app:navGraph="@navigation/nav_graph"
    ... />

Next, add an app:moduleName attribute to any <activity>, <fragment>, or <navigation> destinations in your navigation graphs that are associated with a DynamicNavHostFragment. This attribute tells the Dynamic Navigator library that the destination belongs to a dynamic feature module with the name that you specify.

<fragment
    app:moduleName="myDynamicFeature"
    android:id="@+id/featureFragment"
    android:name="com.google.android.samples.feature.FeatureFragment"
    ... />

When you navigate to one of these destinations, the Dynamic Navigator library first checks if the dynamic feature module is installed. If the dynamic feature module is already present, your app navigates to the destination as expected. If the module isn't present, your app shows an intermediate progress fragment destination as it installs the module. The default implementation of the progress fragment shows a basic UI with a progress bar and handles any installation errors. To customize this UI, or to manually handle installation progress from within your own app screen, see the Customize the progress fragment and Monitor the request state sections in this topic.

Destinations that don't specify app:moduleName continue to work without changes and behave as though your app uses a regular NavHostFragment.

Customize the progress fragment

You can override the progress fragment implementation for each navigation graph by setting the app:progressDestination attribute to the ID of the destination you want to use for handling installation progress. Your custom progress destination should be a Fragment that derives from AbstractProgressFragment. You must override the abstract methods for notifications about installation progress, errors, and other events. You can then show installation progress in a UI of your choice.

The default implementation's DefaultProgressFragment class uses this API to show installation progress.

Monitor the request state

The Dynamic Navigator library enables you to implement a UX flow similar to the one in UX best practices for on-demand delivery, in which a user stays in the context of a previous screen while waiting for installation to finish. This means that you don't need to show an intermediate UI or progress fragment at all. In this scenario, you are responsible for monitoring and handling all installation states, progress changes, errors, and so on.

To initiate this non-blocking navigation flow, pass a DynamicExtras object that contains a DynamicInstallMonitor to NavController.navigate(), as shown in the following example:

Kotlin

val navController = ...
val installMonitor = DynamicInstallMonitor()

navController.navigate(
    destinationId,
    null,
    null,
    DynamicExtras.Builder().setInstallMonitor(installMonitor).build()
)

if (installMonitor.isInstallRequired) {
    installMonitor.status.observe(this, object : Observer<SplitInstallSessionState> {
        override fun onChanged(sessionState: SplitInstallSessionState) {
            when (sessionState.status()) {
                SplitInstallSessionStatus.INSTALLED -> {
                    // Call navigate again here or after user taps again in the UI:
                    // navController.navigate(mDestinationId, mDestinationArgs, null, null);
                    break;
                }
                SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
                    SplitInstallManager.startConfirmationDialogForResult(...)
                    break;
                }

                // Handle all remaining states:
                SplitInstallSessionStatus.FAILED ->
                ...
                SplitInstallSessionStatus.CANCELED ->
                ...
            }

            if (sessionState.hasTerminalStatus()) {
                installMonitor.status.removeObserver(this);
            }
        }
    });
}

Java

NavController navController = ...
DynamicInstallMonitor installMonitor = new DynamicInstallMonitor();

navController.navigate(
    destinationId,
    null,
    null,
    DynamicExtras.Builder().setInstallMonitor(installMonitor).build();
)

if (installMonitor.isInstallRequired()) {
    installMonitor.getStatus().observe(this, new Observer<SplitInstallSessionState>() {
        @Override
        public void onChanged(SplitInstallSessionState sessionState) {
            switch (sessionState.status()) {
                case SplitInstallSessionStatus.INSTALLED:
                    // Call navigate again here or after user taps again in the UI:
                    // navController.navigate(mDestinationId, mDestinationArgs, null, null);
                    break;
                case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
                    SplitInstallManager.startConfirmationDialogForResult(...)
                    break;

                // Handle all remaining states:
                case SplitInstallSessionStatus.FAILED:
                ...
                case SplitInstallSessionStatus.CANCELED:
                ...
            }

            if (sessionState.hasTerminalStatus()) {
                installMonitor.getStatus().removeObserver(this);
            }
        }
    });
}

Immediately after calling navigate(), you should check the value of installMonitor.isInstallRequired to see if the attempted navigation resulted in a dynamic feature module installation.

  • If the value is false, you're navigating to a normal destination and don't need to do anything else.
  • If the value is true, you should start observing the LiveData object that is now in installMonitor.status. This LiveData object emits SplitInstallSessionState updates from the Play Core library. These updates contain installation progress events that you can use to update the UI. Remember to handle all relevant statuses as outlined in the Play Core guide, including asking for user confirmation if necessary.

When the installation finishes, the LiveData object emits a SplitInstallSessionStatus.INSTALLED status. You should then call NavController.navigate() again. Since the module is now installed, the call now succeeds, and the app navigates to the destination as expected.

After reaching a terminal state, such as when installation completes or when installation fails, you should remove your LiveData observer to avoid memory leaks. You can check if the status represents a terminal state by using SplitInstallSessionStatus.hasTerminalStatus().

See AbstractProgressFragment for an example implementation of this observer.

Included graphs

The Dynamic Navigator library supports including graphs that are defined in dynamic feature modules. To include a graph that is defined in a dynamic feature module, do the following:

  1. Use <include-dynamic/> instead of <include/>.
  2. Specify the following attributes:

    • app:graphPackage: the package name of the dynamic feature module. The package name is usually in the form of applicationId.moduleName. You can verify the package name by opening a dynamic feature APK in Android Studio's APK Analyzer and then opening the resources.arsc file.
    • app:graphResName: the name of the navigation graph resource file. The name is derived from the graph's file name. For example, if the graph is in res/navigation/nav_graph.xml, the resource name is nav_graph.
    • android:id - the graph destination ID. The Dynamic Navigator library ignores any android:id values that are found in the root element of the included graph.
    • app:moduleName: the dynamic feature module name.
    <include-dynamic
       android:id="@+id/includedGraph"
       app:moduleName="includedgraphfeature"
       app:graphResName="included_feature_nav"
       app:graphPackage="com.google.android.samples.dynamicnavigator.includedgraphfeature" />
    

Note the following:

  • Dynamically-included graphs don't currently support deep links.
  • Dynamically-loaded nested graphs (that is, a <navigation> element with an app:moduleName) don't currently support deep links.

Android Studio support

Android Studio's Navigation Editor does not currently support the Dynamic Navigator library. Opening a navigation graph that contains any of the tags or attributes outlined in this document might cause exceptions and other undefined behavior. You should work directly with the navigation graph XML.