导航组件提供一种基于 Kotlin 的领域特定语言 (DSL),该语言依赖于 Kotlin 的类型安全构建器。借助该 API,您可以在 Kotlin 代码中(而不是在 XML 资源内部)以声明方式构建图表。如果您希望为应用动态构建导航,该方法会非常有用。例如,您的应用可以从外部 Web 服务下载并缓存导航配置,然后使用该配置在 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 应用的基本示例。在该示例中,我们有两个目的地:home
和 plant_detail
。用户首次启动应用时,系统会显示 home
目的地。该目标地会显示用户花园中的植物列表。当用户选择其中一种植物时,应用会导航到 plant_detail
目的地。
图 1 显示了这些目的地以及 plant_detail
目的地所需的参数和应用用于从 home
导航到 plant_detail
的操作 to_plant_detail
。
![Sunflower 应用有两个目的地以及一个连接这两个目的地的操作。](https://developer.android.google.cn/static/images/guide/navigation/navigation-kotlin-dsl-1.png?authuser=9&hl=ca)
home
和 plant_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"
}
使用 NavGraphBuilder DSL 构建图表
定义常量后,即可构建导航图。
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
属性具有相同的效果。
浏览 Kotlin DSL 图
最后,您可以使用标准的 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 为 Fragment
、Activity
和 NavGraph
这三种类型的目的地提供了内置支持。每种类型都有专属的内嵌扩展函数,可用于构建和配置目的地。
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
,则可以推断出类型。如果同时指定了 defaultValue
和 type
,这些类型必须匹配。如需查看可用实参类型的完整列表,请参阅 NavType 参考文档。
提供自定义类型
某些类型(例如 ParcelableType
和 SerializableType
)不支持从路线或深层链接使用的字符串中解析值。这是因为它们在运行时不依赖反射。通过提供自定义 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 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>(nav_routes.plant_search) {
label = getString(R.string.plant_search_title)
argument(nav_arguments.search_parameters) {
type = SearchParametersType
defaultValue = SearchParameters("cactus", emptyList())
}
}
NavType
封装了每个字段的写入和读取,
这意味着,当您导航到NavType
目标,以确保格式匹配:
val params = SearchParameters("rose", listOf("available"))
val searchArgument = SearchParametersType.serializeAsValue(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 资源文件以生成 Directions
和 Arguments
类。
了解更多内容
如需了解如何为 Kotlin DSL 和 Navigation Compose 代码提供类型安全,请参阅 Navigation 中的类型安全页面。