导航与路由
导航是 UI 应用程序的关键组成部分,它允许用户在不同应用程序屏幕之间移动。 Compose Multiplatform 采用了 Jetpack Compose 的导航方法。
导航库目前处于 Beta 阶段。 欢迎您在 Compose Multiplatform 项目中试用。 我们非常感谢您在 YouTrack 中提供反馈。
设置
要使用 Navigation 库,请将以下依赖项添加到您的 commonMain
源代码集:
kotlin {
// ...
sourceSets {
// ...
commonMain.dependencies {
// ...
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta05")
}
// ...
}
}
Compose Multiplatform 1.8.2 需要 Navigation 库版本 2.9.0-beta05。
示例项目
要查看 Compose Multiplatform 导航库的实际应用,请查看 nav_cupcake 项目, 该项目由 使用 Compose 在屏幕之间导航 Android codelab 转换而来。
与 Jetpack Compose 一样,要实现导航,您应该:
- 列出 应包含在导航图中的路由。每个路由都必须是定义路径的唯一字符串。
- 创建 一个
NavHostController
实例作为您的主要可组合属性来管理导航。 - 将
NavHost
可组合项添加 到您的应用:- 从您之前定义的路由列表中选择起始目标。
- 直接创建导航图(作为创建
NavHost
的一部分),或者使用NavController.createGraph()
函数以编程方式创建。
每个返回栈条目(图中包含的每个导航路由)都实现了 LifecycleOwner
接口。 应用不同屏幕之间的切换会使其状态从 RESUMED
变为 STARTED
,然后再变回。 RESUMED
也被称为“稳定状态”:当新屏幕准备就绪并激活时,导航被认为是完成的。 关于 Compose Multiplatform 中当前实现的详细信息,请参见 生命周期 页面。
Web 应用中的浏览器导航支持
面向 Web 的 Compose Multiplatform 完全支持通用的 Navigation 库 API, 并且在此基础上允许您的应用从浏览器接收导航输入。 用户可以使用浏览器中的“返回”和“前进”按钮在浏览器历史记录中反映的导航路由之间移动, 也可以使用地址栏来了解当前位置并直接前往某个目标。
要将 Web 应用绑定到公共代码中定义的导航图, 您可以在 Kotlin/Wasm 代码中使用 window.bindToNavigation()
方法。 您可以在 Kotlin/JS 中使用相同的方法,但要将其包装在 onWasmReady {}
代码块中,以确保 Wasm 应用程序已初始化并且 Skia 已准备好渲染图形。 以下是设置示例:
//commonMain source set
@Composable
fun App(
onNavHostReady: suspend (NavController) -> Unit = {}
) {
val navController = rememberNavController()
NavHost(...) {
//...
}
LaunchedEffect(navController) {
onNavHostReady(navController)
}
}
//wasmJsMain source set
@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalBrowserHistoryApi
fun main() {
val body = document.body ?: return
ComposeViewport(body) {
App(
onNavHostReady = { window.bindToNavigation(it) }
)
}
}
//jsMain source set
@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalBrowserHistoryApi
fun main() {
onWasmReady {
val body = document.body ?: return@onWasmReady
ComposeViewport(body) {
App(
onNavHostReady = { window.bindToNavigation(it) }
)
}
}
}
调用 window.bindToNavigation(navController)
后:
- 浏览器中显示的 URL 会反映当前路由(在 URL 片段中,
#
字符之后)。 - 应用会解析手动输入的 URL,将其转换为应用内的目标。
默认情况下,当使用类型安全导航时,目标会根据 kotlinx.serialization
默认值 转换为 URL 片段,并附加实参: <应用包>.<可序列化类型>/<实参1>/<实参2>
。 例如,example.org#org.example.app.StartScreen/123/Alice%2520Smith
。
自定义路由与 URL 之间的转换
由于 Compose Multiplatform 应用是单页应用,框架会操纵地址栏以模仿常规的 Web 导航。 如果您希望使 URL 更具可读性,并将实现与 URL 模式分离, 您可以直接为屏幕分配名称,或者为目标路由开发完全自定义的处理方式:
为了简单地使 URL 可读,请使用
@SerialName
注解显式设置可序列化对象或类的序列名称:kotlin// 此路由不会使用应用包名和对象名, // 而是直接转换为 URL,例如 “#start” @Serializable @SerialName("start") data object StartScreen
要完全构建每个 URL,您可以使用可选的
getBackStackEntryRoute
lambda。
完整 URL 自定义
要实现完全自定义的路由到 URL 转换:
- 将可选的
getBackStackEntryRoute
lambda 传递给window.bindToNavigation()
函数 以指定在必要时应如何将路由转换为 URL 片段。 - 如有需要,添加代码来捕获地址栏中的 URL 片段(当有人点击或粘贴您的应用 URL 时) 并将 URL 转换为路由以相应地导航用户。
以下是一个简单的类型安全导航图示例,可与以下 Web 代码示例(commonMain/kotlin/org.example.app/App.kt
)一起使用:
// 导航图中路由实参的可序列化对象和类
@Serializable data object StartScreen
@Serializable data class Id(val id: Long)
@Serializable data class Patient(val name: String, val age: Long)
@Composable
internal fun App(
onNavHostReady: suspend (NavController) -> Unit = {}
) = AppTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = StartScreen
) {
composable<StartScreen> {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Starting screen")
// 打开“Id”屏幕并带有合适参数的按钮
Button(onClick = { navController.navigate(Id(222)) }) {
Text("将 222 作为参数传递给 ID 屏幕")
}
// 打开“Patient”屏幕并带有合适参数的按钮
Button(onClick = { navController.navigate(Patient( "Jane Smith-Baker", 33)) }) {
Text("将 'Jane Smith-Baker' 和 33 传递给 Person 屏幕")
}
}
}
composable<Id> {...}
composable<Patient> {...}
}
LaunchedEffect(navController) {
onNavHostReady(navController)
}
}
在 wasmJsMain/kotlin/main.kt
中,将 lambda 添加到 .bindToNavigation()
调用:
@OptIn(
ExperimentalComposeUiApi::class,
ExperimentalBrowserHistoryApi::class,
ExperimentalSerializationApi::class
)
fun main() {
val body = document.body ?: return
ComposeViewport(body) {
App(
onNavHostReady = { navController ->
window.bindToNavigation(navController) { entry ->
val route = entry.destination.route.orEmpty()
when {
// 使用其序列描述符识别路由
route.startsWith(StartScreen.serializer().descriptor.serialName) -> {
// 将对应的 URL 片段设置为 “#start”
// 而不是 “#org.example.app.StartScreen”
//
// 此字符串必须始终以 `#` 字符开头,以将数据
// 保留在 URL 片段中
"#start"
}
route.startsWith(Id.serializer().descriptor.serialName) -> {
// 访问路由实参
val args = entry.toRoute<Id>()
// 将对应的 URL 片段设置为 “#find_id_222”
// 而不是 “#org.example.app.ID%2F222”
"#find_id_${args.id}"
}
route.startsWith(Patient.serializer().descriptor.serialName) -> {
val args = entry.toRoute<Patient>()
// 将对应的 URL 片段设置为 “#patient_Jane%20Smith-Baker_33”
// 而不是 “#org.company.app.Patient%2FJane%2520Smith-Baker%2F33”
"#patient_${args.name}_${args.age}"
}
// 不为所有其他路由设置 URL 片段
else -> ""
}
}
}
)
}
}
确保每个对应于路由的字符串都以
#
字符开头,以将数据保留在 URL 片段中。 否则,当用户复制和粘贴 URL 时,浏览器将尝试访问错误的端点,而不是将控制权传递给您的应用。
如果您的 URL 具有自定义格式,则应添加反向处理 以将手动输入的 URL 与目标路由进行匹配。 执行匹配的代码需要在 window.bindToNavigation()
调用将 window.location
绑定到导航图之前运行: