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

news2024/11/25 10:11:27

【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的特定调用的路由。

现在,您可以在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?

此函数接受一个泛型类型和一个路由地址。路由地址可以包含或不包含查询参数,但在匹配目的地时,查询参数将被忽略。匹配成功后,将使用此 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主要有三个关键功能:

  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()
}

一旦你获取到它,你就可以开始与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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/977931.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C# PSO 粒子群优化算法 遗传算法 随机算法 求解复杂方程的最大、最小值

复杂方程可以自己定义&#xff0c;以下是看别人的题目&#xff0c;然后自己来做 以下是计算结果 private void GetMinResult(out double resultX1, out double min){double x1, result;Random random1 new Random(DateTime.Now.Millisecond* DateTime.Now.Second);min 99999…

创建MyBatis的映射文件模板

Settings->Editor->->Code Style->File and Code Templates <?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dt…

tomcat部署、tomcat虚拟主机及tomcat多实例

目录 一、Tomcat概述 tomcat是什么&#xff1f; 什么是 servlet&#xff1f; 什么是 JSP? Tomcat 功能组件结构 Container 结构分析 Tomcat 请求过程 二、Tomcat部署 1、关闭防火墙和selinux&#xff0c;并将Tomcat所需软件包传到/opt目录下 2、安装JDK 3、设置JDK…

MATLAB 2022b 中设置关闭 MATLAB 之前进行询问

在 MATLAB 2022b 中可以进行设置&#xff0c;在关闭 MATLAB 之前进行询问&#xff0c;防止意外关闭 MATLAB。如图&#xff1a;

《Effective STL》读书笔记(一):容器

容器类型&#xff1a; 标准STL序列容器&#xff1a;vector, string, deque, list标准STL关联容器&#xff1a;set, multiset, map, multimap非标准序列容器slist和rope非标准关联容器&#xff1a;hash_set, hash_multiset, hash_map, hash_multimap标准的非STL容器&#xff1a…

LabVIEW利用纳米结构干电极控制神经肌肉活动

LabVIEW利用纳米结构干电极控制神经肌肉活动 随着人口老龄化&#xff0c;长期护理的必要性变得更加重要&#xff0c;医疗中心的压力开始达到惊人的水平。全球对所有社会和经济部门的认识对于更好地协调卫生和社会服务之间的护理以及为更多的院外治疗提供条件至关重要。 关于医…

SSE 和 WebSocket 应用

SSE 和 WebSocket 应用 一.SSE 和 WebSocket 对比二.SSE 和 WebSocket 调试SpringBoot 下 SSE 应用1.依赖2.启动类3.接口类4.Html 测试5.测试结果 SpringBoot 下 WebSocket 应用1.依赖2.启动类3.WS 切点配置4.WS连接类配置5.WS Html 测试6.测试结果 一.SSE 和 WebSocket 对比 …

数据结构与算法-选择冒泡快排

一&#xff1a;选择排序 场景&#xff1a;找出一个班上身高最高的人你会怎么找&#xff1f;A B C D A B 选择排序的思路和插入排序非常相似&#xff0c;也分已排序和未排序区间。但选择排序每次会从未排序区间中找到最小的元素&#xff0c;将其放到已排序区间的末尾。但是不像插…

浅谈硬件连通性测试方法有哪些

硬件连通性测试是一种用于验证硬件设备之间连接的稳定性和可靠性的测试过程。那么&#xff0c;硬件连通性测试方法有哪些呢?下面&#xff0c;就来看看具体介绍吧! 1、电气测试&#xff1a;电气测试用于检查硬件设备之间的电源和信号连接。这包括使用万用表或示波器测量电压、电…

Java开发之Redis(面试篇 持续更新)

文章目录 前言一、redis使用场景1. 知识分布2. 缓存穿透① 问题引入② 举例说明③ 解决方案④ 实战面试 3. 缓存击穿① 问题引入② 举例说明③ 解决方案④ 实战面试 4. 缓存雪崩① 问题引入② 举例说明③ 解决方案④ 实战面试 5. 缓存-双写一致性① 问题引入② 举例说明③ 解决…

面试设计模式-责任链模式

一 责任链模式 1.1 概述 在进行请假申请&#xff0c;财务报销申请&#xff0c;需要走部门领导审批&#xff0c;技术总监审批&#xff0c;大领导审批等判断环节。存在请求方和接收方耦合性太强&#xff0c;代码会比较臃肿&#xff0c;不利于扩展和维护。 1.2 责任链模式 针对…

uboot命令解析流程

uboot命令解析: (1)bootdelay没有打断,跑的是autoboot_command abortboot —>run_command_list (bootcmd) (2)否则走的cli_loop cli_loop –>cli_simple_loop ----> cli_readline —>run_command_repeatable -----> &#xff08;解析命令 匹配命令 运行命令 ) …

lv3 嵌入式开发-8 linux shell脚本函数

目录 1 函数的定义 2 函数的调用 3 变量的作用域 4 练习 1 函数的定义 基本语法&#xff1a; function name() {statements[return value] }function是 Shell 中的关键字&#xff0c;专门用来定义函数&#xff1b; name是函数名&#xff1b; statements是函数要执行…

java八股文面试[数据库]——自适应哈希索引

自适应Hash索引&#xff08;Adatptive Hash Index&#xff0c;内部简称AHI&#xff09;是InnoDB的三大特性之一&#xff0c;还有两个是 Buffer Pool简称BP、双写缓冲区&#xff08;Doublewrite Buffer&#xff09;。 1、自适应即我们不需要自己处理&#xff0c;当InnoDB引擎根…

FOXBORO FBM232 P0926GW 自动化控制模块

Foxboro FBM232 P0926GW 是 Foxboro&#xff08;福克斯博罗&#xff09;自动化控制系统的一部分&#xff0c;通常用于监测和控制工业过程。以下是关于这种类型的自动化控制模块可能具有的一些常见功能&#xff1a; 数字输入通道&#xff1a; FBM232 P0926GW 控制模块通常具有多…

2、在Windows 10中安装和配置 PostgreSQL 15.4

一、PostgreSQL 安装前简介 PostgreSQL&#xff08;通常简称为PG SQL&#xff09;是一个强大、开源的关系型数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它具有广泛的功能和可扩展性&#xff0c;被广泛用于企业和开发项目中,PostgreSQL 具有如下一些关键特点&…

VUE3+TS项目无法找到模块“../version/version.js”的声明文件

问题描述 在导入 ../version/version.js 文件时&#xff0c;提示无法找到模块 解决方法 将version.js改为version.ts可以正常导入 注意&#xff0c;因为version.js是我自己写的模块&#xff0c;我可以直接该没有关系&#xff0c;但是如果是引入的其他的第三方包&#xff0c…

Windows系统的桌面显示信息工具___BGInfo使用

一、BGInfo简介 BGInfo(桌面显示信息工具)是微软开发的用于在Windows系统中实现将Windows系统信息【如:当前用户名、CPU、操作系统版本、IP地址、硬盘等】或自定的内容显示在桌面壁纸上的操作工具,用户可以根据自己的需要定制属于自己的桌面内容(特别是对应企业来说通过域…

原理之Thread与Runnable的关系

原理之Thread与Runnable的关系 附录 课程 附录 1.Thread和Runnable的关系

深入实现 MyBatis 底层机制的任务阶段4 - 开发 Mapper 接口和 Mapper.xml

&#x1f600;前言 在我们的自定义 MyBatis 底层机制实现过程中&#xff0c;我们已经深入研究了多个任务阶段&#xff0c;包括配置文件的读取、数据库连接的建立、执行器的编写&#xff0c;以及 SqlSession 的封装。每个任务阶段都为我们揭示了 MyBatis 内部工作原理的一部分&a…