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

【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 {@Routerlateinit 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.javareturn ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}

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

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

fun findServiceByRouter(serviceClassList: List<Any>,router: String,
): Any? {val routerUri = URI.create(router).baseUrival service = serviceClassList.firstOrNull {val serviceRouter = getRouterFromClassAnnotation(it::class)if (serviceRouter.isNullOrEmpty().not()) {val serviceUri = URI.create(serviceRouter!!).baseUriserviceUri == routerUri} else {false}}return service
}private fun getRouterFromClassAnnotation(targetClass: KClass<*>): String? {val routerAnnotation = targetClass.findAnnotation<Destination>() ?: return nullreturn 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 nullif (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 nullfillRouterToServiceProperty(router = router,service = service,property = routerProperty,)return service
}private fun KClass<*>.findRouterProperty(): KProperty<*>? {return declaredMemberProperties.firstOrNull { property ->val isRouterProperty = property.findAnnotation<Router>() != nullisRouterProperty}
}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.setterval propertyType = setter.parameters[1]if (propertyType.type != stringKType) errorRouterParameterType(propertyType)property.setter.call(service, router)
}

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

GitHub

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

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

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

相关文章

OpenCV(三十二):轮廓检测

1.轮廓概念介绍 在计算机视觉和图像处理领域中&#xff0c;轮廓是指在图像中表示对象边界的连续曲线。它是由一系列相邻的点构成的&#xff0c;这些点在边界上连接起来形成一个封闭的路径。 轮廓层级&#xff1a; 轮廓层级&#xff08;Contour Hierarchy&#xff09;是指在包含…

雅思 《九分达人》阅读练习(二)

目录 雅思阅读练习 《九分达人》test3 paragraph3 1.单词含义要记准确&#xff0c;敏感度要上来。 2.找准定位&#xff0c;之后理解句子大致含义。 说说关于判断题的做题方法 关于“承认”有哪些单词 同替词汇 think 可以用什么其他单词来替换 单词 一些疑问 I have…

win10 sourcetree打开一闪就退出

参考文档: 解决方案参考文档一: Solved: cant install Sourcetree on Windows 10Solved: when I double-click the installation file or run as administrator,I can see a splash screen and disappear in 10 seconds,then I open thehttps://community.atlassian.com/t5…

【C++】vector的模拟实现【完整版】

目录 一、vector的默认成员函数 1、vector类的大体结构 2、无参构造函数 3、拷贝构造函数 4、Swap(operator需要用) 5、赋值重载operator 6、析构函数 二、vector的三种遍历方式 1、size和capacity(大小和容量) 2、 operator[]遍历 3、迭代器iterator遍历和范围for 三…

php常用算法

许多人都说 算法是程序的核心&#xff0c;一个程序的好于差,关键是这个程序算法的优劣。作为一个初级phper&#xff0c;虽然很少接触到算法方面的东西 。但是对于冒泡排序&#xff0c;插入排序&#xff0c;选择排序&#xff0c;快速排序四种基本算法&#xff0c;我想还是要掌握…

Oracle数据库环境变量配置以及可能遇到的问题解决

一、如何配置Oracle数据库环境变量&#xff08;以win10为例&#xff09; 1、找到此电脑&#xff0c;鼠标右键&#xff0c;点击属性。 2、点击属性成功后&#xff0c;进入如下页面&#xff0c;找到“高级系统设置”&#xff0c;点击进入。 3、找到环境变量&#xff0c;点击进入…

STM32F4X RTC

STM32F4X RTC 什么是RTCSTM32F4X RTCSTM32F4X RTC框图STM32F4X RTC计数频率STM32F4X RTC日历STM32F4X RTC闹钟 STM32F4X RTC例程 什么是RTC RTC全程叫Real-Time Clock实时时钟&#xff0c;是MCU中一个用来计时的模块。RTC的一个主要作用是用来显示实时时间&#xff0c;就像日常…

pip安装skimage的方法

在安装skimage时&#xff0c;可能会报错误&#xff1a; 可以尝试&#xff1a;pip install scikit-image进行安装&#xff0c;使用时只需要&#xff1a;import skimage

《消息队列》专栏介绍

《消息队列》专栏介绍 目录 《消息队列》专栏介绍专栏导言什么是消息队列呢&#xff1f;应用场景&#xff08;作用&#xff09; 为什么要用消息队列呢&#xff1f;异步处理削峰填谷 举个例子 分布式消息队列的优势 应用解耦优点发布订阅优点 分布式消息队列应用场景不同消息队列…

武警三维数字沙盘电子沙盘虚拟现实模拟推演大数据人工智能开发教程第15课

部队三维数字沙盘电子沙盘虚拟现实模拟推演大数据人工智能开发教程第15课 现在不管什么GIS平台首先要解决的就是数据来源问题&#xff0c;因为没有数据的GIS就是一个空壳&#xff0c;下面我就目前一些主流的数据获取 方式了解做如下之我见&#xff08;主要针对互联网上的一些…

Linux之Shell概述

目录 Linux之Shell概述 学习shell的原因 shell是什么 shell起源 查看当前系统支持的shell 查看当前系统默认shell Shell 概念 Shell 程序设计语言 Shell 也是一种脚本语言 用途 Shell脚本的基本元素 基本元素构成&#xff1a; Shell脚本中的注释和风格 Shell脚本编…

期货基础知识

一、期货是什么&#xff1f;  期货是与现货相对应&#xff0c;并由现货衍生而来。期货通常指期货合约&#xff0c;期货与现货完全不同&#xff0c;现货是实实在在可以交易的货&#xff08;商品&#xff09;&#xff0c;期货主要不是货&#xff0c;而是以某种大众产品如棉花、大…

2023-大数据应用开发-工业数据实时处理-参考结果

工业数据实时处理-答案 任务一&#xff1a;实时数据采集 1、 在主节点使用Flume采集/data_log目录下实时日志文件中的数据&#xff0c;将数据存入到Kafka的Topic中&#xff08;Topic名称分别为ChangeRecord、ProduceRecord和EnvironmentData&#xff0c;分区数为4&#xff09…

vue响应式详解

1. 响应式的定义 我们都知道&#xff0c;vue是基于javascript的&#xff0c;那我们使用一段javascript代码来描述响应式 let a 1,b 1,c; c a b; console.log(c) // 输出 2 // 改变 a的值 a 3; // 重新给c赋值 即把 c a b; 再执行一遍c的值才能变为 4 // c a b; // …

基于SpringBoot的无忌在线考试系统(源码+讲解+调试运行)做毕设课设均可

技术栈 前后端分离 前端使用: Vue Element Plus 后端使用: SpringBoot Mysql8.0 Mybatis-Plus 功能 分为 管理员端 和 老师端 和 学生端 管理员端 登陆页 ​科目管理 查看所有科目 ,增加 ,修改 ,删除科目 , 模糊搜索课程 ​考试管理 查看所有考试 ,增加 ,修改 ,删除考试 题库…

小白学go基础05-变量声明形式

和Python、Ruby等动态脚本语言不同&#xff0c;Go语言沿袭了静态编译型语言的传统&#xff1a;使用变量之前需要先进行变量的声明。 变量声明形式使用决策流程图 这里大致列一下Go语言常见的变量声明形式&#xff1a; var a int32 var s string "hello" var i 13 …

【RabbitMQ】RabbitMQ 服务无法启动。系统出错。发生系统错误 1067。进程意外终止。

问题描述 RabbitMQ 服务无法启动。 rabbitmq-service.bat startRabbitMQ 服务正在启动 . RabbitMQ 服务无法启动。系统出错。发生系统错误 1067。进程意外终止。原因分析 RabbitMQ和Erlang版本不匹配。 解决方案 查询并安装RabbitMQ版本对应Erlang版本 https://www.rabbitm…

如何指定this

<script>/*如何指定this的值可以通过2类方法指定1.调用时指定1.1call方法1.2apply方法2.创建时指定2.1bind方法2.2箭头函数*/// ------1.调用时指定------//1.1call方法:挨个传入参数//1.2apply方法:数组形式传入参数function foo (numA, numB) {console.log(this)consol…

自动化测试基础知识详解

前言 有颜色的标注主要是方便记忆&#xff0c;勾选出个人感觉的重点 块引用&#xff1a;大部分是便于理解的话&#xff0c;稍微看看就行&#xff0c;主要是和正常的文字进行区分的 1、什么是自动化测试 自动化测试是软件测试活动中一个重要分支和组成部分&#xff0c;随着软…

一加11/Ace2/10Pro手机如何实现全局120HZ高刷-游戏超级流畅效果

已经成功root啦。安卓13目前也一样支持LSPosed框架&#xff0c;如果你对LSP框架有需求&#xff0c;也可以使 自测120HZ刷新率诞生以后&#xff0c;很多小伙伴用上了就很难回来啦&#xff0c;一加11/Ace2/10Pro/9pro手 机厂商也对新机做了很多的适配&#xff0c;让我们日常使用起…