This is the second MAD Skills series on Navigation. This is the fourth article in the series on Navigation components. If you want to review past posts, please check out the links below:

  • Overview of navigation components
  • Navigate to the dialog box
  • Use SafeArgs when navigating your application
  • Use deep links to navigate
  • Build your first App Bundle
  • NavigationUI is easy to understand
  • Use navigation components: Conditional navigation
  • Navigation: Nested navigation diagrams and

If you prefer to watch the video rather than read the article, check out the video here.

An overview of the

In the last article, you learned how to use Navigation in multi-module projects. In this article, we will go one step further and convert the coffee Module into a Feature Module. If you are not familiar with the functional modules, check out this video first.

Function modules are not downloaded to the local PC during installation, but are downloaded only when certain functions are used in the application. This not only saves time and bandwidth during application download and installation, but also saves storage space on the device.

So let’s save the user some space! Now get straight to programming!

Function module

Since I modularized the DonutTracker application in the last article, I’ll start by converting an existing coffee module into a success module.

First, I replaced the library plugin with a dynamic-feature plugin in the build.gradle of the coffee module:

id 'com.android.dynamic-feature'
Copy the code

Next, I declare the coffee module as an on-demand module in androidmanifest.xml:


      
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:dist="http://schemas.android.com/apk/distribution"
   package="com.android.samples.donuttracker.coffee">
   <dist:module
       dist:instant="false"
       dist:title="@string/title_coffee">
       <dist:delivery>
           <dist:on-demand />
       </dist:delivery>
       <dist:fusing dist:include="true" />
   </dist:module>
</manifest>
Copy the code

Now that the coffee module has been converted, I added this module as a dynamicFeature:

android {
   //...

   packagingOptions {
       exclude 'META-INF/atomicfu.kotlin_module'
   }

   dynamicFeatures = [':coffee']

}
Copy the code

At the same time in the app module build.gradle, I removed the coffee module from the dependency list and added the navigation-dynamic-features dependency:

implementation "androidx.navigation:navigation-dynamic-features-fragment:$navigationVersion"
Copy the code

When Gradle synchronization is complete, you can update the navigation diagram. I changed the include tag to include-dynamic and added id, graphResName, and moduleName to the function module:

<include-dynamic
   android:id="@+id/coffeeGraph"
   app:moduleName="coffee"
   app:graphResName="coffee_graph"/>
Copy the code

At this point, I can safely remove the ID attribute of the navigation tag in coffee_graph.xml because the Dynamic Navigator library ignores the ID attribute of the root element if the navigation graph is introduced using the include tag.

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   app:startDestination="@id/coffeeList">
   <fragment
       android:id="@+id/coffeeList"
       android:name="com.android.samples.donuttracker.coffee.CoffeeList"
       android:label="@string/coffee_list">
       <action
           android:id="@+id/action_coffeeList_to_coffeeEntryDialogFragment"
           app:destination="@id/coffeeEntryDialogFragment" />
   </fragment>
   <dialog
       android:id="@+id/coffeeEntryDialogFragment"
       android:name="com.android.samples.donuttracker.coffee.CoffeeEntryDialogFragment"
       android:label="CoffeeEntryDialogFragment">
       <argument
           android:name="itemId"
           android:defaultValue="-1L"
           app:argType="long" />
   </dialog>
</navigation>
Copy the code

In the Activity_main layout, I changed the FragmentContainerView name value from NavHostFragment to DynamicNavHostFragment:

<androidx.fragment.app.FragmentContainerView
       android:id="@+id/nav_host_fragment"
       android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:defaultNavHost="true"
       app:navGraph="@navigation/nav_graph" />
Copy the code

Similar to include navigation diagrams, for include-dynamic to work, the id value of the coffee menu item needs to match the navigation diagram name, not the destination page ID:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <item
       android:id="@id/donutList"
       android:icon="@drawable/donut_with_sprinkles"
       android:title="@string/donut_name" />
   <item
       android:id="@id/coffeeGraph"
       android:icon="@drawable/coffee_cup"
       android:title="@string/coffee_name" />
</menu>
Copy the code

That’s all it takes to add dynamic navigation. Now I’ll use Bundletool to test the feature module, or you can use the Play console to test the feature module. If you want to learn more about how to use bundletool and the Play console to test a feature module installation, check out this video.

I also want to test what happens when a module fails to install. To do this, in the Run/Debug Configurations popup, I unchecked donutTracker.coffee from the list of things to deploy. When I run the application again and navigate to the coffeeList page, a generic error message is displayed.

▽ General error message

Now that the functional modules are set up, it’s time to polish the user experience. When a feature module is being downloaded, would it be better to display custom feedback to the user or a more meaningful error message instead of a generic message?

To do this, I can add a listener that handles installation status, progress changes, or error messages when the user stays on the same page. Alternatively, I can add a custom progress Fragment to show progress while a feature module is downloading.

The navigation library already has built-in support for progress fragments. All I need to do is create a Fragment that inherits AbstractProgressFragment.

class ProgressFragment : AbstractProgressFragment(R.layout.fragment_progress) {
}
Copy the code

I added an ImageView, a TextView, and a ProgressBar to show the download status.

<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/default_margin"
   android:paddingTop="@dimen/default_margin"
   android:paddingRight="@dimen/default_margin"
   android:paddingBottom="@dimen/default_margin">
   <ImageView
       android:id="@+id/progressImage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@drawable/coffee_cup"
       android:layout_marginBottom="@dimen/default_margin"
       android:layout_gravity="center"/>
   <TextView
       android:id="@+id/message"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       tools:text="@string/installing_coffee_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>


Copy the code

Next, I overwrote the onProgress() function to update the progressBar, and I overwrote the onFailed() and onCanceled() functions to update the TextView to show the feedback to the user.

override fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long){ progressBar? .progress = (bytesDownloaded.toDouble() *100 / bytesTotal).toInt()
}
 
override fun onFailed(errorCode: Int){ message? .text = getString(R.string.install_failed) }override fun onCancelled(a){ message? .text = getString(R.string.install_cancelled) }Copy the code

I need to add the progressFragment destination to the navigation diagram. Finally, declare the progressFragment as the progressDestination of the navigation diagram.

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   app:startDestination="@id/donutList"
   app:progressDestination="@+id/progressFragment">
<fragment
       android:id="@+id/donutList"
       android:name="com.android.samples.donuttracker.donut.DonutList"
       android:label="@string/donut_list" >
       <action
           android:id="@+id/action_donutList_to_donutEntryDialogFragment"
           app:destination="@id/donutEntryDialogFragment" />
       <action
           android:id="@+id/action_donutList_to_selectionFragment"
           app:destination="@id/selectionFragment" />
   </fragment>
   <dialog
       android:id="@+id/donutEntryDialogFragment"
       android:name="com.android.samples.donuttracker.donut.DonutEntryDialogFragment"
       android:label="DonutEntryDialogFragment">
       <deepLink app:uri="myapp://navdonutcreator.com/donutcreator" />
       <argument
           android:name="itemId"
           app:argType="long"
           android:defaultValue="-1L" />
   </dialog>
   <fragment
       android:id="@+id/selectionFragment"
       android:name="com.android.samples.donuttracker.setup.SelectionFragment"
       android:label="@string/settings"
       tools:layout="@layout/fragment_selection" >
       <action
           android:id="@+id/action_selectionFragment_to_donutList"
           app:destination="@id/donutList" />
   </fragment>
   <fragment
       android:id="@+id/progressFragment"
       android:name="com.android.samples.donuttracker.ProgressFragment"
       android:label="ProgressFragment" />
   <include-dynamic
       android:id="@+id/coffeeGraph"
       app:moduleName="coffee"
       app:graphResName="coffee_graph"/>
</navigation>
Copy the code

At this point, I uncheck the coffee module again and when I run the app and navigate to the coffeeList page, the app displays the custom progressFragment page.

△ Customize progressFragment

Similarly, I can test the application using Bundletool to see how the progress bar works while the coffee module is downloading.

summary

Thank you! In this series, we used Chet’s DonutTracker application again and added the coffee logging feature. Because… I like coffee.

New features bring new responsibilities. To provide a better user experience, I first added NavigationUI using navigation to integrate UI components. I then implemented one-time process and conditional navigation. After that, I used nested diagrams and include tags to organize the navigation diagrams and applied modularity to save users network and storage space. Now that we’re done with the app, it’s time to enjoy a delicious cup of coffee and donuts!

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!