[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 注解用于注解一个目的地,它包含两个参数:
route
: 目的地的唯一标识的路由地址,必须是个 URI 类型的String
,不需要包含 query。type
: 路由目的地的接口,如果这个类只有一个父类或接口的话是不用设置这个参数的,可以自动推断出来,但如果包含多个父类就需要通过type
显示指定了。
然后还有个很重要的点,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 主要做三件事情:
- 通过 ServiceLoader 获取接口所有的实现类。
- 通过 URI 匹配具体的目的地类。
- 通过 URI 构建目的地类对象。
第一件事情很简单:
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 的所有内容了。