Kotlin extension function generation 🚀…
here’s why it’s a game changer for Annotation Processing.
Most annotation processors that generate some code have two components:
Annotations is a lightweight component that can (but does not have to) be packed within your application.
Compiler is a “heavier” component that should never be included in your app. Its goal is to find usages of Annotations within your code and do some work based on that. It helps with building your code into the executable app (it helps with the compilation process).
If you’re designing the sort of annotation processor that generates some code, you have to quickly realise one important thing:
Annotation processors cannot modify exisiting code.
They can only generate new code.
This, plus the fact that the Compiler works in compile-time:
makes it a bit difficult to communicate between “written” and generated code.
In other words: it makes it a bit difficult to use the generated code in the code you’re writing. There are different approaches developers like to take to overcome this limitation. Here are the two most popular ones:
The “I don’t care” approach
If you’ve ever used Dagger, you’re probably familiar with this image:
For your IDE to be aware of the
DaggerAppComponent you have to first build the project so the Dagger compiler can generate the
The Dagger library essentially says:
I don’t care. If you configure everything correctly I will give you the
DaggerAppComponentin compile-time, but I won’t help you with interacting with it up-front. It’s your IDE’s job to figure this thing out…
Thankfully, the IDE can do some trickery to be able to resolve this. It analyses already generated classes from the previous round of code generation. That’s why you need to first build the project for the class to become “visible”, and why the class will once again become “invisible” if you clean your project.
This is what I like to call the “I don’t care” approach. This approach is surprisingly popular even though, it’s quite inelegant. It demands the responsibility for the proper operation from the IDE or the manual typing of the user.
“Reflection bridge” approach
This approach is quite sophisticated and when I first saw it and analysed it, I must say I was super impressed...
From the perspective of “noob-proofing”, this is the opposite approach from the previous one. It takes the full responsibility on the proper operation on the library itself.
The core of this approach is the third component, the Library:
This is something that, by design, has to be able to work in runtime, so it needs to be included in your application.
Since both Library & Annotations should (usually) be included in your app, they can be merged together:
The most important thing about the approach is that the library has some knowledge about the way compiler generates code:
- It knows what is the policy of naming the generated code.
- It knows what will be the exposed api of the generated code. The most elegant way to accomplish that is for the library to “share” an interface with the compiler (that will be implemented by the generated code).
Those are the two essential features of something I like to call a Reflection Bridge.
It is a library component that with some help from Java Reflection can:
- Find the generated code (using the knowledge about the code naming)
- Execute the generated code (using the knowledge about the code api)
All this happens in runtime.
The Butterknife library knows that classes generated by the compiler will:
- Have names composed of: name of the class & the
- Will be implementing
When you add any of the Butterknife’s
@Bind annotations to your class (let’s take for example
MainActivity), Butterknife’s compiler will detect that and generate for it the
The library will use its Reflection Bridge to:
- Find the
- Create an instance of
- Let you have it in the form of
This way, in a clean and elegant way (I know some people say Reflection is not elegant, but I believe this is an acceptable usage of it), the library will communicate with the generated code during runtime.
The new “Extension bridge” approach for Kotlin
It is quite similar to the Reflection bridge, but with one significant difference: it does not use Java Reflection.
Kotlin has this magical thing called… extension functions. And those can be nicely utilised for communicating with the generated code.
The way Kotlin extension functions work, lets you balance at edge of modifying the existing code with the annotation processor.
The proposed “Extension bridge” has two parts:
- Extension Bridge “End Cap” - precompiled, part of the library module
- Extension Bridge “Core”- compiled on demand, by the compiler
Extension Bridge “End Cap”
Provides dummy extension functions for the user to be available out of the box. Straight after importing the library module. So the user does not have to think what will be the generated code.
Methods in the “End Cap” must be of the highest level possible, so the extension functions generated for the “Core” can be “more specific” and be chosen by the compiler (and by the IDE) after the code generation.
Extension Bridge “Core”
This provides extension functions that will be chosen by the compiler over the “End Cap” methods. Those, generated extension functions will communicate directly with the generated code of the compiler.
The whole approach will be much easier to understand if I show you what it would look like if it was used for the Dagger and Butterknife:
Dagger2 “Extension Bridge” proof-of-concept
The working proof-of-concept library can be found here:
dagger2-ktx - Kotlin extension bridge library for Dagger2 (proof-of-concept)github.com
Library part (the “End Cap”):
It contains a
Dagger object declaration, and an extension function that mocks the one that will be generated by the compiler. It’s important that it must be an extension function, not member function of the
Dagger class (as that way it wouldn’t be properly overridden).
Even without any classes generated, the usage would look like this:
You could write that out of the box. Without the need for building the project.
Generated part (the “Core”):
This simple generated extension function creates an easy way to communicate with the generated code. It is possible due to this simple fact:
KClass<AppComponent> is more specific than
KClass<T> from the “End Cap”. That’s why the generated function will be chosen over the library extension function.
DaggerAppComponent and the Extension Bridge “Core” is generated, the
Dagger.create(AppComponent::class) will properly return an
AppComponent instance from the
Without any need to alter the
Butterknife “Extension Bridge” proof-of-concept
This one is actually very simple conceptually. It utilises Kotlin extensions functions instead of Java Reflection. The working proof-of-concept library can be found here:
Normally you would write something like this:
With the extension bridge approach this simply changes to:
So the “outside” perspective on the library looks almost the same (you just need to add
Library part (the “End Cap”):
The library contains a
ButterKnifeKtx object declaration, and few extension functions that mock those that will be generated by the compiler.
bind extension functions are very general (non-specific). The parameter targets must be non-specific so they can be easily overridden by the extension functions generated by the compiler.
They represent the exact same methods that are within the original Butterknife class.
Generated part (the “Core”):
When you add any of the
Butterknife annotations to your class (
@OnClick, etc…), the butterknife-ktx-compiler will generate extension functions to override some of those from the “End Cap”.
Depending on the type of class that contains any of the annotations from
Butterknife, the compiler will override proper functions from the library.
Example: for annotations added inside an
Activity (or in a
Dialog for that matter), the compiler will generate this simple function (an example for class
The usage inside the
MainActivity is as simple as the regular Butterknife, but no Java Reflection is being used in the process.
In case you were just jumping around the post (no hard feelings), this new approach really works. Here are two libraries that prove that:
butterknife-ktx - Kotlin extension bridge library for Butterknife (proof-of-concept)github.com