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 的一切内容了。