KRouter 1.0 发布,支持参数注入以及 KMP 跨平台

741 阅读4分钟

因为时间关系,KRouter 第一版本写的比较粗糙,只能说勉强能用,最近空了点重新设计了一下并且完善了使用方式,支持了 Kotlin 跨平台以及参数注入,实现方案也从 ServiceLoader 彻底替换成通过 KSP 收集路由信息,并且发布到了 maven 中央仓库。

github.com/0xZhangKe/K…

作为一个 Router 框架不仅要有收集路由的能力,还要有将收集到的路由提供给路由框架的能力,这也是初版我用 ServiceLoader 的原因,所以这次新版本开发我刚开始打算用 KCP 修改字节码来实现这样的能力,结果研究了好几天发现还是不行,最终被卡在了 KCP 无法读写依赖模块的类文件问题上,目前我知道的是 KCP 只能读写当前模块的文件,依赖到的文件则不行,KSP 也是搞了一些骚操作做到的,并且相关文档也确实少得可怜,所以就放弃了,如果有人知道如何解决也可以告诉我下。

如何使用

使用方式仍然跟以前一样简单直接,首先给目标类添加注解:

@Destination("screen/main")
class MainScreen(
    @RouteParam("id") val id: String,
    @RouteParam("name") val name: String,
): Screen

然后使用 KRouter 获取对应的类:

val screen = KRouter.route<Screen>("screen/main?name=zhangke&id=123")

KRouter 目前提供了三个注解,分别是 @Destination@RouteUri@RouterParam.

@Destination

顾名思义,@Destination 注解表示路由的目标类,也就是目的地,接受一个参数作为路由地址。

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Destination(val route: String)

然后按照下面的方式使用:

@Destination("screen/profile/detail")
class ProfileDetailScreen: Screen

@RouterParam

@RouterParam 注解用于标识路由参数,被该注解标识的字段将会被自动注入参数。其中也包含一个参数表示被赋值字段对应路由中的 query 字段名,KRouter 会根据这个字段名去路由中解析出来并赋值。

@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class RouteParam(val name: String)

使用方式如下:

@Destination("screen/home/detail")
class HomeDetailScreen(
    @RouteParam("id") val id: String,
) : Screen {
}

除了构造器参数注入之外还支持属性字段注入。

@Destination("screen/home/detail")
class HomeDetailScreen(
    @RouteParam("id") val id: String,
) : Screen {

    @RouteParam("title") var title: String? = null
}

参数类型目前只支持基本类型和 String 类型,其他的暂不支持,对于希望传对象的需求可以转成 Json 字符串然后 encode 到路由中传递。

另外需要注意的是,由于 KSP 目前无法获取到参数以及属性的默认值,因此被注入的字段暂不支持设置默认值,这意味着如果你的被注入字段包含一个默认值,并且路由中不包含这个参数的话,那这个默认值将会失效。

@RouteUri

使用该注解注入的参数或属性将会被赋值为完整的路由,由于 @RouterParam 注解只能用来注入字段,对于一些解析过程复杂,参数类型复杂的场景,可以使用 @RouteUri 获取完整的路由。

KRouterModule

KRouterModule 是一个接口,用来实现具体的路由能力,可以通过 KRouter 类动态添加自定义的 Module,默认会先使用动态添加的 Module 进行路由,路由失败则使用 KRouter 内部的路由继续。

interface KRouterModule {
   fun route(uri: String): Any?
}

添加依赖

KRouter 提供了两个 KSP 插件。

  • krouter-collecting-compiler:用于收集路由信息,非主模块使用。
  • krouter-reducing-compiler:用于汇总各个模块路由信息,只有主工程模块(app模块)需要使用。

如下:

// 非主模块使用
ksp("io.github.0xzhangke:krouter-collecting-compiler:latest_version")
// 主模块使用
ksp("io.github.0xzhangke:krouter-reducing-compiler:latest_version")

另外还提供了一个注解模块和一个运行时模块。

  • krouter-runtime:运行时模块,提供了 KRouterKRouterModule 以及注解。
  • krouter-annotation:注解模块,只包含注解部分。

如下:

// 仅需要使用注解的模块使用
implementation("io.github.0xzhangke:krouter-runtime:latest_version")
// 需要使用路由能力的模块使用
implementation("io.github.0xzhangke:krouter-annotation:latest_version")

实现方案

首先,collecting-compiler 会收集所在模块所有的路由目标类信息,并生成一个所属于当前模块的KRouterModule ,生成的类大概如下:

public class RouterCollection_1726153189290() : KRouterModule {
    override fun route(uri: String): Any? {
        val routerUri = com.zhangke.krouter.internal.KRouterUri.create(uri)
        return when (routerUri.baseUrl) {
            "screen/home/detail" -> {
                com.zhangke.krouter.sample.home.HomeDetailScreen(
                    id = routerUri.requireQuery("id"),
                )
            }
            "screen/home/landing" -> {
                com.zhangke.krouter.sample.home.HomeLandingScreen(
		    router = uri,
                )
            }
            else -> null
        }
    }
}

所有依赖了 collecting-compiler 插件的的模块都会生成一个这样的类。

然后在项目的主模块(一般是 app 模块)需要依赖 reducing-compiler 插件,这个插件的作用是用来生成一个固定包名和类名的类,并且找到所有 collecting-compiler 生成的类然后添加到该类中。

public class AutoReducingModule() : KRouterModule {

  private val moduleList: List<KRouterModule> = listOf<KRouterModule>(
          com.zhangke.krouter.generated.RouterCollection_1726153189283(),
          com.zhangke.krouter.generated.RouterCollection_1726153189290(),
          com.zhangke.krouter.generated.RouterCollection_1726153189284(),
          com.zhangke.krouter.generated.RouterCollection_1726153189709()
      )

  override fun route(uri: String): Any? = moduleList.firstNotNullOfOrNull { it.route(uri) }
}

KRouter 在运行时会通过反射创建这个类,然后路由过程委托给该类实现,这样就完成了整个路由的收集和实现过程。