Home Android & Kotlin Tutorials

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.

5/5 2 Ratings

Version

  • Kotlin 1.4, Android 6.0, Android Studio 4.2

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.

Note: This tutorial assumes you’re familiar with Android development and Android Studio. If these topics are new to you, read Beginning Android Development and Kotlin for Android tutorials first.

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.

  1. 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 always null.
  2. retroquick-annotations is mostly empty, but will house the annotation classes.
  3. 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 a Person with ID equal to the passed id parameter, and name equal to "Person $id".
  • POST at path /person: Sends back the Person 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:

The Build output window in Android Studio

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:

  1. Logging from a processor requires an instance of ProcessingEnvironment.
  2. This code exposes three methods for the three available logging levels: n for note, w for warning and e for error.
  3. 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:

  1. Can only annotate a function.
  2. Is only retained during compilation.
  3. 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.

Validating Code Elements

Add a method to Processor.kt to validate @RetrofitProvider candidates for the conditions above:

private fun validateRetrofitProvider(elem: Element): Boolean {
  (elem as? ExecutableElement)?.let { // 1
    if (!typeUtils.isSameType(it.returnType,
	elemUtils.getTypeElement("retrofit2.Retrofit").asType())) { // 2
      logger.e("@RetrofitProvider should return retrofit2.Retrofit", elem)
      return false
    }
    val modifiers = it.modifiers
    if (Modifier.PUBLIC !in modifiers
        || Modifier.STATIC !in modifiers) { // 3
      logger.e("@RetrofitProvider should be public static", elem)
      return false
    }
    return true
  } ?: return false
}

This method validates three conditions:

  1. First it checks that the element is a function by checking if it’s an ExecutableElement.
  2. Then it checks if its returnType is of type Retrofit2. This requires a combination of typeUtils and elemUtils.
  3. Finally, it ensures the function’s modifiers are PUBLIC and STATIC.
Note: Annotation processing is, first and foremost, a JVM feature. As such, you need to think in terms wider than Kotlin. For example, static isn’t even a Kotlin keyword.

With that set up, capture the function with @RetrofitProvider and validate it. To process(), right above the line with return true, add:

roundEnv.getElementsAnnotatedWith(RetrofitProvider::class.java) // 1
  .also {
    if (!::data.isInitialized && it.size != 1) { // 2
      logger.e("Must have exactly 1 @RetrofitProvider")
      return false
    }
  }
  .forEach {
    if (!validateRetrofitProvider(it)) { // 3
      return false
    }
    data = RetroQuickData(qualifiedName(it)) // 4
    logger.n("RetrofitProvider located: ${data.providerName} \n") // 5
  }

This code:

  1. Annotates all code elements with @RetrofitProvider.
  2. Verifies there’s one, and only one, such element in all the processing rounds. If not, it prints an error and terminates processing.
  3. Validates the found element with validateRetrofitProvider(). It prints errors by itself, so you terminate processing here if validation fails.
  4. If everything’s fine, it instantiates data with the fully qualified name of the annotated function.
  5. Notes the path in the processor log.
Note: You could argue that using composition would be a better solution than a static invocation of a zero-parameter function, and you’d be right. For example, you could pass Retrofit as a parameter to generated code. However, such a solution would be more involved and is outside the scope of this tutorial.

Now take a look at a suitable @RetrofitProvider candidate.

A Suitable @RetrofitProvider Candidate

Now it’s time to put this to use. Open MainActivity.kt and add the following outside the class:

@RetrofitProvider
fun getRetrofit(): Retrofit { // 1
  return Retrofit.Builder()
      .baseUrl("https://tranquil-caverns-05334.herokuapp.com/") // 2
      .client(OkHttpClient.Builder() // 3
          .connectTimeout(10, TimeUnit.SECONDS)
          .readTimeout(10, TimeUnit.SECONDS)
          .writeTimeout(10, TimeUnit.SECONDS)
          .build())
      .addConverterFactory(GsonConverterFactory.create(GsonBuilder() // 3
          .create()))
      .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 4
      .build()
}

This is a:

  1. Public and static function that returns Retrofit.
  2. It points to the test server.
  3. It uses a conventional OkHttpClient with small timeouts.
  4. The requests and response bodies are automatically serialized or deserialized with the Gson library.
  5. Since RetroQuick generates suspendable functions, you need to use CoroutineCallAdapterFactory.

A standalone Kotlin function with no visibility modifiers is, by default, public and static. Now, build the project again. You’ll see the success note in the build log:

The Build output window presenting a note: Task :app:kaptDebugKotlin 
Note: RetrofitProvider located: com.raywenderlich.android.retroquick.getRetrofit

You can also try playing around with triggering error messages. For example, try to duplicate getRetrofit(). Name it getRetrofit2() and try building again.

This time, the compilation will fail, and you’ll get the following error message:

The Build output window with the error: Must have exactly 1 @RetrofitProvider

Note: Before you proceed, remove any extra retrofit provider method you added except getRetrofit()!

Next, you’ll explore @RetroQuick annotations.

@RetroQuick Annotation

Now it’s time to create the annotation that represents a RESTful endpoint for a model class. It should have a type representing its HTTP verb, GET or POST, and specify an optional path.

If the path is empty, assume the endpoint is the same as the model class name. The path can also specify path components that must map to model fields.

Create a new file named RetroQuick.kt in retroquick-annotations‘s only package. Add this as its content:

@Target(AnnotationTarget.CLASS) // 1
@Retention(AnnotationRetention.SOURCE) // 2
annotation class RetroQuick(
    val type: Type = Type.POST, // 3
    val path: String = ""
) {
enum class Type {
    GET, POST
  }
}

As you can see, this annotation class:

  1. Can only annotate classes.
  2. Is retained only during compilation.
  3. Has the two parameters, with type being a separate enum.

As before, find getSupportedAnnotationTypes() in Processor.kt. Update it to support @RetroQuick.

getSupportedAnnotationTypes() now looks like this:

override fun getSupportedAnnotationTypes() = mutableSetOf(
      RetrofitProvider::class.java.canonicalName,
      RetroQuick::class.java.canonicalName // ADD THIS
)

Then, go to Person.kt. Annotate it to represent the GET endpoint for your test server:

@RetroQuick(path="person/{id}", type = RetroQuick.Type.GET)
data class Person

Now, you’ll extract model data.

Extracting Model Data

First, add a few helper data classes to model pieces of your final data. All of these will reside in models of retroquick-processor.

Add a named FieldData.kt. Then add:

data class FieldData(
    val paramName: String,
    val returnType: String
)

This code represents data about a particular field: its parameter and return type.

Then, create EndpointData.kt with the code below:

data class EndpointData(
    val path: String,
    val type: RetroQuick.Type,
    val pathComponents: List<FieldData>
)

This data class holds information about the endpoints for the model class.

Next, add ModelData.kt. To it add:

data class ModelData(
    val packageName: String,
    val modelName: String,
    var endpointData: List<EndpointData>,
) {
  val qualifiedName get() = "$packageName.$modelName"  

  operator fun plusAssign(other: ModelData) {
    endpointData += other.endpointData
  }
}

This class holds all the information for a particular annotated model class: its package, name and data for all the endpoints.

Finally, add the following chunk to RetroQuickData.kt to contain model data:

private var modelMap = mutableMapOf<String, ModelData>() 

val models get() = modelMap.values.toList()

fun addModel(model: ModelData) {
    modelMap[model.qualifiedName]?.let {
      it += model
    } ?: run {
      modelMap[model.qualifiedName] = model
    }
}

Now it’s time to extract model data for each annotated model. Add this long method to Processor.kt:

private fun getModelData(elem: Element, annotation: RetroQuick): Boolean {
    val packageName = elemUtils.getPackageOf(elem).toString()  // 1
    val modelName = elem.simpleName.toString()
    val type = annotation.type  //2
    val path = if (annotation.path.isEmpty())  
        modelName.toLowerCase(Locale.ROOT)
    else annotation.path
    val fieldData = elem.enclosedElements.mapNotNull {   //3
      (it as? VariableElement)?.asType()?.let { returnType ->
        FieldData(it.simpleName.toString(), returnType.toString())
      }
    }
    val pathComponentCandidates = "[{](.*?)}".toRegex()   //4
        .findAll(path)
        .map { it.groupValues[1] }
        .toList()
    val pathComponents = mutableListOf<FieldData>()   //5
    for (candidate in pathComponentCandidates) {
      fieldData.firstOrNull { it.paramName == candidate }?.let {
        pathComponents += it
      } ?: run {
        logger.e("Invalid path component: $candidate")
        return false
      }
    }
    val md = ModelData(packageName, modelName,   //6
        listOf(EndpointData(path, type, pathComponents)))
    data.addModel(md)
    return true
  }

There’s a lot going on here! Here’s a breakdown:

  1. The first two lines extract the package and the name of the annotated model class.
  2. These lines get the HTTP verb from the annotation parameter.
  3. These lines get all the child elements for the model class. You filter them to take only properties and store their names and return types to a list. If path is empty, use the model name for the path. Otherwise, take the provided parameter and parse it further.
  4. In the path, use a Regular Expression to find all the variable path components, as IDs within curly braces.
  5. Check if all those path components map to model class properties. If any don’t, print an error and abort processing.
  6. Finally, pack all this data into ModelData and add it to processor-level data.

Last, you’ll get model data. Add the following code to process(), above return true:

roundEnv.getElementsAnnotatedWith(RetroQuick::class.java)
    .forEach {
      if (!getModelData(it, it.getAnnotation(RetroQuick::class.java))) {
            return false
          }
    }
logger.n("All models: ${data.models}")

The code finds all elements annotated with @RetroQuick, extracts model data from them and then prints a note about their structure.

That’s it. Build the project and check the Build output window. You’ll see the processor collected the necessary data!

The Build output window with all model

Repeatable Annotations

Your annotation works as expected now, but it can only capture a single endpoint with a single path. What if you wanted to add more endpoints?

You could modify the type and path parameters to accept arrays instead of single values, but that’d be clunky for both you and the processor. Besides, annotations should be declarative as well as easy to read and maintain. Why not repeat the annotation then?

Add another @RetroQuick above Person:

@RetroQuick(path="person/{id}", type = RetroQuick.Type.GET)
@RetroQuick
data class Person(

Uh, oh! It throws an error:

The error for duplicated RetroQuick annotation

Well, that looks simple enough to fix.

Go to RetroQuick.kt. Add @Repeatable below @Retention(AnnotationRetention.SOURCE). Your code will look like this:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@Repeatable // ADD THIS
annotation class RetroQuick(

Go back to Person.kt to make sure the error went away. Then, rebuild the project.

The error went away, but the Build output window shows that now the processor isn’t picking your annotations:

The Build output window presents a note with an empty list of models

What’s the deal with that?

@Repeatable takes care of the Kotlin compiler side of things by telling it that it’s fine for that annotation to be present multiple times on the same code element. But, annotation processing is essentially a JVM affair, meaning you need to put @Repeatable‘s Java twin, @java.lang.annotation.Repeatable, to use as well.

There are two steps to make repeatable annotations work:

  1. Create a container annotation for repeatable annotations.
  2. Let the processor know about it.

First, in RetroQuick.kt, declare a new annotation class above @Target(AnnotationTarget.CLASS):

annotation class RetroQuicks(val value: Array<RetroQuick>)

Then, annotate RetroQuick with @java.lang.Repeatable, passing the container annotation class RetroQuicks as the parameter:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@Repeatable
@java.lang.annotation.Repeatable(RetroQuicks::class) // ADD THIS
annotation class RetroQuick(
Note: You’ll see a warning that @java.lang.annotation.Repeatable(RetroQuicks::class) is deprecated, but you need it to continue with the tutorial.

You finished the first part! Now, open Processor.kt and add RetroQuicks to the list of supported annotations:

override fun getSupportedAnnotationTypes() = mutableSetOf(
  RetrofitProvider::class.java.canonicalName,
  RetroQuick::class.java.canonicalName,
  RetroQuicks::class.java.canonicalName // ADD THIS
)

This block instructs the processor to look for and parse RetroQuicks together with RetroQuick and RetrofitProvider.

Then, in process(), add this code above logger.n("All models: ${data.models}"):

roundEnv.getElementsAnnotatedWith(RetroQuicks::class.java)
     .forEach { elem ->
        elem.getAnnotation(RetroQuicks::class.java).value
           .forEach {
              if (!getModelData(elem, it)) {
                return false
              }
           }
     }

Basically, this is the same as the statement that parsed individual @RetroQuick-annotated elements, but expanded to do so for batches of @RetroQuick stored in @RetroQuicks.value.

Note: You need to keep the parsing for both @RetroQuick and @RetroQuicks to handle single and repeated annotations.

Build the project again. Open the Build output window. This time, the processor picks up your repeated annotations and extracts model data for both of them:

The Build output window with repeated model data

Now it’s time to use all the ModelData to generate the actual code.

Code Generation

As in the Annotations: Supercharge Your Development tutorial, you’ll use KotlinPoet for this task.

First, create a new package in retroquick-processor and name it codegen. Then create a new file and name it CodeBuilder.kt. In CodeBuilder.kt add:

class CodeBuilder( // 1
    private val providerName: String,
    private val className: String,
    private val data: ModelData) { 

    private val modelClassName = ClassName(data.packageName, data.modelName) // 2 

    fun build(): TypeSpec = TypeSpec.objectBuilder(className) // 3
      .build()
}

Here you:

  1. Declare CodeBuilder that takes three parameters: the function annotated with @RetrofitProvider, the name of the class to be generated and its model data.
  2. Store a ClassName of the model class for future use.
  3. Declare build that creates a new object.

Now it’s time to add some helper methods to CodeBuilder.kt that’ll make code generation easier.

A Few Helper Methods

First, add typeName() below build():

private fun typeName(returnType: String): TypeName {
  return when (returnType) {
    "int" -> INT
    else -> ClassName(returnType.substring(0, returnType.lastIndexOf('.')),
        returnType.substring(returnType.lastIndexOf('.') + 1))
 }
}

This method maps the property type to something KotlinPoet can work with. Currently, this part only supports Int as a primitive type, although extending it for other primitives is trivial.

Second, add funName() above typeName():

private fun funName(type: RetroQuick.Type): String {
  return when (type) {
    RetroQuick.Type.GET -> "get"
    RetroQuick.Type.POST -> "post"
  }
}

This method maps RetroQuick.Type to their internal strings.

Now it’s time for even more helper methods. Who doesn’t like that? :] This front-loading will pay off in a few minutes.

Add addRetrofitAnnotation() above funName():

private fun FunSpec.Builder.addRetrofitAnnotation(name: String, path: String)
    : FunSpec.Builder =  apply {
	addAnnotation(AnnotationSpec.builder(ClassName("retrofit2.http", name))
	  .addMember("%S", path)
	  .build()
	)
}

This code annotates a function or a method with @GET or @POST, the two annotations Retrofit2 uses to generate call code.

Finally, add the last helper method, addParams():

private fun FunSpec.Builder.addParams( // 1
      pathComponents: List<FieldData>,
      annotate: Boolean,
      addBody: Boolean
  ): FunSpec.Builder =  apply {
    for (component in pathComponents) { // 2
      val paramSpec = ParameterSpec.builder(component.paramName, typeName(component.returnType))
      if (annotate) { // 2
        paramSpec.addAnnotation(AnnotationSpec.builder(
            ClassName("retrofit2.http", "Path"))
          .addMember("%S", component.paramName)
          .build()
        )
      }
      addParameter(paramSpec.build())
    }
    if (addBody) { // 3
      val paramSpec = ParameterSpec.builder("body", modelClassName)
      if (annotate) { // 3
        paramSpec.addAnnotation(AnnotationSpec.builder(
            ClassName("retrofit2.http", "Body"))
          .build()
        )
      }
      addParameter(paramSpec.build())
    }
  }

This method:

  1. Adds parameters to a new function or method. It has three parameters:
    1. pathComponents are parameters that are passed to the endpoint’s path.
    2. annotate is a flag that tells if the parameters should be annotated. It discerns between a method declaration in a Service or in the call implementation.
    3. addBody is a flag that tells if the method has a parameter representing the body of the HTTP request. This is true for POST requests and false for GET.
  2. Creates a Retrofit2 annotation, such as @GET, and passes the path as its parameter.
  3. Adds the body parameter, and annotates it with @Body if necessary.

Next, you’ll add service interface.

Adding Service Interface

The Retrofit call has two components: the service that describes the call and actual call execution.

First, tell the code generator how to build the service part. Add the following code to CodeBuilder.kt:

private fun TypeSpec.Builder.addService(): TypeSpec.Builder = apply {
  val serviceBuilder = TypeSpec.interfaceBuilder("Service") // 1
  for (endpoint in data.endpointData) { // 2
    val name = funName(endpoint.type)
    serviceBuilder.addFunction(FunSpec.builder(name) // 3
        .addModifiers(KModifier.SUSPEND, KModifier.ABSTRACT) // 3
	.addRetrofitAnnotation(name.toUpperCase(Locale.ROOT), endpoint.path)// 4
	.addParams(endpoint.pathComponents, annotate = true, 
             addBody = endpoint.type == RetroQuick.Type.POST) // 5
	.returns(ClassName("retrofit2", "Response") // 6
	    .parameterizedBy(modelClassName.copy(true))) // 6
	.build()
    )
  }
  addType(serviceBuilder.build())
}

This code:

  1. Creates a new interface named Service.
  2. Gets each endpoint with a separate method.
  3. Gets the name for the endpoint function that derived from its HTTP verb. It’s a suspend. KModifier.ABSTRACT is necessary for all interface methods.
  4. Employs addRetrofitAnnotation() as Retrofit2 requires annotating service methods.
  5. Similarly, uses addParams to add the parameters.
  6. Each of these methods returns a retrofit2.Response, with the model class name as generic parameter. copy(true) makes the parameter nullable.

Now, you’ll add call invocations.

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:

  1. Generates suspend for each endpoint with the name equal to the HTTP verb.
  2. The parameters are the same as for the equivalent Service method, except they aren’t annotated. The POST has a body, while the GET doesn’t.
  3. The method returns an optional type of model class.
  4. Creates the service by statically invoking the function annotated with @RetrofitProvider.
  5. Creates the call by invoking the newly created service with the provided name and parameters.
  6. 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.

The screen with test buttons and text reponses 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!

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments