Fragment 与 Kotlin DSL

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

依赖项

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

Groovy

dependencies {
    def nav_version = "2.9.0"

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

Kotlin

dependencies {
    val nav_version = "2.9.0"

    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,以及连接它们的操作。

托管 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 中,一个操作将一个目标 ID 与一个或多个参数关联起来。但是,当使用导航 DSL 时,路由可以包含参数作为路由的一部分。这意味着在使用 DSL 时没有操作的概念。

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

为您的图创建路由

基于 XML 的导航图 作为 Android 构建过程的一部分进行解析。图中定义的每个 id 属性都会创建一个数字常量。这些构建时生成的静态 ID 在运行时构建导航图时不可用,因此导航 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 的 Fragment 类。设置此项的效果与在使用 XML 定义的 Fragment 目标上设置 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 目标

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

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

Activity 目标

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

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>(
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
    label = getString(R.string.plant_search_title)
}

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

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 文件中,而必须手动将Intent 过滤器添加到您的 Activity 中。您提供的 Intent 过滤器应与您应用深层链接的基本路径、操作和 MIME 类型匹配。

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

您还可以使用 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 类。