头图

This is the second MAD Skills series on Navigation. This article is the fourth article in the navigation component series. If you want to review the content published in the past, please check it through the link below:

If you prefer to watch video rather than reading the article, please click here to view video content.

Overview

In last article , you've learned how to use navigation (Navigation) in a multi-module project. In this article, we will go one step further and convert the coffee module into a feature module (Feature Module). If you are not familiar with the functional modules, you can first view the video content.

The functional module is not downloaded to the local during installation, but the corresponding functional module is downloaded when the application uses a certain function. This not only saves time and bandwidth during application download and installation, but also saves device storage space.

So let us save some space for users! Now start programming directly!

function module

Since I have modularized the DonutTracker application in the last article , I will start by converting the existing coffee module into a functional 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'

Next, I AndroidManifest.xml the coffee module as an on-demand module in 06135a762ede53:

<?xml version="1.0" encoding="utf-8"?>
<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>

Now that the coffee module has been converted, I added the module as a dynamic function ( dynamicFeature ):

android {
   //...

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

   dynamicFeatures = [':coffee']

}

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

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

When Gradle synchronization is complete, the navigation map can be updated. I will include label to include-dynamic , and add id , graphResName and pointing function module moduleName :

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

At this point, I can safely remove coffee_graph.xml in navigation label id property, because, if the navigation map is using the label include the introduction, then the Dynamic Navigator library ignores the id attribute of the root element.

<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>

In the layout of activity_main FragmentContainerView the attribute value of name 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" />

Similar to the introduction of the navigation map through include, for the include-dynamic to take effect, the id value of the coffee menu item needs to match the name of the navigation map, instead of 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>

This is all you need to add dynamic navigation. Now I will use bundletool to test function modules, you can also use the Play console to test function modules. If you want to learn more about how to use bundletool and Play console to test the installation of functional modules, please view this video .

I also want to test what happens when the module fails to install. For this reason, in the Run/Debug Configurations pop-up window, I unchecked donuttracker.coffee from the list to be deployed. At this time, when I run the application again and navigate to the coffeeList page, a general error message will be displayed.

△ 通用错误信息

△ General error message

At this point, the function module settings have been completed, and it is time to polish the user experience. When the function module is in the downloading process, would it be better to display custom feedback information to the user or display a more meaningful error message instead of general information?

To this end, I can add a listener that can handle installation status, progress changes or error messages when the user stays on the same page. Or, when the function module is being downloaded, I can add a custom progress Fragment to show the progress.

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

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

I added a 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>

Then, I overwrite the onProgress() function to update progressBar , and I also overwrite the onFailed() and onCanceled() functions to update TextView to show users relevant feedback.

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() {
   message?.text = getString(R.string.install_cancelled)
}

I need to progressFragment destination 06135a762ee0bb to the navigation map. Finally, progressFragment declared map of progressDestination .

<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>

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

△ 自定义 progressFragment

△ Custom progressFragment

Similarly, I can use bundletool to test the application to see how the progress bar will work when the coffee module is downloading.

Summary

Thank you everyone! In this series, we used Chet's DonutTracker application and added the coffee recording function. Because...I like coffee.

New features bring new responsibilities. In order to provide a better user experience, first I added NavigationUI using navigation to integrate UI components. Then, I implemented a one-time process and conditional navigation. After that, I used nested graphs and include tags to organize the navigation graph and modularized the application to save users' network and storage space. At this point, we have completed the application, it's time to enjoy a cup of delicious coffee and donuts!

You are welcome to click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。