Kotlin Multiplatform Development Help

导航与路由

导航是用户界面应用的核心功能,它使用户能在不同应用界面间切换。
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 文档

示例项目

查看 nav_cupcake 项目
(改编自 Android 教程 Navigate between screens with Compose
了解导航库的实际应用。

与 Jetpack Compose 类似,实现导航需完成以下步骤:

  1. 列出路由
    每个路由需为定义路径的唯一字符串。

  2. 创建 NavHostController 实例
    作为管理导航的主可组合项属性。

  3. 添加 NavHost 可组合项

    • 从预定义路由中选择初始目的地

    • 通过直接声明或调用 NavController.createGraph() 创建导航图

导航图中的每个返回栈条目(即每个路由)均实现 LifecycleOwner 接口。
界面切换时,其状态会在 RESUMED ("稳定"状态,表示导航完成)和 STARTED 间转换。
详见 生命周期 了解 Compose Multiplatform 的当前实现细节。

限制

相比 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) 后:

  • 浏览器地址栏显示当前路由(位于 # 后的 URL 片段)

  • 应用能解析手动输入的 URL 并跳转到对应界面

使用类型安全导航时,默认将目的地转换为 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 定制

实现完全自定义的路由转换:

  1. window.bindToNavigation() 传递 getBackStackEntryRoute lambda 以指定路由到 URL 片段的转换逻辑

  2. 添加代码捕获地址栏中的 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 导航组件无法满足需求,可考虑以下第三方方案:

名称

特点

Voyager

实用主义导航方案

Decompose

支持完整生命周期和依赖注入的高级导航方案

Appyx

支持手势控制的模型驱动导航

PreCompose

受 Jetpack Lifecycle/ViewModel/LiveData/Navigation 启发的导航和视图模型

Circuit

为 Kotlin 应用提供的 Compose 驱动架构,含导航和高级状态管理

23 四月 2025