张可的博客

[KRouter] 一个简单轻量的 Kotlin 路由框架

KRouter(Kotlin-Router) 是一个非常轻量级的 Kotlin 路由框架

具体而言,KRouter 是一个通过 URI 发现接口实现类的框架。就像这样:

val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")

起因是段时间用 Voyager 时发现模块间的互相通信没这么灵活,需要一些配置,以及 DeepLink 的使用也有点奇怪,相比较而言我更希望能用路由的方式来实现模块间通信,于是就有了这个库。

https://github.com/0xZhangKe/KRouter

主要通过 KSP、ServiceLoader 以及反射实现。

使用

上面的那行代码几乎就是全部的使用方式了。

正如上面说的,这个是用来发现接口实现类并且通过 URI 匹配目的地的库,那么我们需要先定义一个接口。

interface Screen

然后我们的项目中与很多各自独立的模块,他们都会实现这个接口,并且每个都有所不同,我们需要通过他们各自的路由(即 URI )来进行区分。

// HomeModule
@Destination("screen/home")
class HomeScreen(@Router val router: String = "") : Screen

// ProfileModule
@Destination("screen/profile")
class ProfileScreen : Screen {
    @Router
    lateinit var router: String
}

现在我们的两个独立的模块都有了各自的 Screen 了,并且他们都有自己的路由地址。

val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
val profileScreen = KRouter.route<Screen>("screen/profile?name=zhangke")

现在就可以通过 KRouter 拿到这两个对象了,并且这两个对象中的 router 属性会被赋值为具体调用 KRouter.route 时的路由。这样你就可以在 HomeScreen 以及 ProfileScreen 拿到通过 uri 传的参数了,然后可以使用这些参数做一些初始化之类的操作。

@Destination

Destination 注解用于注解一个目的地,它包含两个参数:

然后还有个很重要的点,Destination 注解的类,也就是目的地类,必须包含一个无参构造器,否则 ServiceLoader 无法创建对象,对于 Kotlin 类来说,需要保证构造器中的每个入参都有默认值。

@Router

Router 注解用于表示目的地类中的那个属性是用来接受传入的 router 参数的,该属性必须是 String 类型。

标记了该注解的属性会被自动赋值,也可以不设置改注解。

举例来说,上面的例子中的 HomeScreen 对象被创建完成后,其 router 字段的值为 screen/home?name=zhangke

特别注意,如果 @Router 注解的属性不在构造器中,那么需要设置为可修改的,即 Kotlin 中的 var 修饰的变量属性。

KRouter

KRouter 是个单例类,其中只有一个方法。

inline fun <reified T : Any> route(router: String): T?

包含一个范形以及一个路由地址,路由地址可以包含 query 也可以不包含,匹配目的地时会忽略 query 字段。

匹配成功后会通过这个 uri 构建对象,并将 uri 传递给改对象中的 @router 注解标注的字段。

集成

首先需要在项目中集成 KSP

然后添加依赖:

// module's build.gradle.kts
implementation("com.github.0xZhangKe.KRouter:core:0.1.5")
ksp("com.github.0xZhangKe.KRouter:compiler:0.1.5")

因为是使用了 ServiceLoader ,所以还需要设置 SourceSet。

// module's build.gradle.kts
kotlin {
    sourceSets.main {
        resources.srcDir("build/generated/ksp/main/resources")
    }
}

或许你还需要添加 JitPack 仓库:

maven { setUrl("https://jitpack.io") }

原理

正如上面所说,本框架主要使用 ServiceLoader + KSP + 反射实现。

框架主要包含两部分,一是编译阶段的部分,二是运行时部分。

KSP 插件

KSP 插件相关的代码在 compiler 模块。

KSP 插件的主要作用是根据 Destination 注解生成 ServiceLoader 的 services 文件

KSP 的其他代码基本都差不多,主要就是先配置 services 文件,然后根据注解获取到类,然后通过 Visitor 遍历处理,我们直接看 KRouterVisitor 即可。

override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
    val superTypeName = findSuperType(classDeclaration)
    writeService(superTypeName, classDeclaration)
}

visitClassDeclaration 方法主要做两件事情,第一是获取父类,第二是写入或创建 services 文件。

流程就是先获取 type 指定的父类,没有就判断只有一个父类就直接返回,否则抛异常。

// find super-type by type parameter
val routerAnnotation = classDeclaration.requireAnnotation<Destination>()
val typeFromAnnotation = routerAnnotation.findArgumentTypeByName("type")
        ?.takeIf { it != badTypeName }

// find single-type
if (classDeclaration.superTypes.isSingleElement()) {
    val superTypeName = classDeclaration.superTypes
        .iterator()
        .next()
        .typeQualifiedName
        ?.takeIf { it != badSuperTypeName }
    if (!superTypeName.isNullOrEmpty()) {
        return superTypeName
    }
}

获取到之后我们需要按照 ServiceLoader 的要求将接口或抽象类的权限定名作为文件名创建一个文件。

然后再将实现类的权限定名写入该文件。

val resourceFileName = ServicesFiles.getPath(superTypeName)
val serviceClassFullName = serviceClassDeclaration.qualifiedName!!.asString()
val existsFile = environment.codeGenerator
    .generatedFile
    .firstOrNull { generatedFile ->
        generatedFile.canonicalPath.endsWith(resourceFileName)
    }
if (existsFile != null) {
    val services = existsFile.inputStream().use { ServicesFiles.readServiceFile(it) }
    services.add(serviceClassFullName)
    existsFile.outputStream().use { ServicesFiles.writeServiceFile(services, it) }
} else {
    environment.codeGenerator.createNewFile(
        dependencies = Dependencies(aggregating = false, serviceClassDeclaration.containingFile!!),
        packageName = "",
        fileName = resourceFileName,
        extensionName = "",
    ).use {
        ServicesFiles.writeServiceFile(setOf(serviceClassFullName), it)
    }
}

这样就自动生成了 ServiceLoader 所需要的 services 文件了。

KRouter

KRouter 主要做三件事情:

第一件事情很简单:

inline fun <reified T> findServices(): List<T> {
    val clazz = T::class.java
    return ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}

获取到之后就可以通过 URL 来开始匹配。

匹配方式就是获取每个目的地类的 Destination 注解中的 router 字段,然后与路由进行对比。

fun findServiceByRouter(
    serviceClassList: List<Any>,
    router: String,
): Any? {
    val routerUri = URI.create(router).baseUri
    val service = serviceClassList.firstOrNull {
        val serviceRouter = getRouterFromClassAnnotation(it::class)
        if (serviceRouter.isNullOrEmpty().not()) {
            val serviceUri = URI.create(serviceRouter!!).baseUri
            serviceUri == routerUri
        } else {
            false
        }
    }
    return service
}

private fun getRouterFromClassAnnotation(targetClass: KClass<*>): String? {
    val routerAnnotation = targetClass.findAnnotation<Destination>() ?: return null
    return routerAnnotation.router
}

因为匹配策略是忽略 query 字段,所以只通过 baseUri 匹配即可。

下面就是创建对象,这里有两种情况需要考虑。

第一是 @Router 注解在构造器中,这种情况需要重新使用构造器创建对象。

第二种是 @Router 注解在普通属性中,此时直接使用 ServiceLoader 创建好的对象然后赋值即可。

如果在构造器中,先获取 routerParameter 参数,然后通过 PrimaryConstructor 重新创建对象即可。

private fun fillRouterByConstructor(router: String, serviceClass: KClass<*>): Any? {
    val primaryConstructor = serviceClass.primaryConstructor
        ?: throw IllegalArgumentException("KRouter Destination class must have a Primary-Constructor!")
    val routerParameter = primaryConstructor.parameters.firstOrNull { parameter ->
        parameter.findAnnotation<Router>() != null
    } ?: return null
    if (routerParameter.type != stringKType) errorRouterParameterType(routerParameter)
    return primaryConstructor.callBy(mapOf(routerParameter to router))
}

如果是普通的变量属性,那么先获取到这个属性,然后做一些类型权限之类的校验,然后调用 setter 赋值即可。

private fun fillRouterByProperty(
    router: String,
    service: Any,
    serviceClass: KClass<*>,
): Any? {
    val routerProperty = serviceClass.findRouterProperty() ?: return null
    fillRouterToServiceProperty(
        router = router,
        service = service,
        property = routerProperty,
    )
    return service
}

private fun KClass<*>.findRouterProperty(): KProperty<*>? {
    return declaredMemberProperties.firstOrNull { property ->
        val isRouterProperty = property.findAnnotation<Router>() != null
        isRouterProperty
    }
}

private fun fillRouterToServiceProperty(
    router: String,
    service: Any,
    property: KProperty<*>,
) {
    if (property !is KMutableProperty<*>) throw IllegalArgumentException("@Router property must be non-final!")
    if (property.visibility != KVisibility.PUBLIC) throw IllegalArgumentException("@Router property must be public!")
    val setter = property.setter
    val propertyType = setter.parameters[1]
    if (propertyType.type != stringKType) errorRouterParameterType(propertyType)
    property.setter.call(service, router)
}

OK,以上就是关于 KRouter 的所有内容了。