KRouter:基于 Decompose 的 KMP 声明式路由库

1 阅读4分钟

KRouter 在 Decompose 之上补齐声明式路由能力,通过 KSP 在编译期生成路由表,提供:

  • 零样板的路由注册(@KRoute 注解 + 自动生成)
  • 类型安全的路径跳转与 KBundle 参数传递
  • 页面 Result 回调(pushForResult / postResult)

适用于采用 Decompose 做导航的 KMP 项目。

image.png

页面栈、生命周期、状态恢复与返回手势由 Decompose 负责;KRouter 只负责「路径 + 参数 → 组件实例」的映射与导航 API。


一、为什么需要 KRouter

导航方案对比

方案问题
Compose NavigationKMP 支持有限(部分平台缺少 BackHandler 与预测性返回手势)
Decompose没有路径路由,「路径 → 组件」映射、参数传递、结果回调均需自建
KRouter在 Decompose 上补齐声明式路由

Compose Multiplatform 官方文档将 Decompose 列为支持完整生命周期与依赖注入的高级导航方案;而 Decompose 本身不提供路由层,因此使用时仍需自行解决:

  • 路由表手写:每增一页就要手动登记路径与组件的对应关系,漏登记则跳转失败。
  • 参数传递无统一抽象:要么构造函数传大量参数,要么自维护参数协议,类型安全与可维护性一般。
  • 缺少页面结果约定:如从设置页返回并带回数据,需自建回调或事件机制。

KRouter 补全上述三点。


二、核心设计

KSP 路由生成

页面只需一行注解:

@KRoute("/home")
class HomeComponent(ctx: ComponentContext) : KRouterComponent(ctx)

KSP 在编译期扫描所有 @KRoute 类,生成完整的注册文件:

object GeneratedRouteTable {
    fun register() {
        KRouter.registerRoute("/home") { ctx -> HomeComponent(ctx) }
        KRouter.registerRoute("/login") { ctx -> LoginComponent(ctx) }
        KRouter.registerRoute("/register") { ctx -> RegisterComponent(ctx) }
        ...
    }
}

运行时在根组件 init 中调用一次 GeneratedRouteTable.register() 即可,无需手写路由表,无反射。

KBundle 参数序列化

KBundle 是 KRouter 的参数容器,支持基础类型与可序列化对象:

操作写入读取
字符串putString("key", value)getString("key")
整型putInt("key", value)getInt("key")
长整型putLong("key", value)getLong("key")
对象putObject("key", value)getObject<T>("key")

对象类型须标注 @Serializable

@Serializable
data class User(val id: String, val name: String)
​
// 跳转时写入
KRouter.push(RoutePath.HOME) {
    putObject("user", User("1", "Alice"))
}
​
// 目标页读取
val user = getBundle().getObject<User>("user")

KBundle 与 KRouteConfig 参与 Decompose 的栈序列化,进程被系统回收后重新进入时,参数可完整还原,无需额外处理。


三、快速开始

3.1 依赖配置

需 Kotlin 1.9+、Compose Multiplatform、Decompose 3.x、KSP。

在共享模块(如 shared)的 build.gradle.kts 中添加:

plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
    id("com.android.library")  // 有 Android 就加
    id("org.jetbrains.compose")
    id("com.google.devtools.ksp")
}
​
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                api("io.github.lx-0713:krouter:1.0.1")
              // Decompose 的 Compose 扩展:根组件用 Children 渲染子页、stackAnimation 做转场,需单独依赖
                api("com.arkivanov.decompose:extensions-compose:3.1.0")
            }
        }
    }
}
​
dependencies {
    add("kspCommonMainMetadata", "io.github.lx-0713:krouter-compiler:1.0.1")
}
​
// 使 KSP 生成代码对 commonMain 可见
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>().configureEach {
    if (name != "kspCommonMainKotlinMetadata") dependsOn("kspCommonMainKotlinMetadata")
}
kotlin.sourceSets.commonMain {
    kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
}

项目需配置 mavenCentral() 仓库。

3.2 路径常量

集中管理路径,便于维护与重构:

object RoutePath {
    const val LOGIN = "/login"
    const val HOME = "/home"
    const val DETAIL = "/detail"
    const val SETTINGS = "/settings"
}

3.3 页面组件

每个页面继承 KRouterComponent 并标注 @KRoute

// 无参数页面
@KRoute(RoutePath.SETTINGS)
class SettingsComponent(ctx: ComponentContext) : KRouterComponent(ctx) {
    @Composable
    override fun Content() {
        Column {
            Text("设置")
            Button(onClick = { KRouter.pop() }) { Text("返回") }
        }
    }
}
​
// 带基础类型参数
@KRoute(RoutePath.DETAIL)
class DetailComponent(ctx: ComponentContext) : KRouterComponent(ctx) {
    private val itemId: String = getBundle().getString("itemId")
​
    @Composable
    override fun Content() {
        Text("商品 ID: $itemId")
        Button(onClick = { KRouter.pop() }) { Text("返回") }
    }
}
​
// 带对象参数(须 @Serializable)
@Serializable
data class User(val username: String)
​
@KRoute(RoutePath.HOME)
class HomeComponent(ctx: ComponentContext) : KRouterComponent(ctx) {
    private val user: User = getBundle().getObject<User>("user") ?: User("未知")
​
    @Composable
    override fun Content() {
        Text("欢迎,${user.username}!")
    }
}

3.4 根组件

init 中注册路由表并创建栈;Content() 中用 Children 渲染栈顶并配置转场:

class RootComponent(componentContext: ComponentContext) : KRouterComponent(componentContext) {
    val childStack: Value<ChildStack<KRouteConfig, KRouterComponent>>
​
    init {
        GeneratedRouteTable.register()
        childStack = KRouter.createChildStack(this, RoutePath.LOGIN)
    }
​
    @Composable
    override fun Content() {
        MaterialTheme {
            Children(
                stack = childStack,
                animation = stackAnimation(slide()),
                onBack = { KRouter.popImmediate() }
            ) {
                Surface(modifier = Modifier.fillMaxSize()) {
                    it.instance.Content()
                }
            }
        }
    }
}

四、导航 API

// 跳转新页面
KRouter.push(RoutePath.DETAIL) { putString("itemId", "123") }
​
// 跳转,栈中已有该路径则不重复压栈
KRouter.pushNew(RoutePath.SETTINGS)
​
// 替换当前页
KRouter.replaceCurrent(RoutePath.HOME) { putString("from", "detail") }
​
// 清空栈并跳转(登录成功、退出登录等场景)
KRouter.replaceAll(RoutePath.HOME) { putObject("user", user) }
​
// 返回
KRouter.pop()
KRouter.popImmediate()   // 响应系统返回键/手势时使用
KRouter.popTo(0)         // 退到栈中指定层(0 为栈底)// 将某路径的页面提到栈顶,不存在则新建
KRouter.bringToFront(RoutePath.HOME) { putString("refresh", "1") }

参数通过 KBundle 传递;传对象时类型须 @Serializable


五、Result 机制

发起页pushForResult 跳转,重写 onComponentResult 接收结果:

@KRoute(RoutePath.HOME)
class HomeComponent(ctx: ComponentContext) : KRouterComponent(ctx) {
    private var settingsResult: KBundle? by mutableStateOf(null)
​
    private fun goToSettings() = pushForResult(RoutePath.SETTINGS)
​
    override fun onComponentResult(bundle: KBundle) {
        settingsResult = bundle
    }
​
    @Composable
    override fun Content() {
        Column {
            settingsResult?.let { Text("设置页返回: ${it.getString("msg")}") }
            Button(onClick = { goToSettings() }) { Text("打开设置") }
        }
    }
}

目标页:返回前 postResult 写入数据,再 pop

Button(onClick = {
    KRouter.postResult { putString("msg", "用户修改了字体大小") }
    KRouter.pop()
}) {
    Text("携带数据返回")
}

结果接收方由 KRouter 内部维护,组件销毁时自动移除,不会产生误回调或泄漏。


六、小结

KRouter 在 Decompose 之上补全路由与参数、结果能力,适用于采用 Decompose 做导航的 KMP 项目。更多 API 与配置见 GitHub