Advanced Annotation Processing
Annotation processing is a powerful tool that allows you to pack more data into your code, and then use that data to generate more code. 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
Advanced Annotation Processing
30 mins
- Getting Started
- Retrofit2 and Test Server
- Processor Logging and Error Handling
- Analyzing and Capturing Functions
- @RetrofitProvider Annotation
- Validating Code Elements
- A Suitable @RetrofitProvider Candidate
- @RetroQuick Annotation
- Extracting Model Data
- Repeatable Annotations
- Code Generation
- A Few Helper Methods
- Adding Service Interface
- Adding Call Invocations
- Final Touches
- Where to Go From Here?
Annotation processing is a powerful tool that lets you pack more data into your code and then use that data to generate more code. They are denoted by the @
symbol, and are used to add extra functionality to your code. This tutorial helps you build upon the knowledge from the Annotations: Supercharge Your Development tutorial. Be sure to check it out to familiarize yourself with annotations, the concept of annotation processing and generating new source files.
This is an advanced topic which will require some basic knowledge of annotation processing. If you are not already familiar with this topic, certainly check out the Annotations: Supercharge Your Development tutorial.
This is an advanced topic which will require some basic knowledge of annotation processing. If you are not already familiar with this topic, certainly check out the Annotations: Supercharge Your Development tutorial.
In this tutorial, you’ll build an annotation processor that generates code for accessing RESTful API endpoints for a model class, using Retrofit2 to do the heavy lifting.
Along the way, you’ll learn some finer points of annotation processing, such as:
- Logging from inside the processor and error handling.
- Analyzing code elements for kind, data types and visibility modifiers.
- Using repeatable annotations.
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project.
Open the starter project and you’ll find three modules: retroquick, retroquick-annotations and retroquick-processor.
-
retroquick is a small app that tests the code generated by the annotation processor. It has a sample model class in Person.kt and a simple, two-button UI in MainActivity.kt. The buttons trigger mock server calls using
testCall
, but the mocked response is alwaysnull
. - retroquick-annotations is mostly empty, but will house the annotation classes.
- retroquick-processor is the annotation processor you’ll use to generate code files. It contains a bare bones annotation processor in Processor.kt, as well as the necessary processor setup.
The dependencies for all the modules are already there for you, including dependencies between modules. retroquick-processor uses retroquick-annotations and retroquick invokes both via the kapt tool.
Retrofit2 and Test Server
Retrofit is an Android and Java library that makes networking easy. It works in conjunction with OkHttp to make HTTP requests and handles serialization and deserialization on its own.
It also has built-in support for coroutines. Check out Android Networking With Kotlin Tutorial: Getting Started to learn more about it.
To test the app, you will use a small server, deployed at Heroku. Conveniently enough, it works with the same Person
model as this project, and exposes two endpoints:
-
GET at path
/person/{id}
: Returns aPerson
with ID equal to the passedid
parameter, and name equal to"Person $id"
. -
POST at path
/person
: Sends back thePerson
data from the request body.
Now, you will start by implementing logging from within the processor, followed by handling errors.
Processor Logging and Error Handling
First, you’ll add logging to your annotation processor which will let you print debug notes, warnings and errors while processing. The messages will display in the Build output window, alongside other build tasks:
In retroquick-processor add a new package named util. Then, inside newly created package, add ProcessorLogger.kt. Add this as its content:
import javax.annotation.processing.ProcessingEnvironment
import javax.tools.Diagnostic
import javax.lang.model.element.Element
class ProcessorLogger(private val env: ProcessingEnvironment) { // 1
fun n(message: String, elem: Element? = null) { // 2
print(Diagnostic.Kind.NOTE, message, elem)
}
fun w(message: String, elem: Element? = null) { // 2
print(Diagnostic.Kind.WARNING, message, elem)
}
fun e(message: String, elem: Element? = null) { // 2
print(Diagnostic.Kind.ERROR, message, elem)
}
private fun print(kind: Diagnostic.Kind,
message: String,
elem: Element?) {
print("\n")
env.messager.printMessage(kind, message, elem) // 3
}
}
Here’s a code breakdown:
- Logging from a processor requires an instance of
ProcessingEnvironment
. - This code exposes three methods for the three available logging levels:
n
for note,w
for warning ande
for error. - Ultimately, all three methods use
print
, which displays the message at the appropriate level and for the given code element.
Next, in retroquick-processor open Processor.kt. Add this property:
private lateinit var logger: ProcessorLogger
Then, initialize it inside the env?.let
block in init()
:
logger = ProcessorLogger(it)
Great job! Now your processor has a logger and can communicate with the world!
Next, you’ll analyze and capture functions.
Analyzing and Capturing Functions
As you may know, Retrofit2 uses a Retrofit
instance to convert Service
interfaces into executable code. So, your generated code will need access to such an instance to make the magic happen.
You might consider delegating that to autogenerated code as well. But it’s better to give the developer full control over what their Retrofit
instance looks like: what kind of HTTP client it uses, which interceptors, which headers it appends to every call.
@RetrofitProvider Annotation
First, use a simple annotation to mark a function that provides Retrofit
. Its name is RetrofitProvider
. Quite imaginative, huh?
In the sole package of retroquick-annotations, create a new file. Name it RetrofitProvider.kt and add:
@Target(AnnotationTarget.FUNCTION) // 1
@Retention(AnnotationRetention.SOURCE) // 2
annotation class RetrofitProvider // 3
As you can see, it’s a simple annotation class that:
- Can only annotate a function.
- Is only retained during compilation.
- Has no parameters.
Next, think about how the generated code will use this function. It’ll invoke it statically, and potentially from outside the package where the generated classes reside, meaning it needs to be public as well.
To keep things simpler, it can’t have any parameters. And you only want to have a single @RetrofitProvider
function in your codebase since having more than one wouldn’t make much sense. Now you’ll bake all these rules into your annotation processor.
Create a class to hold the data you’ll gather while processing.
In retroquick-processor create a new package named models. In models, create a new file, RetroQuickData.kt. Define this simple class in it:
data class RetroQuickData(val providerName: String) {}
providerName
holds the fully qualified name of the function annotated with @RetrofitProvider
.
Next, open Processor.kt and add RetrofitProvider
to the list of supported annotations. Now, getSupportedAnnotationTypes()
looks like this:
override fun getSupportedAnnotationTypes() = mutableSetOf(
RetrofitProvider::class.java.canonicalName
)
In Processor.kt, add a RetroQuickData
instance property, right below the list of declarations above init()
for later use:
private lateinit var data: RetroQuickData
Next, you’ll validate code elements.