【KRouter】一個(gè)簡(jiǎn)單且輕量級(jí)的Kotlin Routing框架
KRouter(Kotlin-Router)是一個(gè)簡(jiǎn)單而輕量級(jí)的Kotlin路由框架。
具體來(lái)說(shuō),KRouter是一個(gè)通過(guò)URI來(lái)發(fā)現(xiàn)接口實(shí)現(xiàn)類(lèi)的框架。它的使用方式如下:
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
之所以這樣做,是因?yàn)樵谑褂肰oyager一段時(shí)間后,我發(fā)現(xiàn)模塊之間的通信不夠靈活,需要一些配置,而且使用DeepLink有點(diǎn)奇怪,所以我更喜歡使用路由來(lái)實(shí)現(xiàn)模塊之間的通信,于是我開(kāi)發(fā)了這個(gè)庫(kù)。
這個(gè)庫(kù)主要通過(guò)KSP、ServiceLoader和反射來(lái)實(shí)現(xiàn)。
使用方法
上述代碼基本上就是使用的全部?jī)?nèi)容。
如前所述,這是用于發(fā)現(xiàn)接口實(shí)現(xiàn)類(lèi)并通過(guò)URI匹配目標(biāo)的庫(kù),因此我們首先需要定義一個(gè)接口。
interface Screen
然后我們有一個(gè)包含許多獨(dú)立模塊的項(xiàng)目,這些模塊實(shí)現(xiàn)了這個(gè)接口,每個(gè)模塊都不同,我們需要通過(guò)它們各自的路由(即URI)來(lái)區(qū)分它們。
// HomeModule
@Destination("screen/home")
class HomeScreen(@Router val router: String = "") : Screen
// ProfileModule
@Destination("screen/profile")
class ProfileScreen : Screen {
@Router
lateinit var router: String
}
現(xiàn)在我們有兩個(gè)獨(dú)立的模塊,它們各自擁有自己的屏幕(Screens),并且它們都有自己的路由地址。
val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
val profileScreen = KRouter.route<Screen>("screen/profile?name=zhangke")
現(xiàn)在,您可以通過(guò)KRouter獲取這兩個(gè)對(duì)象,并且這些對(duì)象中的路由屬性將被分配給對(duì)KRouter.route
的特定調(diào)用的路由。
現(xiàn)在,您可以在HomeScreen
和ProfileScreen
中獲取通過(guò)URI傳遞的參數(shù),并且可以使用這些參數(shù)進(jìn)行一些初始化和其他操作。
@Destination
@Destination
注解用于標(biāo)記目的地(Destination),包含兩個(gè)參數(shù):
-
route
:目的地的唯一標(biāo)識(shí)路由地址,必須是 URI 類(lèi)型的字符串,不需要包含查詢(xún)參數(shù)。 -
type
:目的地的接口。如果類(lèi)只有一個(gè)父類(lèi)或接口,您無(wú)需設(shè)置此參數(shù),它可以自動(dòng)推斷。但如果類(lèi)有多個(gè)父類(lèi)或接口,您需要通過(guò) type 參數(shù)明確指定。
需要特別注意的是,被 @Destination
注解標(biāo)記的類(lèi)必須包含一個(gè)無(wú)參數(shù)構(gòu)造函數(shù),否則 ServiceLoader
無(wú)法創(chuàng)建對(duì)象。對(duì)于 Kotlin 類(lèi),您還需要確保構(gòu)造函數(shù)的每個(gè)輸入?yún)?shù)都具有默認(rèn)值。
@Router
@Router
注解用于指定目的地類(lèi)的哪個(gè)屬性用于接收傳入的路由參數(shù),該屬性必須是字符串類(lèi)型。
使用此注解標(biāo)記的屬性將自動(dòng)分配一個(gè)值,或者您可以不設(shè)置注解。例如,在上述示例中,當(dāng)創(chuàng)建 HomeScreen
對(duì)象時(shí),其 router 字段的值將自動(dòng)設(shè)置為 screen/home?name=zhangke
。
特別要注意,如果被@Router
注解的屬性不在構(gòu)造函數(shù)中,那么該屬性必須聲明為可修改的,即在 Kotlin 中應(yīng)為 var 修飾的可變屬性。
KRouter 是一個(gè) Kotlin Object 類(lèi),它只包含一個(gè)函數(shù):
inline fun <reified T : Any> route(router: String): T?
此函數(shù)接受一個(gè)泛型類(lèi)型和一個(gè)路由地址。路由地址可以包含或不包含查詢(xún)參數(shù),但在匹配目的地時(shí),查詢(xún)參數(shù)將被忽略。匹配成功后,將使用此 URI 構(gòu)造對(duì)象,并將 URI 傳遞給目標(biāo)對(duì)象中的 @router
注解字段。
集成
首先,您需要在項(xiàng)目中集成 KSP。
https://kotlinlang.org/docs/ksp-overview.html
然后,添加以下依賴(lài)項(xiàng):
// 模塊的 build.gradle.kts
implementation("com.github.0xZhangKe.KRouter:core:0.1.5")
ksp("com.github.0xZhangKe.KRouter:compiler:0.1.5")
由于使用了 ServiceLoader,您還需要設(shè)置 SourceSet。
// 模塊的 build.gradle.kts
kotlin {
sourceSets.main {
resources.srcDir("build/generated/ksp/main/resources")
}
}
可能還需要添加 JitPack 倉(cāng)庫(kù):
maven { setUrl("https://jitpack.io") }
工作原理
正如前面所提到的,KRouter 主要通過(guò) ServiceLoader + KSP + 反射來(lái)實(shí)現(xiàn)。
這個(gè)框架由兩個(gè)主要部分組成:編譯階段和運(yùn)行時(shí)階段。
KSP 插件
與 KSP 插件相關(guān)的代碼位于編譯器模塊中。
KSP 插件的主要任務(wù)是根據(jù) Destination
注解生成 ServiceLoader
的服務(wù)文件。
KSP 代碼的其余部分基本相同,主要工作包括首先配置服務(wù)文件,然后根據(jù)注解獲取類(lèi),最后通過(guò) Visitor 進(jìn)行迭代。您可以直接查看 KRouterVisitor 來(lái)了解更多細(xì)節(jié)。
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val superTypeName = findSuperType(classDeclaration)
writeService(superTypeName, classDeclaration)
}
visitClassDeclaration
方法主要有兩個(gè)主要功能,第一是獲取父類(lèi),第二是編寫(xiě)或創(chuàng)建服務(wù)文件。
流程首先是獲取指定類(lèi)型的父類(lèi),如果沒(méi)有父類(lèi),且只有一個(gè)父類(lèi)時(shí),可以直接返回,否則會(huì)引發(fā)異常。
// 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
}
}
一旦獲取到父類(lèi),我們需要?jiǎng)?chuàng)建一個(gè)文件,其文件名以接口或抽象類(lèi)的權(quán)限作為所需的 ServiceLoader 文件名。
然后,我們將已實(shí)現(xiàn)類(lèi)的權(quán)限名稱(chēng)寫(xiě)入該文件。
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主要有三個(gè)關(guān)鍵功能:
- 通過(guò)ServiceLoader獲取接口的所有實(shí)現(xiàn)類(lèi)。
- 將特定的目標(biāo)類(lèi)與URI進(jìn)行匹配。
- 從URI構(gòu)建目標(biāo)類(lèi)對(duì)象。
第一件事非常簡(jiǎn)單:
inline fun <reified T> findServices(): List<T> {
val clazz = T::class.java
return ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}
一旦你獲取到它,你就可以開(kāi)始與URL進(jìn)行匹配。
這個(gè)匹配的方式是獲取每個(gè)目標(biāo)類(lèi)的Destination注解中的路由字段,然后將其與路由進(jìn)行比較。
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
}
匹配策略是忽略查詢(xún)字段,只需通過(guò)baseUri進(jìn)行匹配即可。
接下來(lái)的步驟是創(chuàng)建對(duì)象。有兩種情況需要考慮:
第一種情況是@Router
注解位于構(gòu)造函數(shù)中,在這種情況下,需要再次使用構(gòu)造函數(shù)創(chuàng)建對(duì)象。
第二種情況是@Router
注解位于普通屬性中。在這種情況下,可以直接使用ServiceLoader
創(chuàng)建的對(duì)象,然后將值分配給它。
如果@Router
注解位于構(gòu)造函數(shù)中,您可以首先獲取routerParameter
,然后使用PrimaryConstructor
重新創(chuàng)建對(duì)象。
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))
}
如果它是一個(gè)普通的變量屬性,首先獲取屬性,然后進(jìn)行一些類(lèi)型權(quán)限和其他檢查,然后調(diào)用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)
}
上面是關(guān)于KRouter的全部?jī)?nèi)容,希望對(duì)你有所幫助!文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-701236.html
GitHub
https://github.com/0xZhangKe/KRouter文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-701236.html
到了這里,關(guān)于【KRouter】一個(gè)簡(jiǎn)單且輕量級(jí)的Kotlin Routing框架的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!