片段和 Kotlin DSL

导航组件提供基于 Kotlin 的领域特定语言 (DSL),该语言依赖于 Kotlin 的 类型安全构建器。此 API 允许您在 Kotlin 代码中声明式地编写图形,而不是在 XML 资源中编写。如果您想要动态构建应用的导航,这将非常有用。例如,您的应用可以从外部 Web 服务下载和缓存导航配置,然后使用该配置在活动的 onCreate() 函数中动态构建导航图形。

依赖项

要将 Kotlin DSL 与 Fragment 一起使用,请将以下依赖项添加到应用的 build.gradle 文件中

Groovy

dependencies {
    def nav_version = "2.8.0"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.8.0"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

构建图形

以下是一个基于 Sunflower 应用 的基本示例。在此示例中,我们有两个目标:homeplant_detail。当用户首次启动应用时,home 目标存在。此目标显示来自用户花园的植物列表。当用户选择其中一种植物时,应用会导航到 plant_detail 目标。

图 1 显示了这些目标以及 plant_detail 目标所需的参数,以及应用用于从 home 导航到 plant_detail 的操作 to_plant_detail

The Sunflower app has two destinations along with an action that
            connects them.
图 1. Sunflower 应用有两个目标,homeplant_detail,以及一个连接它们的行动。

托管 Kotlin DSL 导航图形

在构建应用的导航图形之前,您需要一个地方来托管图形。此示例使用 Fragment,因此它在一个 NavHostFragment 中托管图形,该图形位于 FragmentContainerView

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

请注意,在本示例中未设置 app:navGraph 属性。该图未在 res/navigation 文件夹中定义为 资源,因此需要在活动的 onCreate() 过程中设置。

在 XML 中,操作将目标 ID 与一个或多个参数绑定在一起。但是,在使用 Navigation DSL 时,路由可以包含作为路由一部分的参数。这意味着在使用 DSL 时,没有操作的概念。

下一步是定义在定义图时将使用的路由。

为您的图创建路由

基于 XML 的导航图 在 Android 构建过程中被解析。为图中定义的每个 id 属性创建一个数字常量。这些在构建时生成的静态 ID 在运行时构建导航图时不可用,因此 Navigation DSL 使用 可序列化类型 而不是 ID。每个路由都由一个唯一的类型表示。

在处理参数时,这些参数 内置到路由类型中。这使您可以为导航参数提供类型安全。

@Serializable data object Home
@Serializable data class Plant(val id: String)

定义完路由后,就可以构建导航图了。

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

在本示例中,使用 fragment() DSL 构建器函数定义了两个片段目标。此函数需要两个 类型参数

首先,一个 Fragment 类,它为该目标提供 UI。设置此项的效果与在使用 XML 定义的片段目标上设置 android:name 属性相同。

其次,是路由。这必须是一种可序列化的类型,它扩展自 Any。它应该包含此目标将使用的任何导航参数及其类型。

该函数还接受一个可选的 lambda 函数,用于进行其他配置,例如目标标签,以及用于自定义参数和深层链接的嵌入式构建器函数。

最后,您可以使用 NavController.navigate() 调用从 home 导航到 plant_detail

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

PlantDetailFragment 中,您可以通过获取当前的 NavBackStackEntry 并调用其上的 toRoute 来获取路由实例,从而获取导航参数。

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

如果 PlantDetailFragment 使用 ViewModel,请使用 SavedStateHandle.toRoute 获取路由实例。

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

本指南的其余部分描述了常见的导航图元素、目标以及在构建图时如何使用它们。

目标

Kotlin DSL 为三种目标类型提供内置支持:FragmentActivityNavGraph 目标,每种目标都有自己的内联扩展函数可用于构建和配置目标。

片段目标

fragment() DSL 函数可以使用 UI 的片段类和用于唯一标识此目标的路由类型进行参数化,然后使用一个 lambda 函数,您可以在其中提供 使用 Kotlin DSL 图进行导航 部分中所述的额外配置。

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

活动目标

activity() DSL 函数接受路由的类型参数,但不参数化为任何实现的活动类。相反,您在尾随 lambda 中设置可选的 activityClass。这种灵活性使您可以为应该使用 隐式意图 启动的活动定义一个活动目标,在该目标中,明确的活动类将没有意义。与片段目标一样,您还可以配置标签、自定义参数和深层链接。

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

navigation() DSL 函数可用于构建 嵌套导航图。此函数接受一个类型参数,用于将路由分配给该图。它还接受两个参数:图的起始目标的路由,以及一个 lambda 函数,用于进一步配置该图。有效元素包括其他目标、自定义参数类型、深层链接和 目标的描述性标签。此标签对于使用 NavigationUI 将导航图绑定到 UI 组件非常有用。

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = Home) {
   // label, other destinations, deep links
}

支持自定义目标

如果您使用的是 新的目标类型,该类型不直接支持 Kotlin DSL,则可以使用 addDestination() 将这些目标添加到 Kotlin DSL 中。

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

或者,您也可以使用一元加运算符将新构建的目标直接添加到图中。

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

提供目标参数

目标参数可以定义为路由类的一部分。这些参数的定义方式与任何 Kotlin 类相同。必需的参数定义为不可为空的类型,可选的参数定义为具有默认值。

用于表示路由及其参数的底层机制是基于字符串的。使用字符串对路由进行建模可以使导航状态在 配置更改系统启动的进程死亡 期间存储和恢复到磁盘。出于这个原因,每个导航参数都需要可序列化,也就是说,它应该具有一个方法,将参数值的内存中表示形式转换为 String

Kotlin 序列化插件 在将 @Serializable 注释添加到对象时,会自动为 基本类型 生成序列化方法。

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

提供自定义类型

对于自定义参数类型,您需要提供一个自定义的 NavType 类。这使您可以完全控制如何从路由或深层链接中解析您的类型。

例如,用于定义搜索屏幕的路由可能包含一个表示搜索参数的类。

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@Serializable
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

自定义的 NavType 可以编写为

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

然后,它可以在您的 Kotlin DSL 中像任何其他类型一样使用。

fragment<SearchFragment, SearchRoute> {
    label = getString(R.string.plant_search_title)
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
}

导航到目标时,创建路由实例。

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

可以在目标中的路由中获取参数。

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters

深度链接

可以向任何目标添加深层链接,就像在 XML 驱动的导航图中一样。在使用 Kotlin DSL 创建深层链接时,所有在 为目标创建深层链接 中定义的相同过程都适用。

但是,在创建 隐式深层链接 时,您没有可以分析 <deepLink> 元素的 XML 导航资源。因此,您不能依赖于将 <nav-graph> 元素放置在您的 AndroidManifest.xml 文件中,而必须手动将 意图过滤器 添加到您的活动中。您提供的意图过滤器应与应用程序深层链接的基路径、操作和 mimetype 相匹配。

深层链接通过在目标的 lambda 函数内调用 deepLink 函数添加到目标中。它接受路由作为参数化类型,以及一个 basePath 参数,用于深层链接使用的 URL 的基路径。

您还可以使用 deepLinkBuilder 尾随 lambda 函数添加操作和 mimetype。

以下示例为 Home 目标创建深层链接 URI。

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

URI 格式

深层链接 URI 格式是根据以下规则自动从路由的字段生成的。

  • 必需的参数作为 *路径参数* 追加(示例:/{id})。
  • 具有默认值的参数(可选参数)作为 *查询参数* 追加(示例:?name={name})。
  • 集合作为 *查询参数* 追加(示例:?items={value1}&items={value2})。
  • 参数的顺序与路由中字段的顺序匹配。

例如,以下路由类型

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

生成的 URI 格式为

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

您可以添加的深层链接数量没有限制。每次调用 deepLink() 时,都会将一个新的深层链接追加到为该目标维护的列表中。

限制

Safe Args 插件与 Kotlin DSL 不兼容,因为该插件会查找 XML 资源文件以生成 DirectionsArguments 类。