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:


AppComponent class annotated with @dagger.Component)For your IDE to be aware of the DaggerAppComponent you have to first build the project so the Dagger compiler can generate the DaggerAppComponent(in compile-time).
The Dagger library essentially says:
I donāt care. If you configure everything correctly I will give you the DaggerAppComponent in 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 best example of this approach is Jake Whartonās Butterknife library:


The Butterknife library knows that classes generated by the compiler will:
- Have names composed of: name of the class & the
_ViewBinding - Will be implementing
Unbinderinterface.
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 MainActivity_ViewBinding class:


The most important part of the Butterknifeās library is the Butterknife class itself. When you call:


MainActivity example)The library will use its Reflection Bridge to:
- Find the
MainActivity_ViewBindingclass. - Create an instance of
MainActivity_ViewBindingclass. - Let you have it in the form of
Unbinderinterface.
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.


After the DaggerAppComponent and the Extension Bridge āCoreā is generated, the Dagger.create(AppComponent::class) will properly return an AppComponent instance from the DaggerAppComponent.create() initializer.
Without any need to alter the ExampleApplication:


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 Ktx to ButterKnife).
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.
All 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 (@BindView, @BindViews, @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 MainActivity):


The usage inside the MainActivity is as simple as the regular Butterknife, but no Java Reflection is being used in the process.
TLDR:
In case you were just jumping around the post (no hard feelings), this new approach really works. Here are two libraries that prove that:
If you enjoyed this post, please show your support! Clap, follow, comment,Ā share.
This really means aĀ lot!

