KRouter(Kotlin-Router) 是一个非常轻量级的 Kotlin 路由结构。
详细而言,KRouter 是一个经过 URI 发现接口完成类的结构。就像这样:
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
起因是段时刻用 Voyager 时发现模块间的互相通讯没这么灵敏,需求一些装备,以及 DeepLink 的运用也有点古怪,相比较而言我更希望能用路由的方法来完成模块间通讯,于是就有了这个库。
github.com/0xZhangKe/K…
首要经过 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 的一切内容了。