通过功能模块导航

Dynamic Navigator 库扩展了 Jetpack Navigation 组件的功能,使其能够与 功能模块中定义的目标配合使用。此库还可在导航到这些目标时,无缝安装按需功能模块。

设置

为了支持功能模块,请在应用模块的 build.gradle 文件中添加以下依赖项:

Groovy

dependencies {
    def nav_version = "2.9.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.9.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"
    ... />

接下来,将 app:moduleName 属性添加到 <activity><fragment><navigation> 目标中,这些目标位于与 DynamicNavHostFragment 关联的 com.android.dynamic-feature 模块的导航图中。此属性会告知 Dynamic Navigator 库该目标属于您指定名称的功能模块。

<fragment
    app:moduleName="myDynamicFeature"
    android:id="@+id/featureFragment"
    android:name="com.google.android.samples.feature.FeatureFragment"
    ... />

当您导航到其中一个目标时,Dynamic Navigator 库首先检查功能模块是否已安装。如果功能模块已存在,您的应用会按预期导航到目标。如果模块不存在,您的应用会显示一个中间进度 fragment 目标,同时安装模块。进度 fragment 的默认实现会显示一个带有进度条的基本 UI,并处理任何安装错误。

two loading screens that show UI with a progress bar when navigating
         to a feature module for the first time
图 1. 首次导航到按需功能时显示进度条的 UI。当相应模块下载时,应用会显示此屏幕。

要自定义此 UI,或在您的应用屏幕内手动处理安装进度,请参阅本主题中的自定义进度 fragment监控请求状态部分。

未指定 app:moduleName 的目标继续工作,行为与应用使用常规 NavHostFragment 相同。

自定义进度 fragment

您可以通过将 app:progressDestination 属性设置为您要用于处理安装进度的目标的 ID,从而为每个导航图替换进度 fragment 实现。您的自定义进度目标应该是一个 Fragment,该 Fragment 派生自 AbstractProgressFragment。您必须重写用于通知安装进度、错误和其他事件的抽象方法。然后,您可以在您选择的 UI 中显示安装进度。

默认实现的 DefaultProgressFragment 类使用此 API 来显示安装进度。

监控请求状态

Dynamic Navigator 库可让您实现与 按需交付的 UX 最佳实践中类似的 UX 流程,在该流程中,用户在等待安装完成时仍停留在上一个屏幕的上下文中。这意味着您根本不需要显示中间 UI 或进度 fragment。

screen that shows a bottom nav bar with an icon that indicates
         that a feature module is downloading
图 2. 显示底部导航栏下载进度的屏幕。

在这种情况下,您负责监控和处理所有安装状态、进度更改、错误等。

要启动此非阻塞导航流程,请将包含 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 对象会发出 SplitInstallSessionState 来自 Play Core 库的更新。这些更新包含安装进度事件,您可以使用它们来更新 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 库支持包含功能模块中定义的图。要包含功能模块中定义的图,请执行以下操作:

  1. 使用 <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" />
    
  2. <include-dynamic ... /> 中,您必须指定以下属性:

    • app:graphResName:导航图资源文件的名称。该名称派生自图的文件名。例如,如果图位于 res/navigation/nav_graph.xml,则资源名称为 nav_graph
    • android:id - 图目标 ID。Dynamic Navigator 库会忽略包含的图的根元素中找到的任何 android:id 值。
    • app:moduleName:模块的软件包名称。

使用正确的 graphPackage

正确获取 app:graphPackage 很重要,否则 Navigation 组件将无法包含功能模块中指定的 navGraph

动态功能模块的软件包名称是通过将模块名称附加到基本应用模块的 applicationId 中构建的。因此,如果基本应用模块的 applicationIdcom.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 导航图的 startDestination。动态模块负责其自身的导航图,基本应用对此一无所知。

include-dynamic 机制使基本应用模块能够包含动态模块中定义的 嵌套导航图。此嵌套导航图的行为与任何嵌套导航图相同。根导航图(即嵌套图的父级)只能将嵌套导航图本身定义为目标,而不能定义其子级。因此,当 include-dynamic 导航图是目标时,将使用 startDestination

限制

  • 动态包含的图目前不支持深层链接。
  • 动态加载的嵌套图(即带有 app:moduleName 元素的 <navigation>)目前不支持深层链接。