【KRouter】一个简单且轻量级的Kotlin Routing框架

【KRouter】一个简单且轻量级的Kotlin Routing框架

KRouter(Kotlin-Router)是一个简单而轻量级的Kotlin路由框架。

具体来说,KRouter是一个通过URI来发现接口实现类的框架。它的使用方式如下:

kotlin 复制代码
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")

之所以这样做,是因为在使用Voyager一段时间后,我发现模块之间的通信不够灵活,需要一些配置,而且使用DeepLink有点奇怪,所以我更喜欢使用路由来实现模块之间的通信,于是我开发了这个库。

这个库主要通过KSP、ServiceLoader和反射来实现。

使用方法

上述代码基本上就是使用的全部内容。

如前所述,这是用于发现接口实现类并通过URI匹配目标的库,因此我们首先需要定义一个接口。

kotlin 复制代码
interface Screen

然后我们有一个包含许多独立模块的项目,这些模块实现了这个接口,每个模块都不同,我们需要通过它们各自的路由(即URI)来区分它们。

kt 复制代码
// HomeModule
@Destination("screen/home")
class HomeScreen(@Router val router: String = "") : Screen

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

现在我们有两个独立的模块,它们各自拥有自己的屏幕(Screens),并且它们都有自己的路由地址。

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

现在,您可以通过KRouter获取这两个对象,并且这些对象中的路由属性将被分配给对KRouter.route的特定调用的路由。

现在,您可以在HomeScreenProfileScreen中获取通过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 类,它只包含一个函数:

kotlin 复制代码
inline fun <reified T : Any> route(router: String): T?

此函数接受一个泛型类型和一个路由地址。路由地址可以包含或不包含查询参数,但在匹配目的地时,查询参数将被忽略。匹配成功后,将使用此 URI 构造对象,并将 URI 传递给目标对象中的 @router 注解字段。

集成

首先,您需要在项目中集成 KSP。

https://kotlinlang.org/docs/ksp-overview.html

然后,添加以下依赖项:

kotlin 复制代码
// 模块的 build.gradle.kts
implementation("com.github.0xZhangKe.KRouter:core:0.1.5")
ksp("com.github.0xZhangKe.KRouter:compiler:0.1.5")

由于使用了 ServiceLoader,您还需要设置 SourceSet。

kotlin 复制代码
// 模块的 build.gradle.kts
kotlin {
    sourceSets.main {
        resources.srcDir("build/generated/ksp/main/resources")
    }
}

可能还需要添加 JitPack 仓库:

kotlin 复制代码
maven { setUrl("https://jitpack.io") }

工作原理

正如前面所提到的,KRouter 主要通过 ServiceLoader + KSP + 反射来实现。

这个框架由两个主要部分组成:编译阶段和运行时阶段。

KSP 插件

与 KSP 插件相关的代码位于编译器模块中。

KSP 插件的主要任务是根据 Destination 注解生成 ServiceLoader 的服务文件。

KSP 代码的其余部分基本相同,主要工作包括首先配置服务文件,然后根据注解获取类,最后通过 Visitor 进行迭代。您可以直接查看 KRouterVisitor 来了解更多细节。

kt 复制代码
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
    val superTypeName = findSuperType(classDeclaration)
    writeService(superTypeName, classDeclaration)
}

visitClassDeclaration 方法主要有两个主要功能,第一是获取父类,第二是编写或创建服务文件。

流程首先是获取指定类型的父类,如果没有父类,且只有一个父类时,可以直接返回,否则会引发异常。

kt 复制代码
// 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 文件名。

然后,我们将已实现类的权限名称写入该文件。

kt 复制代码
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主要有三个关键功能:

  1. 通过ServiceLoader获取接口的所有实现类。
  2. 将特定的目标类与URI进行匹配。
  3. 从URI构建目标类对象。
    第一件事非常简单:
kt 复制代码
inline fun <reified T> findServices(): List<T> {
    val clazz = T::class.java
    return ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}

一旦你获取到它,你就可以开始与URL进行匹配。

这个匹配的方式是获取每个目标类的Destination注解中的路由字段,然后将其与路由进行比较。

kt 复制代码
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重新创建对象。

kt 复制代码
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方法分配值。

kt 复制代码
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

相关推荐
2401_85828611几秒前
101.【C语言】数据结构之二叉树的堆实现(顺序结构) 下
c语言·开发语言·数据结构·算法·
y25081 分钟前
《Object类》
java·开发语言
曙曙学编程2 分钟前
初级数据结构——树
android·java·数据结构
小技与小术6 分钟前
数据结构之树与二叉树
开发语言·数据结构·python
hccee27 分钟前
C# IO文件操作
开发语言·c#
hummhumm32 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊42 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
zmd-zk1 小时前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
好奇的菜鸟1 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.01 小时前
Go语言进阶&依赖管理
开发语言·后端·golang