【KRouter】一个简单且轻量级的Kotlin Routing框架
KRouter(Kotlin-Router)是一个简单而轻量级的Kotlin路由框架。
具体来说,KRouter是一个通过URI来发现接口实现类的框架。它的使用方式如下:
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
之所以这样做,是因为在使用Voyager一段时间后,我发现模块之间的通信不够灵活,需要一些配置,而且使用DeepLink有点奇怪,所以我更喜欢使用路由来实现模块之间的通信,于是我开发了这个库。
这个库主要通过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
}
现在我们有两个独立的模块,它们各自拥有自己的屏幕(Screens),并且它们都有自己的路由地址。
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
val profileScreen = KRouter.route<Screen>("screen/profile?name=zhangke")
现在,您可以通过KRouter获取这两个对象,并且这些对象中的路由属性将被分配给对KRouter.route
的特定调用的路由。
现在,您可以在HomeScreen
和ProfileScreen
中获取通过URI传递的参数,并且可以使用这些参数进行一些初始化和其他操作。
@Destination
@Destination
注解用于标记目的地(Destination),包含两个参数:
route
:目的地的唯一标识路由地址,必须是 URI 类型的字符串,不需要包含查询参数。type
:目的地的接口。如果类只有一个父类或接口,您无需设置此参数,它可以自动推断。但如果类有多个父类或接口,您需要通过 type 参数明确指定。
需要特别注意的是,被 @Destination
注解标记的类必须包含一个无参数构造函数,否则 ServiceLoader
无法创建对象。对于 Kotlin 类,您还需要确保构造函数的每个输入参数都具有默认值。
@Router
@Router
注解用于指定目的地类的哪个属性用于接收传入的路由参数,该属性必须是字符串类型。
使用此注解标记的属性将自动分配一个值,或者您可以不设置注解。例如,在上述示例中,当创建 HomeScreen
对象时,其 router 字段的值将自动设置为 screen/home?name=zhangke
。
特别要注意,如果被@Router
注解的属性不在构造函数中,那么该属性必须声明为可修改的,即在 Kotlin 中应为 var 修饰的可变属性。
KRouter 是一个 Kotlin Object 类,它只包含一个函数:
inline fun <reified T : Any> route(router: String): T?
此函数接受一个泛型类型和一个路由地址。路由地址可以包含或不包含查询参数,但在匹配目的地时,查询参数将被忽略。匹配成功后,将使用此 URI 构造对象,并将 URI 传递给目标对象中的 @router
注解字段。
集成
首先,您需要在项目中集成 KSP。
https://kotlinlang.org/docs/ksp-overview.html
然后,添加以下依赖项:
// 模块的 build.gradle.kts
implementation("com.github.0xZhangKe.KRouter:core:0.1.5")
ksp("com.github.0xZhangKe.KRouter:compiler:0.1.5")
由于使用了 ServiceLoader,您还需要设置 SourceSet。
// 模块的 build.gradle.kts
kotlin {
sourceSets.main {
resources.srcDir("build/generated/ksp/main/resources")
}
}
可能还需要添加 JitPack 仓库:
maven { setUrl("https://jitpack.io") }
工作原理
正如前面所提到的,KRouter 主要通过 ServiceLoader + KSP + 反射来实现。
这个框架由两个主要部分组成:编译阶段和运行时阶段。
KSP 插件
与 KSP 插件相关的代码位于编译器模块中。
KSP 插件的主要任务是根据 Destination
注解生成 ServiceLoader
的服务文件。
KSP 代码的其余部分基本相同,主要工作包括首先配置服务文件,然后根据注解获取类,最后通过 Visitor 进行迭代。您可以直接查看 KRouterVisitor 来了解更多细节。
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val superTypeName = findSuperType(classDeclaration)
writeService(superTypeName, classDeclaration)
}
visitClassDeclaration
方法主要有两个主要功能,第一是获取父类,第二是编写或创建服务文件。
流程首先是获取指定类型的父类,如果没有父类,且只有一个父类时,可以直接返回,否则会引发异常。
// 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)
}
}
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注解中的路由字段,然后将其与路由进行比较。
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
}
匹配策略是忽略查询字段,只需通过baseUri进行匹配即可。
接下来的步骤是创建对象。有两种情况需要考虑:
第一种情况是@Router
注解位于构造函数中,在这种情况下,需要再次使用构造函数创建对象。
第二种情况是@Router
注解位于普通属性中。在这种情况下,可以直接使用ServiceLoader
创建的对象,然后将值分配给它。
如果@Router
注解位于构造函数中,您可以首先获取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)
}
上面是关于KRouter的全部内容,希望对你有所帮助!
GitHub
https://github.com/0xZhangKe/KRouter