Android Ecosystem Multiplatform Tools

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!

image description