Generate the code using KSP : Part 3
Aniket Bhoite
Posted on April 11, 2022
Good Morning, In this part let's start with understanding how processor gets called and how to use it.
Checkout other parts in this series:
Steps
1) Defining MyEvent annotation
First lets define MyEvent
annotations class in annotations module
package com.aniket.myevent.annotations
@Target(AnnotationTarget.CLASS)
annotation class MyEvent
2) Processing all Annotated classes in MyEventProcessor
Registered Class MyEventProcessorProvider
which implements SymbolProcessorProvider
interface. We must override the create method with SymbolProcessorEnvironment variable. In this method we have to initialise our MyEventProcessor
class with following variables(check part 2 point 4)
-
environment.codeGenerator
: helps with creating and managing files -
environment.logger
: for logging to build output.
MyEventProcessor
class implements SymbolProcessor
interface. We have to override abstract method process(resolver: Resolver). In this method we use resolver to get access to classes, functions or variables that are annotated with specified Annotation.
Following code snippet we get us all class's with our Annotation i.e. MyEvent as KSAnnotated implantation.
val symbols = resolver
.getSymbolsWithAnnotation(MyEvent::class.qualifiedName!!)
val unableToProcess = symbols.filterNot { it.validate() }
We have to filter symbols because we only want symbols which are instance of KSClassDeclaration
and then we will visit each symbol one by one separately
val dependencies = Dependencies(false, *resolver.getAllFiles().toList().toTypedArray())
symbols.filter { it is KSClassDeclaration && it.validate() }
.forEach { it.accept(MyEventKClassVisitor(dependencies), Unit) }
return unableToProcess.toList()
In KSP we use Visitor Pattern, We will create MyEventKClassVisitor
inner class that implements KSVisitorVoid()
interface. Here we are only concerned with Classes so we will only override visitClassDeclaration method.
JavaTPoint website has very good easy explanation on visitor pattern, click here to read
private inner class MyEventKClassVisitor(val dependencies: Dependencies) : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration:
KSClassDeclaration, data: Unit) {
....
}
}
Before we go any further we need some validations that the classes that are annotated with MyEvent
are not Abstract or Interface.
if (classDeclaration.isAbstract()) {
logger.error(
"||Class Annotated with MyEvent should kotlin data class",classDeclaration)
}
if (classDeclaration.classKind != ClassKind.CLASS) {
logger.error(
"||Class Annotated with Projections should kotlin data class", classDeclaration)
}
Next we need some variable strings.
val className = classDeclaration.simpleName.getShortName()
val classPackage = classDeclaration.packageName.asString() + "." + className
val classVariableNameInCamelCase = className.replaceFirst(className[0], className[0].lowercaseChar()) //need this for using in generated code
logger.warn("package $classPackage")
We will also access that Class's primary constructor variables list
val properties = classDeclaration.primaryConstructor?.parameters ?: emptyList()
if (properties.isEmpty())
logger.error("No variables found in class", classDeclaration)
Tip: Use logger to understand and verify KSP variables.
Now that we are on topic of properties lets add some helper Kotlin Extension function in processor module as below
fun KSValueParameter.isNotKotlinPrimitive(): Boolean {
return when (type.element?.toString()) {
"String", "Int", "Short", "Number", "Boolean", "Byte", "Char", "Float", "Double", "Long", "Unit", "Any" -> false
else -> true
}
}
fun KSValueParameter.getPrimitiveTypeName(): String {
return type.element?.toString() ?: throw IllegalAccessException()
}
Now we have access to all things we need for code generation.
As per our requirement we need to generate 3 things
- Override Method getBundleOfParamsForFirebase which returns Bundle with key values of event params
- Override Method getHashMapOfParamsForCustomAnalytics which returns HashMap
- class that implements Event interface and holds above methods
We need to add all parameters in hashmap and bundle in specific syntax.
for Hashmap we need all properties like
hashmap.put("screenName",parmas.screenName)
for bundle
putString("screenName", params.screenName)
We will loop through all the properties and create their hash map and bundle entries in String builder.
val hashmapEntries = StringBuilder()
val bundleEntries = StringBuilder()
for (prop in properties) {
// Throw Error if param is not primitive
if (prop.isNotKotlinPrimitive())
logger.error("|| Event params variables should be Primitive", prop)
val propName = prop.name?.getShortName() ?: ""
logger.warn("|| ${prop.name?.getShortName()}")
hashmapEntries.append(
"""
put("$propName", $classVariableNameInCamelCase.$propName)
""".trimMargin()
)
val propPrimitiveTypeName = prop.getPrimitiveTypeName()
bundleEntries.append(
"""
put$propPrimitiveTypeName("$propName", $classVariableNameInCamelCase.$propName)
""".trimMargin()
)
}
Now we just have to generate Event class
private val packageName = "com.aniket.myevent"
val toGenerateFileName = "${classDeclaration.simpleName.getShortName()}Event"
val outputStream: OutputStream = codeGenerator.createNewFile(
dependencies = dependencies,
packageName,
fileName = toGenerateFileName
)
outputStream.write(
"""
|package $packageName
|import $classPackage
|import android.os.Bundle
|import $packageName.Event
|class $toGenerateFileName(val $classVariableNameInCamelCase: $className): Event {
| override fun getHashMapOfParamsForCustomAnalytics(): HashMap<*, *>? {
| val map = HashMap<String, Any>().apply {
| $hashmapEntries
| }
| return map
| }
|
| override fun getBundleOfParamsForFirebase(): Bundle {
| val bundle = Bundle().apply {
| $bundleEntries
| }
| return bundle
| }
|}
""".trimMargin().toByteArray()
)
That's it. Lets look at example of our usecase.
For example with following class we ran or build app
import com.aniket.myevent.annotations.MyEvent
@MyEvent
data class TicketBookToClickedParams(
val eventName: String,
val screenName: String,
val ticketNumber: Int,
val ticketAmount: String,
)
The generated code of above Annotated class will be as follow at project/app/build/generated/ksp/debug/kotlin/com/aniket/myevent :
import android.os.Bundle
class TicketBookToClickedParamsEvent(val ticketBookToClickedParams: TicketBookToClickedParams) :
Event {
override fun getHashMapOfParamsForCustomAnalytics(): HashMap<*, *>? {
val map = HashMap<String, Any>().apply {
put("eventName", ticketBookToClickedParams.eventName)
put("screenName", ticketBookToClickedParams.screenName)
put("ticketNumber", ticketBookToClickedParams.ticketNumber)
put("ticketAmount", ticketBookToClickedParams.ticketAmount)
}
return map
}
override fun getBundleOfParamsForFirebase(): Bundle {
val bundle = Bundle().apply {
putString("eventName", ticketBookToClickedParams.eventName)
putString("screenName", ticketBookToClickedParams.screenName)
putInt("ticketNumber", ticketBookToClickedParams.ticketNumber)
putString("ticketAmount", ticketBookToClickedParams.ticketAmount)
}
return bundle
}
}
At this point we are able generate Event class. Now the only thing remaining is Initialising and calling their methods to fire respective Events. We will do that in 4th and last part. Using KSP output in app: Part 4
Links
GitHub repo part-3 branch: https://github.com/aniketbhoite/ksp-my-event/tree/part-3
Posted on April 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.