导航与路由
导航是用户界面应用的核心功能,它使用户能在不同应用界面间切换。
Compose Multiplatform 采用 Jetpack Compose 的导航方案。
配置
在 commonMain
源集中添加以下依赖以使用导航库:
kotlin {
// ...
sourceSets {
// ...
commonMain.dependencies {
// ...
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10")
}
// ...
}
}
类型安全导航
自 1.7.0 版本起,Compose Multiplatform 支持通用代码中的类型安全导航,具体实现参考
Jetpack 文档。
限制
相比 Jetpack Compose,Compose Multiplatform 导航当前存在以下限制:
Web 应用中的浏览器导航支持
Compose Multiplatform 完整支持通用导航库 API,并允许 Web 应用响应浏览器导航操作。
用户可通过浏览器 前进/后退 按钮在记录于浏览历史的路由间跳转,也可直接通过地址栏导航。
在 Kotlin/Wasm 或 Kotlin/JS 代码中调用 window.bindToNavigation()
将通用代码定义的导航图与 Web 应用绑定:
@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalBrowserHistoryApi
fun main() {
val body = document.body ?: return
ComposeViewport(body) {
val navController = rememberNavController()
// 假设通用代码中的主可组合函数为 App()
App(navController)
LaunchedEffect(Unit) {
window.bindToNavigation(navController)
}
}
}
调用 window.bindToNavigation(navController)
后:
使用类型安全导航时,默认将目的地转换为 kotlinx.serialization
默认格式
并附加参数的 URL 片段:
<应用包名>.<可序列化类型>/<参数1>/<参数2>
例如: example.org#org.example.app.StartScreen/123/Alice%2520Smith
自定义路由与 URL 转换
Compose Multiplatform 应用是单页应用,框架通过操作地址栏模拟常规网页导航。
若需提升 URL 可读性或解耦实现与 URL 模式,可直接为界面命名或完全自定义路由处理逻辑:
使用 @SerialName
注解显式设置可序列化对象/类的名称以简化 URL:
// 此路由将转换为简洁的 "#start" 而非 "#org.example.app.StartScreen"
@Serializable @SerialName("start") data object StartScreen
通过 getBackStackEntryRoute
lambda 实现完全自定义的 URL 生成。
完整 URL 定制
实现完全自定义的路由转换:
向 window.bindToNavigation()
传递 getBackStackEntryRoute
lambda 以指定路由到 URL 片段的转换逻辑
添加代码捕获地址栏中的 URL 片段(当用户点击或粘贴应用 URL 时),并将其转换为路由
以下示例展示通用代码中的简单类型安全导航图(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(
navController: NavHostController = rememberNavController()
) = AppTheme {
NavHost(
navController = navController,
startDestination = StartScreen
) {
composable<StartScreen> {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("起始界面")
Button(onClick = { navController.navigate(Id(222)) }) {
Text("传递参数 222 到 ID 界面")
}
Button(onClick = { navController.navigate(Patient( "Jane Smith-Baker", 33)) }) {
Text("传递 'Jane Smith-Baker' 和 33 到人员界面")
}
}
}
composable<Id> {...}
composable<Patient> {...}
}
}
在 wasmJsMain/kotlin/main.kt
中为 .bindToNavigation()
添加 lambda:
@OptIn(
ExperimentalComposeUiApi::class,
ExperimentalBrowserHistoryApi::class,
ExperimentalSerializationApi::class
)
fun main() {
val body = document.body ?: return
ComposeViewport(body) {
val navController = rememberNavController()
App(navController)
LaunchedEffect(Unit) {
window.bindToNavigation(navController) { entry ->
val route = entry.destination.route.orEmpty()
when {
// 根据序列化描述符识别路由
route.startsWith(StartScreen.serializer().descriptor.serialName) -> {
// 设置 URL 片段为 "#start" 而非默认格式
"#start"
}
route.startsWith(Id.serializer().descriptor.serialName) -> {
val args = entry.toRoute<Id>()
// 设置为 "#find_id_222" 格式
"#find_id_${args.id}"
}
route.startsWith(Patient.serializer().descriptor.serialName) -> {
val args = entry.toRoute<Patient>()
// 设置为 "#patient_Jane%20Smith-Baker_33" 格式
"#patient_${args.name}_${args.age}"
}
else -> ""
}
}
}
}
}
若 URL 采用自定义格式,需在 window.bindToNavigation()
绑定前添加逆向处理逻辑:
@OptIn(
ExperimentalComposeUiApi::class,
ExperimentalBrowserHistoryApi::class,
ExperimentalSerializationApi::class
)
fun main() {
val body = document.body ?: return
ComposeViewport(body) {
val navController = rememberNavController()
App(navController)
LaunchedEffect(Unit) {
val initRoute = window.location.hash.substringAfter('#', "")
when {
initRoute.startsWith("start") -> {
navController.navigate(StartScreen)
}
initRoute.startsWith("find_id") -> {
val id = initRoute.substringAfter("find_id_").toLong()
navController.navigate(Id(id))
}
initRoute.startsWith("patient") -> {
val name = initRoute.substringAfter("patient_").substringBefore("_")
val id = initRoute.substringAfter("patient_").substringAfter("_").toLong()
navController.navigate(Patient(name, id))
}
}
window.bindToNavigation(navController) { ... }
}
}
}
第三方替代方案
若 Compose Multiplatform 导航组件无法满足需求,可考虑以下第三方方案:
23 四月 2025