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. By Ivan Kušt.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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