[KRouter] A simple and lightweight Kotlin Routing framework

[KRouter] A simple and lightweight Kotlin Routing framework

KRouter (Kotlin-Router) is a simple and lightweight Kotlin routing framework.

Specifically, KRouter is a framework for discovering interface implementation classes through URIs. It is used like this:

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

The reason for this is that after using Voyager for a period of time, I found that the communication between modules is not flexible enough, requires some configuration, and using DeepLink is a bit strange, so I prefer to use routing to achieve communication between modules, so I developed this library.

This library is mainly implemented through KSP, ServiceLoader and reflection.

Instructions

The above code is basically all that is used.

As mentioned earlier, this is the library for discovering interface implementation classes and matching targets by URI, so we first need to define an interface.

interface Screen

We then have a project with many independent modules that implement this interface, each module is different and we need to distinguish them by their respective routes (i.e. URI).

// HomeModule
@Destination("screen/home")
class HomeScreen(@Router val router: String = "") : Screen

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

Now we have two independent modules, each with its own screens (Screens), and they both have their own routing address.

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

Now, you can get these two objects through KRouter, and the route properties in these objects will be assigned to the KRouter.routeroute for the specific call to the pair.

Now you can get the parameters passed through the URI in HomeScreenand ProfileScreenand you can do some initialization and other operations with those parameters.

@Destination

@Destination The annotation is used to mark the destination (Destination) and contains two parameters:

  • route: The routing address uniquely identifying the destination, which must be a string of URI type and does not need to contain query parameters.
  • type: The interface of the destination. If the class has only one superclass or interface, you don't need to set this parameter, it can be inferred automatically. But if the class has multiple parent classes or interfaces, you need to specify this explicitly through the type parameter.

It is important to note that @Destinationthe annotated class must contain a parameterless constructor, otherwise ServiceLoader the object cannot be created. For Kotlin classes, you also need to ensure that each input parameter of the constructor has a default value.

@Router

@RouterThe annotation is used to specify which attribute of the destination class is used to receive the incoming routing parameters, and the attribute must be a string type.

Properties marked with this annotation will automatically be assigned a value, or you can leave the annotation alone. For example, in the above example, when HomeScreenthe object is created, the value of its router field is automatically set to screen/home?name=zhangke.

In particular, if the @Routerannotated property is not in the constructor, then the property must be declared as modifiable, that is, it should be a variable property modified by var in Kotlin.

KRouter is a Kotlin Object class that contains only one function:

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

This function accepts a generic type and a route address. Route addresses may or may not contain query parameters, but query parameters are ignored when matching destinations. After a successful match, an object will be constructed using this URI, and the URI will be passed to the annotation field in the target object @router .

integrated

First, you need to integrate KSP in your project.

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

Then, add the following dependencies:

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

Since you use ServiceLoader, you also need to set SourceSet.

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

It may also be necessary to add the JitPack repository:

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

working principle

As mentioned earlier, KRouter is mainly implemented through ServiceLoader + KSP + reflection.

This framework consists of two main parts: the compilation phase and the runtime phase.

KSP Plugins
The code related to KSP plugins is located in the compiler module.

The main task of the KSP plugin is to generate service files based on Destination annotations .ServiceLoader

The rest of the KSP code is basically the same, the main work includes first configuring the service file, then getting the class according to the annotation, and finally iterating through the Visitor. You can directly look at KRouterVisitor for more details.

override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
    
    
    val superTypeName = findSuperType(classDeclaration)
    writeService(superTypeName, classDeclaration)
}

visitClassDeclarationThe method mainly has two main functions, the first is to obtain the parent class, and the second is to write or create a service file.

The process first is to obtain the parent class of the specified type. If there is no parent class and there is only one parent class, it can be returned directly, otherwise an exception will be thrown.

// 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
    }
}

Once the parent class is obtained, we need to create a file with the name of the interface or abstract class as the required ServiceLoader file name.

We then write the name of the authority of the implemented class to this file.

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 has three key functions:

  1. Obtain all implementation classes of the interface through ServiceLoader.
  2. Match a specific target class against a URI.
  3. Build a target class object from a URI.
    The first thing is very simple:
inline fun <reified T> findServices(): List<T> {
    
    
    val clazz = T::class.java
    return ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}

Once you get it, you can start matching URLs.

The way this matches is to get the route field in the Destination annotation of each target class, and then compare it to the route.

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
}

The matching strategy is to ignore the query field and just match by baseUri.

The next step is to create the object. There are two situations to consider:

The first case is @Routerwhen the annotation is in the constructor, in which case the object needs to be created again using the constructor.

The second case is @Routerwhen the annotation is in a normal attribute. In this case, you can directly use ServiceLoaderthe created object and then assign values ​​to it.

If @Routerthe annotation is in the constructor, you can get it first routerParameter, then PrimaryConstructorrecreate the object with it.

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

If it's a normal variable property, first get the property, then do some type permissions and other checks, then call the setter method to assign the value.

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

The above is all about KRouter, I hope it will be helpful to you!

GitHub

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

Guess you like

Origin blog.csdn.net/u011897062/article/details/132685268