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:
- Overview of navigation components
- Navigate to the dialog box
- Use SafeArgs when navigating in the app
- Use deep link navigation
- Build your first app bundle
- NavigationUI
- Use navigation component: Conditional navigation
- navigation: nested navigation map and <include>
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
.
△ 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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。