Custom retrofit2 annotations

This is an updated article of my article from 2016 on medium

I recently wrote an updated version of How to write a custom retrofit annotation.

In some cases it would be nice to have custom annotation support in retrofit2.
A year ago I started to write a library, which requires this.
In this article I will explain how to achieve and implement this.

Why do you need custom annotations?

I guess in most cases you don’t need to use custom annotations, but interceptors. Since retrofit2 is based on okhttp, you can use interceptors (in retrofit 1.X this was supported by retrofit itself, too). But what does it do? Well, basically every request you call (using retrofit2 or plain okhttp) calls all setup interceptors before executing requests.
The Interceptor is an interface which contains only one method to implement, called (surprise ^^) intercept, which get’s an object of Chain to proceed.

How to use interceptors?

You can use Chain to execute

1
return chain.proceed(chain.request()) // returns Response

or modify request and execute

1
2
3
4
5
val request = chain.request().newBuilder()
// modify the request as you need it
...
.build()
chain.proceed(request)

the next upcoming request.
This setup applies for ALL requests that run with the okhttp client in which you setup these interceptors.

Sometimes this is not enough, especially when you have runtime requirements for the modifications of the request (e.g. an authorize token for the headers)

Why did I come up with this?

At the droidcon Berlin 2015 I was thinking about a solution how to combine the android account manager and retrofit (1.9 back in the time). Most of the requests are authenticated ones, meaning they need to contain some kind of authorization token in the header. One could say now:

Why not just use interceptors for this and add it to the header.

Yes, you could do this. But there are many more questions raising up, such as:

  • Do I need authentication on every call?
  • What if I need different token types?
  • What if the user isn’t logged in and there’s no token at all? Do I want to fire the request?
  • Is the token still valid?
  • Do I need to bother the user reentering his credentials or can I refresh the token automatically?

However, I decided to write an annotation can handles all this cases for requests that are annotated with it. So that the developer only can concentrate on defining the request itself, instead of handling all these cases in probably each case of a request.

1
2
3
@Authenticated
@GET("/some/path")
fun someCall(): Call<MyResponseObject>

The resulting library is called retroauth have a look.

Implementing a custom annotations

Defining the annotation

I guess this is the easiest part, just define your annotation as you like it. In my case I needed an annotation that can
take some Information.

1
2
3
4
5
@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
@Retention(RUNTIME)
annotation class SomeAnnotation(
val value: String
)

Linking the request with the annotation

Now we need to read the annotation and pass containing data (in our example above the value: String) to to an Interceptor in which
we can modify using this data.
So we need some map/registry that can store a request identifier (any kind of identifier, which can identify a request by it’s object) and some informational data, we got from the annotation.
Since requests are re-created, we cannot use the request object itself, but an Integer(hash) will do the job.
It turned out to be a good working practice to use:

1
2
3
4
internal object RequestIdentifier {
@JvmStatic
fun identify(request: Request) = request.url.hashCode() + 31 * request.method.hashCode()
}

to create the request Identifier.

Additionally, we need an Interceptor that reads this map/registry and applies your modifications on the request.

graph describing the relation between the map/registry the CallAdapter and the Interceptor

In order to fill this map/registry with information, we need to grab the request right after it has been created. The
only place of reading the annotations is the CallAdapter.
Since we don’t know which CallAdapter will be used (remember, there are several ones and they may differ from request to request),
the only thing we can do is:

  • getting the CallAdapter that would’ve been chosen
  • wrapping it and pretend we’re this CallAdapter

With that, we proxy the CallAdapter in which we then, get the information for the annotation out.

CallAdapters are created by a CallAdapter.Factory which is both part of retrofit.

CallAdapter.Factory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class SomeCustomAdapterFactory(
private val registration: MutableMap<Int, String> = mutableMapOf()
) : CallAdapter.Factory() {

private fun isAnnotated(annotations: Array<Annotation>): SomeAnnotation? {
for (annotation in annotations) {
if (SomeAnnotation::class == annotation.annotationClass) return annotation as SomeAnnotation
}
return null
}

@Suppress("UNCHECKED_CAST")
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
val annotation = isAnnotated(annotations)
// getting all calladapters except this one
val callAdapterFactories = retrofit.callAdapterFactories().filterNot { it is SomeCustomAdapterFactory }
// iterating through them in order to find the one which would be used normally
for (i in callAdapterFactories.indices) {
// try getting the calladapter which would be used normally
val adapter = callAdapterFactories[i].get(returnType, annotations, retrofit)
if (adapter != null) {
// adapter for return type found
if (annotation != null) {
// if the reques was annotated
return ProxyCallAdapter(
adapter as CallAdapter<Any, Any>,
registration,
annotation.value
)
}
return adapter
}
}
return null
}
}

When retrofit calls the CallAdapter.Factory.get method, we check if the requested method contains an annotation.
If so, we return our wrapped ProxyCallAdapter. If not, we return the CallAdapter found for the type.

ProxyCallAdapter

And here the simple ProxyCallAdapter that doesn’t do anything special, but records the request and it’s annotation
content into our map/registry. Everything else is Proxy functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
class ProxyCallAdapter<RETURN_TYPE : Any>(
private val adapter: CallAdapter<Any, RETURN_TYPE>,
private val registration: MutableMap<Int, String>,
private val info: String
) : CallAdapter<Any, RETURN_TYPE> {

override fun responseType(): Type = adapter.responseType()

override fun adapt(call: Call<Any>): RETURN_TYPE {
registration[RequestIdentifier.identify(call.request())] = info
return adapter.adapt(call)
}
}

Manipulating the request

We still need to implement the Interceptor, which takes the information and manipulate the request. This now turns
out to be easy.

1
2
3
4
5
6
7
8
9
10
11
12
class SomeCustomInterceptor(private val registration: MutableMap<Int, String>) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val annotationContent = registration[RequestIdentifier.identify(request)]
if(annotationContent != null) {
request = chain.request().newBuilder()
.addHeader("SomeCustomHeader", annotationContent)
.build()
}
return chain.proceed(request)
}
}

When intercepting the request we check if there was any information created for this particular request (using the map).
If so, we modify the request as we need it, if not we just execute it.

How retrofit decides on the CallAdapter.Factory

All this would work nice already, if we can make sure that retrofit is using our SomeCustomAdapterFactory, instead
of any other applied one. E.g. you want to use RxJava3CallAdapter:

1
2
3
4
5
6
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.addCallAdapterFactory(SomeCustomAdapterFactory())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
...
.build()

How do we know that our SomeCustomAdapterFactory is used? Taking a look at the retrofits implementation tells us
that retrofit is iterating through all available ones. The first one returning a non-null value, “wins”.

So we need to make sure, that SomeCustomAdapterFactory is the first one applied, when building the retrofit instance.

Wrap up

This is a sample on how you would build your retrofit instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val map: MutableMap<Int, String> = mutableMapOf()
val factory = SomeCustomAdapterFactory(map)
val interceptor = SomeCustomInterceptor(map)
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.client(
OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
)
.addCallAdapterFactory(SomeCustomAdapterFactory()) // first one!
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
...
.build()

I recommend creating your own retrofit Builder to ensure,
the order and the addition of the interceptor to the client.

Summary

In this article we discovered how to implement a custom annotation for retrofit2.