Home · Android & Kotlin Tutorials

Navigation and Dynamic Features

In this tutorial, you’ll learn how to use an experimental version of Navigation Controller to navigate between dynamic feature modules.

5/5 4 Ratings

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3.5

At I/O 2018, Google introduced a new publishing format: Android App Bundles. These eliminate the need to publish multiple, large APK files for your app to run well on different devices. With App Bundles, each user gets an APK that’s tailor-made for their specific device.

App Bundles also offer dynamic delivery. This means you don’t have to install the whole app to use it, but divide the app into feature modules. At first, you just install the main module. You then install other modules only when needed.

In this tutorial, you’ll learn to use Dynamic Navigator libraries by refactoring Garden Planner, a single-module app, to use feature modules and dynamic navigation.

Your task is to separate the app into the following feature modules:

  • The existing app module.
  • An info feature module containing a screen with section details.
  • A notes feature module containing screens to add section and garden notes.
Note: This tutorial assumes you know what Navigation Component and Android App Bundles are. For more information about Navigation component, check out our Navigation Architecture Component Tutorial.

There’s also an awesome Jetpack Navigation Controller screencast, which quickly walks you through how to build a navigation graph.

For a start on App bundles check out our Getting Started With Android App Bundles tutorial.

Getting Started

Start by using the Download Materials button at the top or bottom of this page to download everything you’ll need for this project.

Next, open the Garden Planner project in Android Studio 3.5 or later by selecting Open an existing Android Studio project from the Android Studio welcome screen:

Opening an existing project in Android Studio

Take a moment to get familiar with the app. Build and run and you’ll see the app’s main screen:

Garden Planner's main screen

The app is a simple garden planner that lets you plan out the layout of your garden. Tap on an empty tile and you’ll get a screen that lets you select what you’d like to plant there:

Options to plant in your garden

See the details or edit the selection by tapping on the section again:

Detail view of the Garden Planner

Tap on Info to get details about which plant you’ve set for a specific section:

Cabbage detail screen

Finally, you can add notes to either the section or the garden in general:

Adding notes

Before you start refactoring the app, take a deeper look at how the new features work.

Why Use Android App Bundles?

While Android App Bundles offer several advantages, they have one major disadvantage: Navigation between screens in modules is more complicated than with standard library modules. Here’s why.

When you use standard library modules, the main module can see them all. This makes using anything from the library modules simple:

Standard library module dependency layout

With feature modules, the dependencies are reversed. The modules have the main module as a dependent. Additionally, it’s possible that the user hasn’t installed a feature module yet. This makes navigating to screens implemented in feature modules tricky:

Feature module dependency layout

Until now, you had two approaches to use:

  1. Use reflection to access code in the feature module.
  2. Create a library module with interfaces for dynamic features that acts as a common dependency for base module and feature modules. You then load implementations at runtime with ServiceLoader.

Now, there’s a third, simpler option: Using a Dynamic Navigator library. This enables you to use Navigation Controller to navigate between destinations in feature modules.

Note: Google is actively developing the library, but it’s currently unstable. You shouldn’t use it in production until it reaches a stable release.

How Dynamic Delivery Works

The idea behind dynamic delivery is that the Play Store generates optimized APKs for each user’s device configuration. This has two benefits:

  • You upload only one archive on the Play Store, saving time.
  • Each user gets an optimized APK when they download the app, which makes the download size smaller.

How does it work? First, you upload the app in the new delivery format, Android App Bundle, which contains all the compiled code and resources for all configurations.

When users download your app, the Play Store uses dynamic delivery to generate optimized APK for their device.

Play Store delivering customized app downloads

How Feature Modules Work

Another benefit of dynamic delivery is that your app doesn’t have to install all at once. Instead, you break it down into pieces that install on demand.

This is possible thanks to a mechanism in Dynamic Delivery called split APK.

Note: Split APKs are available on Android 5.0 and higher.

Split APKs are similar to regular APKs, except they contain only a part of your app. Android is able to treat multiple split APKs as a single app. This enables you to break up your app into smaller chunks that download and install when required.

There are three types of split APKs:

  • Base APK: Contains code and resources for the app’s base functionality. Downloads when the user first downloads the app.
  • Configuration APKs: Hold native libraries and specific screen density, CPU architecture or language resources.
  • Dynamic feature APKs: Contain code and resources for a feature of your app from a single feature module. You can customize when and how the feature downloads using Play Core library.

How Navigation Component Works

Jetpack Navigation component consists of three key parts:

  • Navigation graph: An XML resource file that contains definitions for all navigation destinations in the app and their connections.
  • NavHost implementation: A container that displays destinations you define in navigation graph. Navigation component library provides default NavHostFragment implementation.
  • NavController: Manages navigation between destinations within NavHost.

When you navigate through your app, you tell NavController which destination you want to navigate to. NavController then displays the proper destination in the NavHost.

Note: for more information on Navigation Controller, check the official Android Developer documentation.

Navigation With Dynamic Feature Modules

Google’s Dynamic Navigator library extends the functionality of Navigation Component by enabling it to navigate to destinations from other feature modules. It does so by providing a new implementation of NavHost interface: DynamicNavHostFragment.

It’s time to add the library to the app!

Open the project-level build.gradle and add the maven block inside repositories so that the repository looks like:

allprojects {
  repositories {
    google()
    jcenter()
    maven {
      url "https://ci.android.com/builds/submitted/6043188/androidx_snapshot/latest/repository/"
    }
  }
}

This will add the snapshot repository to access the Dynamic Navigator library.

Finally, add the library dependency. Open build.gradle from the app module and add the following under dependencies:

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

Now, build and run to see if you get any errors.

Build and Run After Gradle Changes

DynamicNavHostFragment

You’re ready to start refactoring the sample project to use feature modules.

The next thing to do is to replace NavHostFragment with DynamicNavHostFragment. Open activity_main.xml, located in app/src/res/layout. Replace the following line:

android:name="androidx.navigation.fragment.NavHostFragment"

with:

android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"

Note that DynamicNavHostFragment supports everything that NavHostFragment supports, plus it can navigate to a destination from a feature module.

Build and run again. There are no visible changes yet, but you’ve laid the foundation for adding feature modules.

Build and Run After Gradle Changes

Migration to Dynamic Feature Modules

Now it’s time to add two dynamic feature modules:

  • info: A feature module containing a screen with section details.
  • notes: The feature module containing screens for adding section and garden notes.

Right-click on the app module in the project’s Hierarchy viewer and select New ▸ Module.

Menu to select a new module in the Hierarchy viewer

Select Dynamic Feature Module from the list and click Next.

New Module screen

Under Module name, enter info and under package name enter com.raywenderlich.android.gardenplanner.info.

Make sure you’ve selected API level 19 under Minimum API level to match the minimum API level in the app module. Click Next.

Configuring your module

On the next screen, enter Info for Module title. Select Do not include module at install-time (on-demand only) under Install-time inclusion. Also check the Fusing checkbox.

This ensures that if the device is using Android version 4.4 or lower, which doesn’t support split APKs, the dynamic feature module will install immediately when app installs.

Module Download Options screen

Click Finish and wait for Gradle to sync. Open build.gradle from the new module and add the following after the first apply plugin line:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

This will add Kotlin support to the new feature module. Now, add the following inside the android section:

compileOptions {
  sourceCompatibility JavaVersion.VERSION_1_8
  targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = "1.8"
}

This will prevent compile errors by syncing the project with the Gradle files. Sync the project.

Now, select the info package in the app module. Right-click and choose Refactor ▸ Move.

Where to find Refactor ▸ Move

On the dialog that appears, select the second option, Move directory … to another source root and click OK.

Menu showing the Move directory ... to another source root option

In the Select Source Root dialog, select src/main/java from the info module and click OK.

Selecting the source route

You’ll now see the contents of the info package in the info module.

Directory structure showing the info package in the info module

Next, you’ll need to set up your layout and values folders.

To do this, right-click on the res folder in the info module and select New ▸ Directory.

Menu setting up a new directory

For the name, type layout, then repeat the process and create another folder named values.

Creating the values folder

Now that you have these files set up in the app module, you want to create them in the info module as well.

Do this by dragging and dropping fragment_garden_item_info.xml from res/layout in the app module into the new res/layout folder in the info module. Press OK if a move dialog appears.

Then drag and drop res/values/info_string.xml from app module to res/values/ folder in info module and press OK if a move dialog appears.

New directory structure in the info module.

You’ll have to fix imports for the resources you’ve moved, so open GardenItemInfoRepository and replace:

import com.raywenderlich.android.gardenplanner.R

with

import com.raywenderlich.android.gardenplanner.info.R

Do the same in GardenItemInfoFragment. This makes sure that you import the Resources file from the info module instead of the one from the app module.

Now, build and run to make sure there are no errors.

Garden Planner home screen – so far, so good

Before you move on to your next step, you have one more quick thing to do: Fix the screen that displays your information.

Start by opening nav_graph.xml from res/navigation.

Find the fragment tag with the following ID: gardenItemInfoFragment and add the following property to it:

app:moduleName="info"

It should now look like this:

<fragment
  android:id="@+id/gardenItemInfoFragment"
  android:name="com.raywenderlich.android.gardenplanner.info.GardenItemInfoFragment"
  app:moduleName="info" >

  <argument android:name="gardenSection"
    app:argType="com.raywenderlich.android.gardenplanner.model.GardenSection" />

</fragment>

Now build and run. You won’t see any changes when running your app from Android Studio. To test dynamic feature modules, you have to upload your app to the Play Store. You’ll do that next.

Testing Navigation and Dynamic Features

Note: For more information on uploading you app to the Play Store, check out this section of Android App Bundles: Getting Started.

Once you’ve uploaded your app, you’re in luck! You can use Play Store to test a version of your app with dynamic features with internal app sharing.

Sharing a version of your app on the Play Store to test

Once you’ve uploaded a version of your app with dynamic features, you’ll get a link to use for testing. Download the app to your device using the URL and you’re ready to try your new features.

First, test downloading the info feature module on demand. To do this, tap on a tile that has a plant in it, then tap Info.

App loading the info feature module

Note: To share a URL to your app via the Play Store, you must first publish your app. For more details on how to do this, check the official documentation.

If you’re not ready to publish yet, you can set up a closed or internal test for your app. Google’s official documentation tells you how.

Including Graphs From Dynamic Features

To navigate from the main module to the dynamic feature modules, you will include the navigation graphs.

First, create a new dynamic feature module named notes. Repeat the steps you used to create the first dynamic module:

  1. Right-click on app module and select New ▸ Module.
  2. Select Dynamic Feature Module and click Next.
  3. Under Module name, type notes.
  4. Under Package name, type com.raywenderlich.android.gardenplanner.notes and click Next.
  5. For Module title, enter Notes and click Finish.

Again, wait for Gradle to sync. Open build.gradle in the new module and add the following after the first apply plugin line:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"

Add the following in the android section:

 on the Play Store
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = "1.8"
}

Also, add the following dependencies:

implementation 'androidx.room:room-runtime:2.2.3'
implementation "androidx.room:room-ktx:2.2.3"
  kapt "androidx.room:room-compiler:2.2.3"

Select the notes package in the app module. Right-click and choose Refactor ▸ Move. Select the second option Move directory … to another source root again and click OK.

Finally, in the Select Source Root dialog, select src/main/java in the notes module and click OK.

Copying the classes for the Notes module

Create three new folders in the res folder of the notes module: layout, navigation and values.

Creating new folders for your notes module

Now, you need to set up the new module, just as you did for the previous modules. To do this:

  1. Drag and drop res/values/notes_strings.xml to the new values folder.
  2. Next, drag and drop res/navigation/notes_nav_graph.xml to the new navigation folder.
  3. Finally, drag and drop the following layout files to the new layout folder:
  • bottom_sheet_add_note.xml
  • fragment_note_details.xml
  • fragment_notes.xml
  • item_note.xml

Notes module setup

Finally, replace the import:

import com.raywenderlich.android.gardenplanner.R

with:

import com.raywenderlich.android.gardenplanner.notes.R

in AddNoteDialogFragment, NoteAdapter, NoteDetailsFragment and NotesFragment.

Again, you’re making sure that the app is using the files from the right module.

Including Subgraphs From Feature Modules

Open nav_graph.xml and locate include tag. Replace it with the following:

<include-dynamic
  android:id="@+id/notes_nav_graph"
  app:moduleName="notes"
  app:graphResName="notes_nav_graph"
  app:graphPackage="com.raywenderlich.android.gardenplanner.notes">

  <argument android:name="gardenSection"
    app:argType="com.raywenderlich.android.gardenplanner.model.GardenSection"
    app:nullable="true" />

</include-dynamic>

This is the special version of include from Dynamic Navigator library that enables you to include subgraphs from feature modules. Note the extra properties:

  • moduleName: Contains the name of the feature module where the graph resource resides.
  • graphResName: Specifies the name of the subgraph resource.
  • graphPackage: Holds the root package of the feature module that you specified when creating the module.

Open notes_nav_graph in the notes module. Remove the following property from the root navigation tag:

android:id="@+id/notes_nav_graph"

This will prevent a crash when including notes_nav_graph in nav_graph in the app module.

Build and run. Tap on the Floating Action button at the bottom-right. You’ll see the screen with the list of notes.

Congratulations! You’ve successfully included a navigation graph from a feature module.

Smoother Transitions Between Features

To provide the best user experience, you can customize the way the app behaves while its feature modules are loading.

There are two approaches you can use, depending on the level of customization you need:

  1. Providing a custom progress fragment that shows while the feature module is loading.
  2. Monitoring the state of the feature module download and handling it yourself.

Customize the Progress Fragment

The dynamic feature navigator library provides a base fragment that shows progress while the module is loading: AbstractProgressFragment.

To try it out, expand the app module in Project Explorer and right-click on res/layout. Select New ▸ Layout Resource File.

Setting up a new layout resource file

Under File name enter fragment_progress and click OK.

Setting up the progress fragment

Paste the following in the file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:paddingLeft="@dimen/horizontal_margin"
  android:paddingTop="@dimen/vertical_margin"
  android:paddingRight="@dimen/horizontal_margin"
  android:paddingBottom="@dimen/vertical_margin">

  <ImageView
    android:id="@+id/progressImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_launcher_foreground"
    android:layout_marginBottom="@dimen/vertical_margin"
    android:contentDescription="@string/module_icon"
    android:layout_gravity="center"/>

  <TextView
    android:id="@+id/message"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:text="@string/installing_notes_module"/>

  <ProgressBar
    android:id="@+id/progressBar"
    style="@style/Widget.AppCompat.ProgressBar.Horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:progress="10" />

</LinearLayout>

This is the layout for the custom progress fragment.

Now, right-click on com.raywenderlich.android.gardenplanner in the app module and select New ▸ Kotlin File/Class. Under name enter GardenPlannerProgressFragment and click OK.

Paste the following in the new file:

package com.raywenderlich.android.gardenplanner

import androidx.navigation.dynamicfeatures.fragment.ui.AbstractProgressFragment
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
import kotlinx.android.synthetic.main.fragment_progress.view.*

class GardenPlannerProgressFragment : AbstractProgressFragment(R.layout.fragment_progress) {

  override fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long) {
    view?.progressBar?.progress = (bytesDownloaded.toDouble() * 100 / bytesTotal).toInt()
  }

  override fun onFailed(errorCode: Int) {
    view?.message?.text = getString(R.string.installing_module_failed)
  }
  
  override fun onCancelled() {
    view?.message?.text = getString(R.string.installing_module_cancelled)
  }
}

This creates a new fragment that extends AbstractProgressFragment. The layout you created in the previous step passes to this fragment via Constructor. There are four callback methods provided that you can override:

  • onProgress(): Updates download progress.
  • onFailed(): Called if installing the module failed.
  • onCancelled(): Called if the user cancels downloading and installing the module.

Each progress update sets the progress bar to the new value. All other actions display a simple message on the TextView, above ProgressBar.

Open nav_graph and add a new fragment destination with the ID notesProgressFragment:

<fragment
  android:id="@+id/notesProgressFragment"
  android:name=
    "com.raywenderlich.android.gardenplanner.GardenPlannerProgressFragment" />

In the root navigation, add the following property:

app:progressDestination="@+id/notesProgressFragment"

This tells Navigation Controller to show GardenPlannerProgressFragment while the dynamic module loads.

To test this, you’ll have to upload the app to the Play Store or share via a Play Store link and then download it, as described above. Click the Floating Action button on the main screen. You’ll briefly see the custom progress dialog.

Now, build and run.

Loading progress shown on the main app screen

Non-Blocking Navigation Flow

If you need a different UI flow than showing a progress dialog while a feature module loads, the Dynamic Feature Navigator library provides a way.

Note: Check the official Android guidelines fox best UX practices.

The idea is that you monitor dynamic feature install flow and handle it any way you want. DynamicInstallMonitor provides that capabilit.

To try it yourself, you’ll first add some support methods.

Open GardenSectionDetailsDialogFragment and add the following code:

private fun navigateToInfo(installMonitor: DynamicInstallMonitor) {
  findNavController().navigate(
    GardenSectionDetailsDialogFragmentDirections
      .actionShowItemInfo(args.gardenSection),
        DynamicExtras.Builder().setInstallMonitor(installMonitor).build()
  )
}

This method initiates navigation to the Info screen and passes the given DynamicInstallMonitor.

Next, add the method to show the module install confirmation request:

private fun requestInfoInstallConfirmation(sessionState: SplitInstallSessionState) {
  view?.infoProgressBar?.visibility = View.GONE
  view?.infoButton?.isEnabled = true
  startIntentSenderForResult(
    sessionState.resolutionIntent().intentSender,
    INSTALL_REQUEST_CODE,
    null, 0, 0, 0, null
  )
}

Now, add a method to let the user know the install failed:

private fun showInfoInstallFailed() {
  view?.infoProgressBar?.visibility = View.GONE
  view?.infoButton?.isEnabled = true
  Toast.makeText(context, R.string.installation_failed, 
    Toast.LENGTH_SHORT).show()
}

And the one that shows information if the user cancels the install:

private fun showInfoInstallCanceled() {
  view?.infoProgressBar?.visibility = View.GONE
  view?.infoButton?.isEnabled = true
  Toast.makeText(context, R.string.installation_cancelled, 
    Toast.LENGTH_SHORT).show()
}

Next, replace the infoButton OnClick listener with the following code:

infoButton.setOnClickListener {
  //1
  val installMonitor = DynamicInstallMonitor()
  //2
  navigateToInfo(installMonitor)
  //3
  if (installMonitor.isInstallRequired) {
    view.infoProgressBar.visibility = View.VISIBLE
    view?.infoButton?.isEnabled = false

    installMonitor.status.observe(
      viewLifecycleOwner,
      object : Observer<SplitInstallSessionState> {
        override fun onChanged(sessionState: SplitInstallSessionState?) {
          when (sessionState?.status()) {
            SplitInstallSessionStatus.INSTALLED -> {
              view.infoProgressBar.visibility = View.GONE
              view?.infoButton?.isEnabled = true
              navigateToInfo(installMonitor)
            }
            SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> 
              requestInfoInstallConfirmation(sessionState)
            SplitInstallSessionStatus.FAILED -> showInfoInstallFailed()
            SplitInstallSessionStatus.CANCELED -> showInfoInstallCanceled()
          }

          sessionState?.let {
            if (it.hasTerminalStatus()) {
              installMonitor.status.removeObserver(this)
            }
          }
        }
    })
  }
}

Here you:

  1. Create a new instance of DynamicInstallMonitor.
  2. Pass it to NavController and navigate to the Info screen.
  3. After navigating, you use isInstallRequired to check if the feature module is installed. If so, there’s nothing more to do. Otherwise, you have to observe DynamicInstallMonitor‘s status.

You have to handle four cases by checking the value of status from SplitInstallSessionState:

  • If the module installs successfully, you use NavController to navigate to the Info screen.
  • If the install needs confirmation from the user, you start the appropriate Intent by calling requestInfoInstallConfirmation(), which you added earlier.
  • If the install failed, you show the appropriate error message.
  • If the user cancels the install, you show the appropriate info.

Finally, after checking the status of the install, you check if you can unsubscribe from observing DynamicInstallMonitor status by using DynamicInstallMonitor.isEndState().

Handling the Results of the Installation Attempt

There’s one thing remaining to do: Handle the result of the install confirmation intent.

To do that, override onActivityResult:

override fun onActivityResult(requestCode: Int, 
    resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)
  if(requestCode == INSTALL_REQUEST_CODE) {
    if(resultCode == Activity.RESULT_CANCELED) {
      showInfoInstallCanceled()
    }
  }
}

The only thing to check here is if the user canceled the install and then to show the appropriate message, if so.

Again, to test the changes you’ll need to upload your app to the Play Store. After you’ve done so, delete it from your device or emulator and install the current version from the Play Store.

To test the new functionality, tap on a tile and select an Item to occupy it. Then select that item again and click on Info at the bottom. You’ll see the progress bar briefly while the info module downloads and installs.

Now build and run.

Final app screen

Where to Go From Here?

That’s it! You’ve explored what the Dynamic Navigator library offers. It’s exciting to see how the library matures and what more features are in store.

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Make sure you check the official documentation. You can track the development of the library by checking this issue.

Also, you can check this video from Android Dev Summit 2019.

I hope you’ve enjoyed this tutorial on Navigation and Dynamic Features. If you have any comments or questions, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments