Creating library using annotation processing and Kotlin Poet

Annotation processing is one of the most interesting topics for me to learn and understand and here I want to share my results.

I’m almost sure that every Android developer uses annotation processors in projects. Usually we are using frameworks with already written processors e.g Butterknife, DataBinding, Dagger, etc. But what about writing our own processor to make our code look better. First of all I want to describe a problem that I’m going to solve.

I’m using Kotlin and I don’t like how interface implementation looks like. It looks almost like in Java. Here is a simple example:

Imagine we have an interface with two methods:

interface Callback {
fun success(response: String, code: Int)
fun error(errorCode: Int)
}

Looks familiar. And then we need to pass this interface to the function setCallBack(callback: Callback) . There are two ways: the first one — you can implement this Callback in your class and then pass this into the function, and the second way is using anonymous class. Like this:

someClass.setCallback(object : Callback {
override fun success(response: String, code: Int) {
}
override fun error(errorCode: Int) {
}
})

Looks like Java :)

What if we can make this interface look better by using code generation? There is a Kotlin pattern that can do that.

final class PimpedCallback : Callback {
private var _success: ((String, Int) -> Unit)? = null
private var _error: ((Int) -> Unit)? = null override fun success(arg0: String, arg1: Int) {
_success?.invoke(arg0, arg1)
}
fun success(receiver: (String, Int) -> Unit): Unit {
_success = receiver
}
override fun error(arg0: Int) {
_error?.invoke(arg0)
}
fun error(receiver: (Int) -> Unit): Unit {
_error = receiver
}
}

And also we need to change setCallback method:

fun setListener(init: PimpedCallback.() -> Unit) {
callBack = PimpedCallback().apply(init)
}

Looks like an overkill, but let’s check the callback implementation:

someClass.setListener {
success { response, code -> processResponse(response, code) }
error { _ -> }
}

Yeah, lambda power. Important that here functions are optional. But there is still a question about writing these pimped interfaces. For each function we have to add a field and a setter for the field. Also we need to add a class that implements our interface and add invoke() to functions implementations. Looks not two difficult for interface with two functions, but what if we have three and more? We need to write this big and ugly classes to have cute implementation. So I decided to create a small library called Trembita. Btw Trembita is an ukrainian music instrument. And now it’s time for Annotation processing and Kotlin Poet

Annotation processing is a tool from Java 1.5 that can help you build new programs during compilation. There are a lot of articles on this topic, so here I’ll just describe all key components and how to implement them for this particular usecase.

First of all we need an annotation. I’ve called my annotation @Trembita

package com.trembita.annotation @Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Trembita

We have to add here @Retention and @Target . Constant RUNTIME means that annotation is stored in class file and usable in runtime. AnnotationTarget.CLASS means that our annotation is applicable only for classes. That is what we are looking for. Now we need to create processor — the class that is responsible for reading annotations and processing (so obvious).

class TrembitaProcessor : AbstractProcessor() {}

Then we need to override some functions, the most important one is process:

this function provides set of annotations (in our case it’s only one), and RoundEnviroment object, that contains all classes that uses our annotation. Actually it’s not a class — it’s TypeElement . TypeElement is an interface that contains information about the class and its enclosing elements — in other words class members, parent classes, interfaces, etc.

Here we get classes that are annotated by @Trembita and then mapped to my own TrembitaClass wrapper. Then we map them to TrembitaClasses:

So here we are creating base properties of future generated class: package name, path where class would be stored, and the most important one — class body.

To create the class body we’ll use Kotlin Poet. All magic is inside TrembitaGenerator.generateClass() that takes TrembitaClass model as a parameter.

So here we define the class name and add class modifiers. In our case there are KModifier.PUBLIC and KModifier.FINAL . And using function addSuperinterface we can add a typename of interface that we are going to extend.

The class body is generated in generateTrembitaMethods() function. This function is quite big so we split it and check all parts of it.

First what we need to do — find all enclosed elements and check each one that has ElementKind.METHOD . In else case we throw the IllegalStateException() because we don’t support any other class members yet.

So let’s look into PimpedCallback . So for one function of the interface we have to create:

  • field with lambda
  • function realisation
  • field initialisation function

So let’s build the first one:

In order to create properties we use PropertySpec.builder that takes a function name and the TypeName, in our case it’s a LambdaTypeName . Here we need to process arguments from our function to convert them to lambda expression. As the result we have list of Elements. As return type I’ve passed Unit , this is temporary solution for the first version. So the library support only void/Unit methods. Then we mention that our property is mutable, nullable and initialised as null .

Now we need to generate original function realisation.

It’s also easy to do with KotlinPoet. We need to use theFunSpec.builder that takes a name as a parameter. As we can see, name is the same as the name of the original function. Then adding parameters, which are exactly the same as in the original function. And now we need statement — that is actually a simple String.

The third part is a function for field initialisation:

and here we have the same algorithm as for original function realisation, except the statement and that we are not overriding this function.

Finally we need to add all these builders to our generated class:

That is it. Class will be generated after build, and can be used in the source code as mentioned above.

https://github.com/root1991/Trembita — here you can check the library source code. This library is in alpha now, so I do not suggest to use it in commercial projects or libraries. In closest future, I will add tests and gradle plugin for the library, so you don’t need to rebuild a project.

Thank you for reading this, hope you enjoyed this article ;)

Android Developer, Tech geek