在功能模块中使用导航 | MAD Skills
这是关于导航 (Navigation) 的第二个 MAD Skills 系列,本文是导航组件系列的第四篇文章,如果您想回顾过去发布的内容,请通过下面链接查看:
导航组件概览 导航到对话框 在应用中导航时使用 SafeArgs 使用深层链接导航 打造您的首个 app bundle 深入浅出 NavigationUI 使用导航组件: 条件导航 导航: 嵌套导航图和 <include>
△ 功能模块的导航视频
概述
在上一篇文章中,您已经学会了如何在多模块工程中使用导航 (Navigation)。在本文中,我们将更进一步,将咖啡模块转换成功能模块 (Feature Module)。如果对功能模块不太熟悉,您可以先查看以下视频内容:
功能模块在安装时并未下载到本地,而是当应用使用到某个功能时才会下载相应的功能模块。这不仅节省了应用下载和安装时的时间和带宽,也节省了设备存储空间。
那么让我们为用户节省一些空间!现在直接开始编程吧!
功能模块
id 'com.android.dynamic-feature'接着,我在 AndroidManifest.xml 中将咖啡模块声明为按需 (on-demand) 模块:
<?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>现在咖啡模块已经转换完成,我将该模块添加为动态功能 (dynamicFeature):
android { //...
packagingOptions { exclude 'META-INF/atomicfu.kotlin_module' }
dynamicFeatures = [':coffee']
}同时在 app 模块的 build.gradle 中,我从依赖列表中移除了咖啡模块并添加了 navigation-dynamic-features 依赖:
implementation "androidx.navigation:navigation-dynamic-features-fragment:$navigationVersion"当 Gradle 同步完成时,即可更新导航图了。我将 include 标签改为 include-dynamic,并添加 id、graphResName 以及指向功能模块的 moduleName:
<include-dynamic android:id="@+id/coffeeGraph" app:moduleName="coffee" app:graphResName="coffee_graph"/>此时,我可以安全地移除 coffee_graph.xml 中 navigation 标签的 id 属性,原因在于,如果导航图是使用 include 标签引入的,那么 Dynamic Navigator 库会忽略根元素的 id 属性。
<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>在 activity_main 布局中,我将 FragmentContainerView 的 name 属性值由 NavHostFragment 改为 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" /><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>class ProgressFragment : AbstractProgressFragment(R.layout.fragment_progress) {}进度 Fragment
https://developer.android.google.cn/guide/navigation/navigation-dynamic
<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>
接着,我覆写了 onProgress() 函数来更新 progressBar,我还覆写了 onFailed() 和 onCanceled() 函数来更新 TextView 以向用户展示相关反馈。
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)}我需要将 progressFragment 目的地添加到导航图中。最后,将 progressFragment 声明为导航图的 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>此时,我再次取消勾选咖啡模块,运行应用并导航至 coffeeList 页面时,应用展示了自定义进度页面 progressFragment。
△ 自定义 progressFragment
类似地,我可以使用 bundletool 测试应用以查看当咖啡模块正在下载时,进度条会如何工作。
小结
推荐阅读