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


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

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

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

    val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
    
    • 1

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

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

    使用方法

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

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

    interface Screen
    
    • 1

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

    // HomeModule
    @Destination("screen/home")
    class HomeScreen(@Router val router: String = "") : Screen
    
    // ProfileModule
    @Destination("screen/profile")
    class ProfileScreen : Screen {
        @Router
        lateinit var router: String
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
    val profileScreen = KRouter.route<Screen>("screen/profile?name=zhangke")
    
    • 1
    • 2

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

    inline fun <reified T : Any> route(router: String): T?
    
    • 1

    此函数接受一个泛型类型和一个路由地址。路由地址可以包含或不包含查询参数,但在匹配目的地时,查询参数将被忽略。匹配成功后,将使用此 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")
    
    • 1
    • 2
    • 3

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

    // 模块的 build.gradle.kts
    kotlin {
        sourceSets.main {
            resources.srcDir("build/generated/ksp/main/resources")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可能还需要添加 JitPack 仓库:

    maven { setUrl("https://jitpack.io") }
    
    • 1

    工作原理

    正如前面所提到的,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)
    }
    
    • 1
    • 2
    • 3
    • 4

    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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    一旦获取到父类,我们需要创建一个文件,其文件名以接口或抽象类的权限作为所需的 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)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    KRouter主要有三个关键功能:

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

    一旦你获取到它,你就可以开始与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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    匹配策略是忽略查询字段,只需通过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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果它是一个普通的变量属性,首先获取属性,然后进行一些类型权限和其他检查,然后调用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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    上面是关于KRouter的全部内容,希望对你有所帮助!

    GitHub

    https://github.com/0xZhangKe/KRouter

  • 相关阅读:
    mysql查看sql日志操作
    二叉树6——二叉树的最大深度
    Spring Boot JPA 存储库派生查询示例
    动态加载内容爬取,Ajax爬取典例
    t-分布扰动策略和变异策略的花授粉算法-附代码
    ICM-20948芯片详解(8)
    MiniTest--小程序自动化测试框架
    小常识:琢磨一下淘宝上手机换套餐的生意
    CSS选择器和样式[补充]
    MySQL:MySQL的集群——主从复制的原理和配置
  • 原文地址:https://blog.csdn.net/u011897062/article/details/132685268