reflection API to get class name at runtime - Survives refactoring - Can’t make spelling mistakes - If you use minimization tools like Proguard, you will lose the constant
processing framework requires you to put a file in the location “META-INF/services/javax.annotation.processing.Processor” that contains the qualified name (package + class name) of your processor. If you forget to do this, your processor will not run (even though we’re using kapt). Use google’s auto-service library to do this for you (refer to link in references) by annotating your processor with @AutoService and adding the dependency to Gradle (see example project in references).
= setOf<String>() override fun getSupportedAnnotationTypes() = setOf(Tag!::class.qualifiedName) }A We must override this method and return a set containing the name of our annotation. If we don’t return anything, our processor won’t be run.
= setOf<String>() override fun getSupportedAnnotationTypes() = setOf(Tag!::class.qualifiedName) override fun process( set: Set<TypeElement>, environment: RoundEnvironment ): Boolean }A - Process function is where we generate code - Return true when you are done generating code - Environment parameter represents all the code you are analyzing
val sourceDir = processingEnv.options["kapt.kotlin.generated"] }Z - processingEnv is a property we use to get setup params - processingEnv.options provides us with key/value pairs given to our processor - kapt.kotlin.generated option provided automatically by kapt, it’s the output directory
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) }Z We get a list of all elements annotated with @Tag from the project
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) }Z Element can represent a class (TypeElement), a function (ExecutableElement), a field (VariableElement), etc. We have a list of Element, so we need to convert to the proper subtype.
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> FunSpec .getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()AB }A }Z FunSpec is from KotlinPoet. It’s a declarative API for defining a function. We create a getter using the proper builder. We add a single return statement. That return statement returns the name of the annotated class.
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> FunSpec .getterBuilder() .addStatement("return %S","Pizza") .build()AB }A }Z Pizza class gets passed through and we create a function that has the body return “Pizza”
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> val getterSpec = FunSpec .getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()A PropertySpec .builder("${typeElement.qualifiedName}.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A }Z We need to create the properties still. PropertySpec is KotlinPoet’s way of creating properties. We are creating an extension property with the type of String. Kotlin poet handles imports for us and keywords so we don’t have to write them ourself. We then add the getter to the property
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> val getterSpec = FunSpec .getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()A PropertySpec .builder("com.food.Pizza.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A }Z When we process the Pizza class, we create the property with the right name
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> val getterSpec = FunSpec .getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()A PropertySpec .builder("${typeElement.qualifiedName}.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A .let { properties !-> FileSpec .builder("com.anthonycr.tagger", "Tags") .addProperties(properties) .build()C }B }Z We have to put our properties somewhere. We’ll put them in a file called “Tags.” We are using KotlinPoet again to create a FileSpec with the provided package name and file name.
val sourceDir = processingEnv.options["kapt.kotlin.generated"] environment .getElementsAnnotatedWith(Tag!::class.java) .filterIsInstance(TypeElement!::class.java) .map { typeElement !-> val getterSpec = FunSpec .getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()A PropertySpec .builder("${typeElement.qualifiedName}.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A .let { properties !-> FileSpec .builder("com.anthonycr.tagger", "Tags") .addProperties(properties) .build()C }B .writeTo(File(sourceDir)) }Z We need to write our FileSpec to disk. It has a writeTo function that takes a file. We create the file from the option given to us by kapt, which is the output directory.
@Target(AnnotationTarget.CLASS) annotation class Tag( val caseStyle: CaseStyle = CaseStyle.NORMAL )A Annotation arguments provide a way to have extra options. Here, we create an option that can convert the tag to UPPERCASE. Annotation arguments MUST have a default value, they cannot be null.
.getterBuilder() .addStatement("return %S", typeElement.simpleName) .build()A PropertySpec .builder("${typeElement.qualifiedName}.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A We need to modify the map function to accommodate the new arguments
val returnValue = when (caseStyle) { CaseStyle.NORMAL !-> typeElement.simpleName.toString() CaseStyle.ALL_CAPS !-> typeElement.simpleName.toString().toUpperCase() } val getterSpec = FunSpec .getterBuilder() .addStatement("return %S", returnValue) .build()A PropertySpec .builder("${typeElement.qualifiedName}.TAG", String!::class) .mutable(false) .getter(getterSpec) .build()B }A We get the annotation from the TypeElement. Then we get the option. We switch on the option and change the return value based on it.
} Processor arguments are a way to add global options to an annotation processor. They are added as key/value string pairs in your gradle file as shown here. We’ll create an option that allows us to change the package name of the “Tags” file.
getSupportedOptions() = setOf<String>() override fun getSupportedAnnotationTypes() = setOf(Tag!::class.qualifiedName) override fun process( set: Set<TypeElement>, environment: RoundEnvironment ): Boolean }A Now the getSupportedOptions function we didn’t use earlier becomes useful.
getSupportedOptions() = setOf("tagger.package_name") override fun getSupportedAnnotationTypes() = setOf(Tag!::class.qualifiedName) override fun process( set: Set<TypeElement>, environment: RoundEnvironment ): Boolean }A We need to return the name of the option. If we don’t, then the framework won’t recognize us as a receiver of the option.
properties !-> FileSpec .builder(packageName, "Tags") .addProperties(properties) .build() }A Similar to how we got the output directory, we get the option from processingEnv. We provide a default value because this option is not required.
Kotlin The annotation processor in this talk can be found on Github. If you are new to annotation processing, I suggest you clone the code and compile and run the sample app on your machine. You can use this presentation to follow along. https://github.com/anthonycr/Tagger