Kotlin
A concise multiplatform language developed by JetBrains
Better Annotation Processing: Supporting Stubs in kapt
This post is largely outdated.
Please refer to the kapt documentation.
We announced kapt
, an Annotation Processing Tool for Kotlin, some time ago, and discussed its limitations. Now most of the limitations are going away with the updated version of kapt
that is available as a 0.1-SNAPSHOT
preview.
Recap: How kapt worked before
The initial version of kapt
worked by intercepting communication between annotation processors (e.g. Dagger 2) and javac
, and added already-compiled Kotlin classes on top of the Java classes that javac
saw itself in the sources. The problem with this approach was that, since Kotlin classes had to be already compiled, there was no way for them to refer to any code generated by the processor (e.g. Dagger’s module classes). Thus we had to write Dagger application classes in Java.
How it works now
As discussed in the previous blog post, the problem can be overcome by generating stubs of Kotlin classes before running javac
and then running real compilation after javac
has finished. Stubs contain only declarations and no bodies of methods. The Kotlin compiler used to create such stubs in memory anyways (they are used for Java interop, when Java code refers back to Kotlin), so all we had to do was serialize them to files on disk.
Example: DBFlow
Stubs enable frameworks that rely on code generated by annotation processors. For example, you can now use DBFlow in Kotlin:
public object ItemRepository { public fun getAll(): MutableList<Item> { return Select() .from(javaClass<Item>()) .where() .orderBy(false, Item_Table.UPDATED_AT) .queryList() } }
The DSL-like functions Select()
, from()
etc are provided by DBFLow library, and Item_Table
is generated by DBFlow’s annotation processor, and the Kotlin code above can happily refer to it!
The full example is available here (thanks to Mickele Moriconi for the initial code).
Note that generating stubs requires relatively much work, because all declarations must be resolved, and sometimes knowing return types requires analysis of expression (bodies of functions or property initializers after the =
sign). So, using stubs in kapt
slows your build down somewhat. That’s why stubs are off by default, and to enable them you need to write the following in your build.gradle
file:
kapt { generateStubs = true }
Also, kapt
can now take care of passing parameters to annotation processors. Here’s an example for the AndroidAnnotations library:
kapt { generateStubs = true arguments { arg("androidManifestFile", variant.outputs[0].processResourcesTask.manifestFile) } }
Source-retained annotations
As you might have noticed, we generate stubs as binary .class
-files, not as .java
sources. This is more convenient for a number of reasons:
- we already generate the necessary bytes for a different purpose,
- in a class file we can simply skip a body of a method, no need to generate a stub body that would make
javac
happy, javac
would compile the stub sources and generate class files that we’d need to remove later,- this way the old (fast) and the new (slower) modes of
kapt
use the same essential mechanisms.
But binary stubs have their own disadvantages:
- if an annotation is marked by
@Retention(RetentionPolicy.SOURCE)
it is not present in the binaries, javac
does not propagate@Inherited
annotations down the class hierarchies.
We have not addressed the latter issue so far, but the former, source-retained annotations is absolutely critical since many popular frameworks (such as DBFlow
) have their annotations source-retained. Fortunately, when javac
reads the binaries, it does not double-check the annotations, and if we write a source-retained annotation to the class file despite the declared retention, it will happily see it. This is what we do now :)
Remaining limitations
There’s some work still pending on kapt
.
The biggest issue with annotation processing itself is supporting @Inherited
annotations. We’ll need to work around javac
not propagating them down the hierarchies of binary classes.
But the real issue lies outside kapt
: many frameworks, such as AndroidAnnotation and the aforementioned DBFlow want to inject values into fields directly, and Kotlin being all about safety and making those fields private
is getting in the way. This is why for now we have to write DBFlow “table classes” in Java, see Item.java
in our example.
So, we are thinking of an opt-in functionality to make fields non-private in classes generated by Kotlin.
Feedback
The new kapt
is not released yet, but you are welcome to try it out and tell us what you think. Here’s an example of how you do it:
repositories { maven { url 'https://raw.github.com/Raizlabs/maven-releases/master/releases' } maven { url 'http://oss.sonatype.org/content/repositories/snapshots' } jcenter() } dependencies { ... // DBFlow kapt 'com.raizlabs.android:DBFlow-Compiler:2.0.0' compile 'com.raizlabs.android:DBFlow-Core:2.0.0' compile 'com.raizlabs.android:DBFlow:2.0.0' // Kotlin compile 'org.jetbrains.kotlin:kotlin-stdlib:0.1-SNAPSHOT' } kapt { generateStubs = true }
Again, see the full DBFlow example here.
Please tell us:
- What worked for you?
- What didn’t?
- What do you like or dislike?
- Any use cases we overlooked?
Thanks!