Annotation Processing: Supercharge Your Development
Annotation processing is a powerful tool for generating code for Android apps. In this tutorial, you’ll create one that generates RecyclerView adapters. By Gordan Glavaš.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Annotation Processing: Supercharge Your Development
25 mins
- Getting Started
- Starting Your First Annotation
- Exploring Annotation Anatomy
- Adding Another Annotation
- Introducing Annotation Processing
- Creating Your First Annotation Processor
- Adding Basic Processor Structure
- Registering Your Processor
- Extracting Annotation Metadata
- Coding Model Classes
- Processing the Annotations
- Generating Source Code
- Specifying the Output Folder
- Using KotlinPoet
- Adding the Processor to Your App
- Where to Go From Here?
As an Android developer, you’ve probably seen plenty of annotations already: They’re those funny code elements that start with @
and occasionally have parameters attached to them.
Annotations associate metadata with other code elements, allowing you to place more information into your code. One way to make use of annotations is to generate new source files based on that information via annotation processing.
In this tutorial, you’ll develop a set of annotations and an annotation processor that automatically generates RecyclerView.Adapter
code for a given model class.
Along the way, you’ll learn:
- What annotations are and how to create them.
- What’s an annotation processor and how to write one.
- How to generate code by using an annotation processor.
- How to profit from the generated code!
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project.
Open the starter project to find a small app named AutoAdapter. MainActivity.kt contains the sole Activity
containing a RecyclerView
. There’s also a model class in Person.kt. It defines a simple person model with a name and an address:
data class Person(
val name: String,
val address: String
)
Your mission — should you choose to accept it — is to write an annotation processor. This annotation processor will automatically generate the adapter for the RecyclerView
in MainActivity
based on annotating Person
.
Starting Your First Annotation
Your first step is to create a new module to hold your annotations. It’s common practice to hold annotations and processors in separate modules — though that’s not a requirement by any means.
Select File ▸ New ▸ New Module…, and then scroll down to select Java or Kotlin library.
On the next screen, you will:
- Name the module autoadapter-annotations.
- Set the package to com.raywenderlich.android.autoadapter.annotations.
- Set the class name to AdapterModel.
- Make sure Kotlin is selected as the language.
Press Finish. Then open AdapterModel.kt, which you’ll find in your newly created module!
Currently, AdapterModel
is a normal Kotlin class, but turning it into an annotation class is simple. You just need to put the annotation
keyword in front of the class. Like this:
annotation class AdapterModel
That’s it! You can now type @AdapterModel
elsewhere in the code to annotate other code elements.
Exploring Annotation Anatomy
Even when writing a simple annotation, you can’t go without using other annotations on it! You’ll annotate your annotation classes with two common annotations — yep, that’s an alliterative mouthful. :]
The first common annotation class is @Target
. It describes the contexts in which an annotation type is applicable. In other words, it tells which code elements you can place this annotation on. You’ll only use AdapterModel
on classes, so add the following code above its declaration:
@Target(AnnotationTarget.CLASS)
The second common annotation class is @Retention
. It tells the compiler how long the annotation should “live.” AdapterModel
only needs to be there during the source compilation phase, so you should add this below the @Target
annotation:
@Retention(AnnotationRetention.SOURCE)
@Retention
is RUNTIME
. If you use runtime retention on an annotation, you’ll be able to query for it using reflection. With the SOURCE
retention that you’re using for AdapterModel
, the annotation won’t make it into the compiled code at all.
Lastly, annotations can have parameters. These allow you to add even more information and fine-tune an annotation’s usage.
The AdapterModel
annotation needs a single parameter, the ViewHolder
layout ID. Update the declaration like this:
annotation class AdapterModel(val layoutId: Int)
Adding Another Annotation
You need one more annotation to specify how model fields map to views. Add another class to autoadapter.annotations, and name it ViewHolderBinding.kt. Replace the default class declaration with the following:
@Target(AnnotationTarget.PROPERTY) // 1
@Retention(AnnotationRetention.SOURCE) // 2
annotation class ViewHolderBinding(val viewId: Int) // 3
This annotation has the same anatomy as the previous one. However:
- Unlike
AdapterModel
, this one will exclusively target properties. - It only needs to be around during the compilation phase.
- Its sole parameter specifies the ID of the view that the annotated property should bind to.
Introducing Annotation Processing
The topic of annotation usage and consumption is broad and deep, yet it boils down to doing more with less. That is, less code (annotations) magically turns into more functionality, and the catalyst for this computational alchemy is annotation processing.
Here’s a quick breakdown of the core concepts. These points should bring you up to speed without going in-depth:
- Annotation processing is a tool built into javac for scanning and processing annotations at compile time.
- It can create new source files; however, it can’t modify existing ones.
- It’s done in rounds. The first round starts when the compilation reaches the pre-compile phase. If this round generates any new files, another round starts with the generated files as its input. This continues until the processor processes all the new files.
This diagram illustrates the process:
Creating Your First Annotation Processor
Time to write your first annotation processor! First, repeat the drill with adding a new Kotlin library module to the project:
- Select File ▸ New ▸ New module…
- Choose Java or Kotlin library.
- Input the module details:
- Name: autoadapter-processor.
- Package: com.raywenderlich.android.autoadapter.processor.
- Class name: Processor.
- Language: Kotlin.
The processor will need to know about your custom annotations. So, open autoadapter-processor/build.gradle and add the following inside the dependencies
block:
implementation project(':autoadapter-annotations')
Sync the project with Gradle files for the change to take effect.
Adding Basic Processor Structure
Open Processor.kt and replace the imports and the class declaration with the following:
import com.raywenderlich.android.autoadapter.annotations.AdapterModel
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 1
class Processor : AbstractProcessor() { // 2
override fun getSupportedAnnotationTypes() =
mutableSetOf(AdapterModel::class.java.canonicalName) // 3
override fun process(annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment): Boolean { // 4
// TODO
return true // 5
}
}
Wow! That’s a lot of code. Here’s a step-by-step rundown:
-
@SupportedSourceVersion
specifies that this processor supports Java 8. - All annotation processors must extend the
AbstractProcessor
class. -
getSupportedAnnotationTypes()
defines a set of annotations this processor looks up when running. If no elements in the target module are annotated with an annotation from this set, the processor won’t run. -
process
is the core method that gets called in every annotation-processing round. -
process
must return true if everything went fine.