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?
Adding Call Invocations
Next, add this method below addService()
to generate code for methods that invoke the endpoints and handle their results:
private fun TypeSpec.Builder.addMethods(): TypeSpec.Builder = apply {
for (endpoint in data.endpointData) { // 1
val name = funName(endpoint.type)
addFunction(FunSpec.builder(name) // 1
.addModifiers(KModifier.SUSPEND) // 1
.addParams(endpoint.pathComponents, annotate = false,
addBody = endpoint.type == RetroQuick.Type.POST) // 2
.returns(modelClassName.copy(true)) // 3
.addStatement("val service = %L().create(Service::class.java)", providerName) // 4
.addCode("""
|val response = service.%L(%L%L) // 5
|return if (response.isSuccessful) { // 6
| response.body()
|} else {
| throw Exception("Error: ${'$'}{response.code()}")
|}
|""".trimMargin(),
name,
if (endpoint.type == RetroQuick.Type.POST) "body, " else "",
endpoint.pathComponents.joinToString { it.paramName })
.build()
)
}
}
This code:
- Generates
suspend
for each endpoint with the name equal to the HTTP verb. - The parameters are the same as for the equivalent Service method, except they aren’t annotated. The
POST
has a body, while theGET
doesn’t. - The method returns an optional type of model class.
- Creates the service by statically invoking the function annotated with
@RetrofitProvider
. - Creates the call by invoking the newly created service with the provided name and parameters.
- Invokes the call and return its body if it was successful. Otherwise, it throws an
Exception
.
Then, tie it all together by updating build()
:
fun build(): TypeSpec = TypeSpec.objectBuilder(className)
.addService() // ADD THIS
.addMethods() // ADD THIS
.build()
All those helper methods make everything come together like Tetris blocks, assuming, of course, you’re good at Tetris. :]
All that’s left is to add some final touches.
Final Touches
Finally, go to Processor.kt and put this into process()
, above the final return statement:
data.models.forEach {
val fileName = "${it.modelName}RetroQuick"
FileSpec.builder(it.packageName, fileName)
.addType(CodeBuilder(data.providerName, fileName, it).build())
.build()
.writeTo(File(kaptKotlinGeneratedDir))
}
With this code, you invoke code generation for every model collected during processing.
Build the project again. The annotation processor will generate PersonRetroQuick.kt with this content:
package com.raywenderlich.android.retroquick
import kotlin.Int
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
object PersonRetroQuick {
suspend fun post(body: Person): Person? {
val service = com.raywenderlich.android.retroquick.getRetrofit().create(Service::class.java)
val response = service.post(body, )
return if (response.isSuccessful) {
response.body()
} else {
throw Exception("Error: ${response.code()}")
}
}
suspend fun get(id: Int): Person? {
val service = com.raywenderlich.android.retroquick.getRetrofit().create(Service::class.java)
val response = service.get(id)
return if (response.isSuccessful) {
response.body()
} else {
throw Exception("Error: ${response.code()}")
}
}
interface Service {
@POST("person")
suspend fun post(@Body body: Person): Response<Person?>
@GET("person/{id}")
suspend fun get(@Path("id") id: Int): Response<Person?>
}
}
Now, you can use the generated code.
Open MainActivity.kt and update testCall()
invocations to use PersonRetroQuick
instead of returning null
:
testGet.setOnClickListener {
testCall(getResults) {
PersonRetroQuick.get(123) // HERE
}
}
testPost.setOnClickListener {
testCall(postResults) {
PersonRetroQuick.post(Person(12, "Name")) // HERE
}
}
Using this code, you call the server endpoints and return their responses.
Finally, build and run! Press either of the buttons. You’ll see the network calls returning data from the server.
Where to Go From Here?
You can download the final version of this project using the Download Materials button at the top or bottom of this tutorial.
Congratulations on making it all the way through! You’ve learned even more about annotation and annotation processing, and you created a handy tool for quickly generating RESTful API code for your model classes.
You can learn more about repeatable annotations in Java and Kotlin from the official docs. It’s always a good idea to know more about code elements, too.
Hopefully, you had some fun on the way. If you have any questions or comments, please join the forum discussion below!