Better Annotation Processing: Supporting Stubs in kapt

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:

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:

Also, kapt can now take care of passing parameters to annotation processors. Here’s an example for the AndroidAnnotations library:

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 in our example.

So, we are thinking of an opt-in functionality to make fields non-private in classes generated by Kotlin.


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:

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?


About Andrey Breslav

Andrey is the lead language designer of Kotlin at JetBrains.
This entry was posted in Android, Libraries, Tools. Bookmark the permalink.

18 Responses to Better Annotation Processing: Supporting Stubs in kapt

  1. Salomon BRYS says:

    Great work 😉

    One thing that I am missing is the ability to generate Kotlin code with an annotation processor.

  2. Dale King says:

    Regarding the visibility of backing fields are you talking about a switch that would make all fields non-private? Don’t like that idea.

    It should be tied to specific annotations. That could be a single additional annotation you add to the injected field to control access or what would be most helpful is to allow defining a mapping from individual annotations to an access level. So I could tell the compiler that any field annotated with javax.inject for example should be package access. The question would then be how to specify that mapping to the compiler.

    • We are talking about a way of annotating individual properties to make their fields public/protected. E.g.:

  3. Mickele Moriconi says:

    Awesome! I can’t wait to try it out :)

  4. HE Guangyu says:

    In my opinion, this implementation is not so elegant, and brings much complexity for users.

    Maybe we should accept the situation, “, there was no way for them to refer to any code generated by the processor (e.g. Dagger’s module classes)”, and hope that:
    1 some programer rewrite Dagger or DBFlow by Kotlin, and elemites these limitation.
    2 Kotlin team provides some guides on how to write annotation-processing-friendly programs for kotlin programmers.

  5. Sven Jacobs says:

    Great progress!

    I’m looking forward to the possibility of field injection in Kotlin with Dagger 2. At the moment you still have to write too much boilerplate code.

    What is the YouTrack issue regarding the non-private fields opt-in functionality?

  6. Sven Jacobs says:

    After a few days of experimenting with Kotlin, Android and Dagger 2 I have the following issue:

    When I perform a clean build and run the application (Cmd+C, Cmd+R) from Android Studio (v1.2.2) everything works fine but once I rerun the project (Cmd+R) without changing any file, the generated classes seem to be missing from the APK:

    java.lang.NoClassDefFoundError: Failed resolution of: L … DaggerApplicationComponent;

    Only when I perform a clean build the application runs from the IDE. This does not seem to happen when compiling via Gradle from command line.

    Any idea what this could be?

  7. Cyril Vlaminck says:

    After some times playing around with Kotlin, I tried integrating AndroidAnnotations as you show in this post. Unfortunately I did not manage to make AndroidAnnotations preprocessor process annotations on my Kotlin classes. They do work on Java classes in the project.

    As far as I understand, stub classes are generated and included in the classpath of the javac that will compile the Java sources and execute the annotation processor but stubs classes are not detected by AA processor.

    Do I miss something? Do you have a working exemple for AndroidAnnotations?

  8. Robbie Straw says:

    This worked really well, I was able to move some JPA Entities over to Kotlin and run the QueryDSL APT by adding two lines to my build.gradle file.

    My only complaint is that there doesn’t seem to be a way to instruct kapt to store the generated sources. So while my build works, the generated classes cannot be seen by my IDE and thus break autocomplete.

  9. Rondillor says:

    I cannot manage to make Dagger2 work:
    Whatever I do it builds right the first time or when I do a clean (gradlew clean build). But when I rebuild a second time (gradlew build for example) I get the following exception:
    UNEXPECTED TOP-LEVEL EXCEPTION: Multiple dex files define Ljavax/inject/Inject;

    But again this disappear each time I do a clean before… I never got these issues wti

Comments are closed.