Dynamic Navigator 库扩展了 Jetpack Navigation 组件 的功能,使其能够与 功能模块 中定义的目标一起使用。此库还提供无缝安装按需功能模块,当您导航到这些目标时。
设置
为了支持功能模块,在您的应用模块的 build.gradle
文件中使用以下依赖项
Groovy
dependencies { def nav_version = "2.8.0" api "androidx.navigation:navigation-fragment-ktx:$nav_version" api "androidx.navigation:navigation-ui-ktx:$nav_version" api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" api("androidx.navigation:navigation-fragment-ktx:$nav_version") api("androidx.navigation:navigation-ui-ktx:$nav_version") api("androidx.navigation:navigation-dynamic-features-fragment:$nav_version") }
请注意,其他 Navigation 依赖项应使用 api 配置,以便它们可用于您的功能模块。
基本用法
要支持功能模块,首先将应用中所有 NavHostFragment
的实例更改为 androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
app:navGraph="@navigation/nav_graph"
... />
接下来,在 com.android.dynamic-feature
模块的导航图中,向您导航图中的任何 <activity>
、<fragment>
或 <navigation>
目标添加一个 app:moduleName
属性,这些目标与 DynamicNavHostFragment
相关联。此属性告诉 Dynamic Navigator 库该目标属于您指定的名称的功能模块。
<fragment
app:moduleName="myDynamicFeature"
android:id="@+id/featureFragment"
android:name="com.google.android.samples.feature.FeatureFragment"
... />
当您导航到这些目标之一时,Dynamic Navigator 库首先检查功能模块是否已安装。如果功能模块已存在,您的应用将按预期导航到该目标。如果模块不存在,您的应用将在安装模块时显示一个中间进度片段目标。进度片段的默认实现显示一个带有进度条的基本 UI,并处理任何安装错误。
要自定义此 UI,或从您自己的应用屏幕中手动处理安装进度,请参阅本主题中的 自定义进度片段 和 监控请求状态 部分。
没有指定 app:moduleName
的目标将继续工作而无需更改,并且表现得好像您的应用使用的是常规 NavHostFragment
。
自定义进度片段
您可以通过将 app:progressDestination
属性设置为要用于处理安装进度的目标的 ID,为每个导航图重写进度片段的实现。您的自定义进度目标应是一个 Fragment
,它派生自 AbstractProgressFragment
。您必须重写有关安装进度、错误和其他事件的通知的抽象方法。然后,您可以在您选择的 UI 中显示安装进度。
默认实现的 DefaultProgressFragment
类使用此 API 显示安装进度。
监控请求状态
Dynamic Navigator 库使您能够实现类似于 按需交付 UX 最佳实践 中的 UX 流程,在该流程中,用户在等待安装完成时停留在先前屏幕的上下文中。这意味着您根本不需要显示任何中间 UI 或进度片段。
在这种情况下,您负责监控和处理所有安装状态、进度更改、错误等。
要启动此非阻塞导航流程,请将包含 DynamicInstallMonitor
的 DynamicExtras
对象传递给 NavController.navigate()
,如以下示例所示
Kotlin
val navController = ... val installMonitor = DynamicInstallMonitor() navController.navigate( destinationId, null, null, DynamicExtras(installMonitor) )
Java
NavController navController = ... DynamicInstallMonitor installMonitor = new DynamicInstallMonitor(); navController.navigate( destinationId, null, null, new DynamicExtras(installMonitor); )
在调用 navigate()
后,您应该检查 installMonitor.isInstallRequired
的值,以查看尝试导航是否导致功能模块安装。
- 如果值为
false
,则您正在导航到常规目标,无需执行任何其他操作。 如果值为
true
,则应开始观察现在位于installMonitor.status
中的LiveData
对象。此LiveData
对象会发出来自 Play Core 库的SplitInstallSessionState
更新。这些更新包含您可以用来更新 UI 的安装进度事件。请记住处理 Play Core 指南 中概述的所有相关状态,包括在必要时 请求用户确认。Kotlin
val navController = ... val installMonitor = DynamicInstallMonitor() navController.navigate( destinationId, null, null, DynamicExtras(installMonitor) ) if (installMonitor.isInstallRequired) { installMonitor.status.observe(this, object : Observer<SplitInstallSessionState> { override fun onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { // Call navigate again here or after user taps again in the UI: // navController.navigate(destinationId, destinationArgs, null, null) } SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> { SplitInstallManager.startConfirmationDialogForResult(...) } // Handle all remaining states: SplitInstallSessionStatus.FAILED -> {} SplitInstallSessionStatus.CANCELED -> {} } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this); } } }); }
Java
NavController navController = ... DynamicInstallMonitor installMonitor = new DynamicInstallMonitor(); navController.navigate( destinationId, null, null, new DynamicExtras(installMonitor); ) if (installMonitor.isInstallRequired()) { installMonitor.getStatus().observe(this, new Observer<SplitInstallSessionState>() { @Override public void onChanged(SplitInstallSessionState sessionState) { switch (sessionState.status()) { case SplitInstallSessionStatus.INSTALLED: // Call navigate again here or after user taps again in the UI: // navController.navigate(mDestinationId, mDestinationArgs, null, null); break; case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: SplitInstallManager.startConfirmationDialogForResult(...) break; // Handle all remaining states: case SplitInstallSessionStatus.FAILED: break; case SplitInstallSessionStatus.CANCELED: break; } if (sessionState.hasTerminalStatus()) { installMonitor.getStatus().removeObserver(this); } } }); }
安装完成后,LiveData
对象会发出 SplitInstallSessionStatus.INSTALLED
状态。然后,您应该再次调用 NavController.navigate()
。由于模块现在已安装,因此调用现在成功,并且应用程序按预期导航到目标。
达到终端状态后,例如安装完成或安装失败时,应删除 LiveData
观察者以避免内存泄漏。您可以使用 SplitInstallSessionStatus.hasTerminalStatus()
检查状态是否表示终端状态。
有关此观察者的示例实现,请参阅 AbstractProgressFragment
。
包含的图表
Dynamic Navigator 库支持包含在功能模块中定义的图表。要包含在功能模块中定义的图表,请执行以下操作
使用
<include-dynamic/>
而不是<include/>
,如以下示例所示<include-dynamic android:id="@+id/includedGraph" app:moduleName="includedgraphfeature" app:graphResName="included_feature_nav" app:graphPackage="com.google.android.samples.dynamic_navigator.included_graph_feature" />
在
<include-dynamic ... />
中,您必须指定以下属性app:graphResName
:导航图表资源文件的名称。该名称派生自图表的文件名。例如,如果图表位于res/navigation/nav_graph.xml
中,则资源名称为nav_graph
。android:id
- 图表目标 ID。Dynamic Navigator 库会忽略包含图表根元素中找到的任何android:id
值。app:moduleName
:模块的包名。
使用正确的 graphPackage
获取 app:graphPackage
的正确值非常重要,因为否则导航组件将无法从功能模块中包含指定的 navGraph
。
动态功能模块的包名是通过将模块的名称附加到基本应用程序模块的 applicationId
来构建的。因此,如果基本应用程序模块的 applicationId
为 com.example.dynamicfeatureapp
,而动态功能模块名为 DynamicFeatureModule
,则动态模块的包名将为 com.example.dynamicfeatureapp.DynamicFeatureModule
。此包名区分大小写。
如果您有任何疑问,可以通过检查生成的 AndroidManifest.xml
来确认功能模块的包名。构建项目后,转到 <DynamicFeatureModule>/build/intermediates/merged_manifest/debug/AndroidManifest.xml
,它应该如下所示
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:dist="http://schemas.android.com/apk/distribution" featureSplit="DynamicFeatureModule" package="com.example.dynamicfeatureapp" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" /> <dist:module dist:instant="false" dist:title="@string/title_dynamicfeaturemodule" > <dist:delivery> <dist:install-time /> </dist:delivery> <dist:fusing dist:include="true" /> </dist:module> <application /> </manifest>
featureSplit
值应与动态功能模块的名称匹配,并且包将与基本应用程序模块的 applicationId
匹配。app:graphPackage
是它们的组合:com.example.dynamicfeatureapp.DynamicFeatureModule
。
导航到 include-dynamic 导航图表
只能导航到 include-dynamic
导航图表的 startDestination
。动态模块负责自己的导航图表,基本应用程序对此一无所知。
include-dynamic 机制使基本应用程序模块能够包含在动态模块中定义的 嵌套导航图表。此嵌套导航图表的行为与任何嵌套导航图表相同。根导航图表(即嵌套图表的父图表)只能将嵌套导航图表本身定义为目标,而不能定义其子图表。因此,当 include-dynamic 导航图表为目标时,使用 startDestination
。
限制
- 动态包含的图表当前不支持深层链接。
- 动态加载的嵌套图表(即具有
app:moduleName
的<navigation>
元素)当前不支持深层链接。