使用 Kotlin DSL 程序化地构建图表

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

依赖项

如需使用 Kotlin DSL,请将以下依赖项添加到应用的 build.gradle 文件中:

Groovy

dependencies {
    def nav_version = "2.7.7"

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

Kotlin

dependencies {
    val nav_version = "2.7.7"

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

构建图表

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

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

Sunflower 应用有两个目的地以及一个连接这两个目的地的操作。
图 1. Sunflower 应用有两个目的地(homeplant_detail)以及一个连接这两个目的地的操作。

托管 Kotlin DSL 导航图

在构建应用的导航图之前,您需要有一个位置来托管该图表。此示例使用 fragment,因此它会将图表托管在 FragmentContainerView 内的 NavHostFragment 中:

<!-- 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。每个路线都由唯一的字符串表示,因此最好将这些路线定义为常量,以降低因拼写错误而导致的 bug 风险。

处理实参时,这些实参会内置到路线字符串中。将这个逻辑内置到路线中同样可以降低因拼写错误而导致的 bug 风险。

object nav_routes {
    const val home = "home"
    const val plant_detail = "plant_detail"
}

object nav_arguments {
    const val plant_id = "plant_id"
    const val plant_name = "plant_name"
}

定义常量后,即可构建导航图。

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = nav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

在该示例中,尾随 lambda 使用 fragment() DSL 构建器函数定义了两个 fragment 目的地。此函数需要从常量中获取的目的地路线字符串。该函数还接受用于其他配置的可选 lambda(如目的地标签)以及用于实参和深层链接的嵌入式构建器函数。

管理每个目的地界面的 Fragment 类将作为放在尖括号 (<>) 中的参数化类型传入。这与在使用 XML 定义的 fragment 目的地上设置 android:name 属性具有相同的效果。

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

private fun navigateToPlant(plantId: String) {
   findNavController().navigate("${nav_routes.plant_detail}/$plantId")
}

PlantDetailFragment 中,您可以获取实参的值,如以下示例所示:

val plantId: String? = arguments?.getString(nav_arguments.plant_id)

如需详细了解如何在导航时提供实参,请参阅提供目的地实参部分。

本指南的其余部分将介绍常见的导航图元素、目的地,以及如何在构建图表时使用它们。

目的地

Kotlin DSL 为 FragmentActivityNavGraph 这三种类型的目的地提供了内置支持。每种类型都有专属的内嵌扩展函数,可用于构建和配置目的地。

fragment 目的地

fragment() DSL 函数可以参数化为实现 fragment 类;它会使用分配给该目的地的唯一路线字符串,后跟您可以在其中提供其他配置的 lambda(如浏览 Kotlin DSL 图部分中所述)。

fragment<FragmentDestination>(nav_routes.route_name) {
   label = getString(R.string.fragment_title)
   // arguments, deepLinks
}

activity 目的地

activity() DSL 函数会使用分配给该目的地的唯一路线字符串,但不会将其参数化为任何实现 activity 类。相反,您可以在尾随 lambda 中设置可选 activityClass。这种灵活性可以让您为应该使用隐式 intent 启动的 activity 定义 activity 目的地,在这种情况下,显式 activity 类将毫无意义。与 fragment 目的地一样,您还可以配置标签、实参和深层链接。

activity(nav_routes.route_name) {
   label = getString(R.string.activity_title)
   // arguments, deepLinks...

   activityClass = ActivityDestination::class
}

navigation() DSL 函数可用于构建嵌套导航图。该函数使用三个实参:分配给图表的路线、图表的起始目的地路线,以及用于进一步配置图表的 lambda。有效元素包括其他目的地、实参、深层链接,以及目的地的描述性标签。在使用 NavigationUI 将导航图绑定到界面组件时,此标签会非常有用

navigation("route_to_this_graph", nav_routes.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
}

提供目的地实参

任何目的地都可以定义可选或必需实参。您可以使用 NavDestinationBuilder(这是针对所有目的地构建器类型的基类)上的 argument() 函数来定义操作。该函数会将实参的名称作为用于构造和配置 NavArgument 的字符串和 lambda。

在 lambda 中,您可以指定实参数据类型、默认值(如果适用),以及其是否可为 null。

fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
    label = getString(R.string.plant_details_title)
    argument(nav_arguments.plant_id) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_id)
        nullable = true  // default false
    }
}

如果指定了 defaultValue,则可以推断出类型。如果同时指定了 defaultValuetype,这些类型必须匹配。如需查看可用实参类型的完整列表,请参阅 NavType 参考文档。

提供自定义类型

某些类型(例如 ParcelableTypeSerializableType)不支持从路线或深层链接使用的字符串中解析值。这是因为它们在运行时不依赖反射。通过提供自定义 NavType 类,您可以准确控制从路线或深层链接中解析类型的方式。这样,您就可以使用 Kotlin 序列化或其他库对自定义类型进行无反射的编码和解码。

例如,用于表示传递给搜索屏幕的搜索形参的数据类可以同时实现 Serializable(提供编码/解码支持)和 Parcelize(支持保存到 Bundle 和从中恢复):

@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 parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

  // Only required when using Navigation 2.4.0-alpha07 and lower
  override val name = "SearchParameters"
}

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

fragment<SearchFragment>(nav_routes.plant_search) {
    label = getString(R.string.plant_search_title)
    argument(nav_arguments.search_parameters) {
        type = SearchParametersType
        defaultValue = SearchParameters("cactus", emptyList())
    }
}

此示例使用 Kotlin 序列化来解析字符串中的值,这意味着,当您导航到目的地时,也必须使用 Kotlin 序列化,以确保格式相符:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = Uri.encode(Json.encodeToString(params))
navController.navigate("${nav_routes.plant_search}/$searchArgument")

您可以通过目的地中的实参获取该形参:

val params: SearchParameters? = arguments?.getParcelable(nav_arguments.search_parameters)

深层链接

深层链接可以添加到任何目的地,就像使用 XML 驱动型导航图一样。为目的地创建深层链接中定义的所有相同程序也适用于使用 Kotlin DSL 创建显式深层链接的过程。

但是,在创建隐式深层链接时,您没有可以针对 <deepLink> 元素进行分析的 XML 导航资源。因此,您不能依赖于在 AndroidManifest.xml 文件中放置 <nav-graph> 元素,而是必须向 activity 手动添加 intent 过滤器。您提供的 intent 过滤器应与应用深层链接的基准网址格式、操作和 MIME 类型匹配。

您可以使用 deepLink() DSL 函数为每个单独的深层链接目的地提供更具体的 deeplink。此函数接受 NavDeepLink,其中包含表示 URI 模式的 String、表示 intent 操作的 String,以及表示 MIME 类型的 String

例如:

deepLink {
    uriPattern = "http://www.example.com/plants/"
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
}

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

下面给出了更复杂的隐式深层链接场景,该场景还定义了基于路径和基于查询的形参:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_routes.plant_detail) {
   label = getString(R.string.plant_details_title)
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}"
   })
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}?name={plant_name}"
   })
}

您可以使用字符串插值来简化定义。

限制

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

了解详情

如需了解如何为 Kotlin DSL 和 Navigation Compose 代码提供类型安全,请参阅 Navigation 中的类型安全页面。