Generate the code using KSP : Part 3
Tags: andorid, ksp, kotlin, metaprogramming
Published: 2022-04-11 06:06:31

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](https://dev.to/aniketbhoite/android-ksp-guide-for-dummies-by-a-dummy-part-1-3dof))
> 2. KSP Gradle setup & Processor's first log: Part 2 ([link](https://dev.to/aniketbhoite/ksp-gradle-setup-processors-first-log-part-2-43b1))
3. Generate the code using KSP : Part 3
4. Using KSP output in app: Part 4 ([link](https://dev.to/aniketbhoite/using-ksp-output-in-app-part-4-45il))

---


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 <u>create</u> 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 <u>process(resolver: Resolver)</u>. 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 <u>visitClassDeclaration</u> method.

JavaTPoint website has very good easy explanation on visitor pattern, [click here to read](https://www.javatpoint.com/visitor-design-pattern-java#:~:text=Visitor%20Pattern,operations%20from%20an%20object%20structure.)  

```
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

1. Override Method <u>getBundleOfParamsForFirebase</u> which returns Bundle with key values of event params
2. Override Method <u>getHashMapOfParamsForCustomAnalytics</u> 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)
```

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 <u>_project/app/build/generated/ksp/debug/kotlin/com/aniket/myevent_</u> :
```
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](https://dev.to/aniketbhoite/using-ksp-output-in-app-part-4-45il)

Links
GitHub repo part-3 branch: https://github.com/aniketbhoite/ksp-my-event/tree/part-3