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š.

Leave a rating/review
Download materials
Save for later
Share

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!
Note: This tutorial assumes you have previous experience developing for Android in Kotlin. If you’re unfamiliar with Kotlin, take a look at this tutorial. If you’re also new to Android development, check out the Getting Started with Android tutorials.

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.

Creating a new Java or Kotlin library module

On the next screen, you will:

  1. Name the module autoadapter-annotations.
  2. Set the package to com.raywenderlich.android.autoadapter.annotations.
  3. Set the class name to AdapterModel.
  4. Make sure Kotlin is selected as the language.

Configuring module settings

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)
Note: Another popular value for @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:

  1. Unlike AdapterModel, this one will exclusively target properties.
  2. It only needs to be around during the compilation phase.
  3. 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:

The annotation processing loop

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:

  1. Select File ▸ New ▸ New module…
  2. Choose Java or Kotlin library.
  3. 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:

  1. @SupportedSourceVersion specifies that this processor supports Java 8.
  2. All annotation processors must extend the AbstractProcessor class.
  3. 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.
  4. process is the core method that gets called in every annotation-processing round.
  5. process must return true if everything went fine.