Navigation 组件提供一种基于 Kotlin 的领域特定语言 (DSL),该语言依赖于 Kotlin 的类型安全构建器。借助该 API,您可以在 Kotlin 代码中(而不是在 XML 资源内部)以声明方式构建图表。如果您想动态构建应用的导航,这会非常有用。例如,您的应用可以从外部 Web 服务下载并缓存导航配置,然后使用该配置在 activity 的 onCreate() 函数中动态构建导航图。
依赖项
如需将 Kotlin DSL 与 fragment 搭配使用,请将以下依赖项添加到应用的 build.gradle 文件中:
Groovy
dependencies { def nav_version = "2.10.2" api "androidx.navigation:navigation-fragment-ktx:$nav_version" }
Kotlin
dependencies { val nav_version = "2.10.2" api("androidx.navigation:navigation-fragment-ktx:$nav_version") }
构建图表
下面是一个基于 Sunflower 应用的基本示例。在此示例中,我们有两个目的地:home 和 plant_detail。当用户首次启动应用时,home 目的地会显示。此目的地会显示用户花园中的植物列表。当用户选择其中一种植物时,应用会转到 plant_detail 目的地。
图 1 显示了这些目的地以及 plant_detail 目的地所需的参数和应用用于从 home 导航到 plant_detail 的操作 to_plant_detail。
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 与一个或多个实参绑定在一起。不过,在使用 Navigation DSL 时,路由可以包含参数作为自身的一部分。这意味着,在使用 DSL 时,不存在操作的概念。
下一步是指定您在定义图表时将会使用的路由。
为图表创建路线
基于 XML 的导航图会在 Android 构建流程中进行解析。系统会为图表中定义的每个 id 属性创建数字常量。在运行时构建导航图时,这些构建时生成的静态 ID 不可用,因此 Navigation DSL 会使用可序列化类型,而不是 ID。每条路线都由一个唯一类型表示。
处理实参时,这些实参会内置到路线类型中。这样,您就可以为导航参数提供类型安全。
@Serializable data object Home
@Serializable data class Plant(val id: String)
使用 NavGraphBuilder DSL 构建图表
定义路线后,即可构建导航图。
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 目的地。此函数需要两个类型参数。
首先,一个 Fragment 类,用于为此目的地提供界面。设置此属性与在使用 XML 定义的 fragment 目的地上设置 android:name 属性具有相同的效果。
其次是路线。此类型必须是可序列化的类型,且扩展自 Any。它应包含此目的地将使用的所有导航参数及其类型。
该函数还接受用于其他配置的可选 lambda(如目的地标签)以及用于自定义实参和深层链接的嵌入式构建器函数。
浏览 Kotlin DSL 图
最后,您可以使用 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 为 Fragment、Activity 和 NavGraph 这三种类型的目的地提供了内置支持。每种类型都有专属的内嵌扩展函数,可用于构建和配置目的地。
fragment 目的地
fragment() DSL 函数可以使用界面的 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 将导航图绑定到界面组件时,此标签会非常有用。
@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 类的定义方式相同。必需实参定义为不可为 null 的类型,可选实参则定义了默认值。
用于表示路由及其参数的基础机制是基于字符串的。使用字符串对路线进行建模,可在配置更改和系统发起的进程终止期间,从磁盘存储和恢复导航状态。因此,每个导航实参都需要可序列化,也就是说,它应该有一个方法,可将实参值的内存中表示形式转换为 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 导航资源。因此,您不能依赖于在 AndroidManifest.xml 文件中放置 <nav-graph> 元素,而是必须向 activity 手动添加 intent 过滤器。您提供的 intent 过滤器应与应用深层链接的基本路径、操作和 MIME 类型匹配。
通过在目的地的 lambda 中调用 deepLink 函数,可将深层链接添加到目的地。它接受作为形参化类型的路线,以及用于深层链接的网址基本路径的形参 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 资源文件以生成 Directions 和 Arguments 类。