Generate the code using KSP : Part 3

aniketbhoite

Aniket Bhoite

Posted on April 11, 2022

Generate the code using KSP : Part 3

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:

  1. Android KSP guide for dummies by a Dummy: Part 1 (link)
  2. KSP Gradle setup & Processor's first log: Part 2 (link)
  3. Generate the code using KSP : Part 3
  4. Using KSP output in app: Part 4 (link)

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

Enter fullscreen mode Exit fullscreen mode

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() }
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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) {
        ....
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Now we have access to all things we need for code generation.

As per our requirement we need to generate 3 things

  1. Override Method getBundleOfParamsForFirebase which returns Bundle with key values of event params
  2. Override Method getHashMapOfParamsForCustomAnalytics which returns HashMap
  3. 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)
Enter fullscreen mode Exit fullscreen mode

for bundle

putString("screenName", params.screenName)
Enter fullscreen mode Exit fullscreen mode

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()
    )
}
Enter fullscreen mode Exit fullscreen mode

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()
)
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
aniketbhoite
Aniket Bhoite

Posted on April 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related