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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Navigation and Dynamic Features
25 mins
- Getting Started
- Why Use Android App Bundles?
- How Dynamic Delivery Works
- How Feature Modules Work
- How Navigation Component Works
- Navigation With Dynamic Feature Modules
- DynamicNavHostFragment
- Migration to Dynamic Feature Modules
- Testing Navigation and Dynamic Features
- Including Graphs From Dynamic Features
- Including Subgraphs From Feature Modules
- Smoother Transitions Between Features
- Customize the Progress Fragment
- Non-Blocking Navigation Flow
- Handling the Results of the Installation Attempt
- Where to Go From Here?
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:
- Providing a custom progress fragment that shows while the feature module is loading.
- 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.
Under File name enter fragment_progress and click OK.
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.
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.
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:
- Create a new instance of
DynamicInstallMonitor
. - Pass it to
NavController
and navigate to the Info screen. - 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 observeDynamicInstallMonitor
‘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.