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 | val request = chain.request().newBuilder() |
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 |
|
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 |
|
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 | internal object RequestIdentifier { |
to create the request Identifier.
Additionally, we need an Interceptor that reads this map/registry and applies your modifications on the request.
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.
CallAdapter
s are created by a CallAdapter.Factory
which is both part of retrofit
.
CallAdapter.Factory
1 | class SomeCustomAdapterFactory( |
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 | class ProxyCallAdapter<RETURN_TYPE : Any>( |
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 | class SomeCustomInterceptor(private val registration: MutableMap<Int, String>) : Interceptor { |
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 | val retrofit = Retrofit.Builder() |
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 | val map: MutableMap<Int, String> = mutableMapOf() |
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.