片段和 Kotlin DSL

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

依赖项

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

Groovy

dependencies {
    def nav_version = "2.8.4"

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

Kotlin

dependencies {
    val nav_version = "2.8.4"

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

构建图表

这是一个基于Sunflower 应用 的基本示例。在此示例中,我们有两个目标:homeplant_detailhome目标在用户首次启动应用时出现。此目标显示用户花园中植物的列表。当用户选择其中一种植物时,应用将导航到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,以及连接它们的 action。

托管 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文件夹中定义为资源,因此需要在 activity 的onCreate()过程中进行设置。

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

下一步是定义在定义图表时将使用的 route。

为您的图表创建 route

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

处理参数时,这些参数内置于 route 类型中。这允许您为导航参数提供类型安全性。

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

定义 route 后,您可以构建导航图。

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 的Fragment类。设置此项与在使用 XML 定义的 Fragment 目标上设置android:name属性的效果相同。

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

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

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

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

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

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

如果PlantDetailFragment正在使用ViewModel,请使用SavedStateHandle.toRoute获取 route 实例。

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

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

目标

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

Fragment 目标

fragment()DSL 函数可以使用 Fragment 的 UI 类和用于唯一标识此目标的 route 类型作为参数,后跟一个 lambda,您可以在其中提供使用您的 Kotlin DSL 图表导航部分中所述的其他配置。

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

Activity 目标

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

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

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

@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
@Parcelize
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 导航资源。因此,您不能依赖于在您的AndroidManifest.xml文件中放置<nav-graph>元素,而必须手动向您的 Activity 添加意图过滤器。您提供的意图过滤器应与应用深层链接的基路径、操作和 MIME 类型匹配。

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

您还可以使用deepLinkBuilder尾随 lambda 添加操作和 MIME 类型。

以下示例为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类。