Custom Annotations with Retrofit 2
In some cases it would be nice to have custom annotation support in retrofit2. A year ago I started to write a library in which I needed this feature. 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 Interceptor?
You can use Chain to execute
return chain.proceed(chain.request()); // returns Response
or modify request and execute
Request request = chain.request().newBuilder()
// modify the request as you need it
the next upcoming request.
This setup applies for ALL requests that run with the okhttp client in which you setup these interceptors.
Well, sometimes this is not enough, especially when you have runtime requirements for the modifications of the request.
Why did I came up with this?
Last year at droidcon Berlin 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 requests, meaning that they need to contain some kind of authentication token in the header. One would say now:
Well, just use interceptors for this and add it to the header.
I wanted to move this a step further. What if there are multiple different Token-Types (i.e. one for read-only, others for administration)? What if the user is not logged in yet? What if the token is not valid anymore? What if the token can be refreshed?
However, I decided to write an annotation that covers all this cases, so that the developer only needs to concentrate on defining the request itself, instead of handling all these cases in probably each case of a request. So in the end I wanted to add annotations like this
which does the following:
- Check if there is a user for my given account type
If not: open Login
If there are multiple: let the user choose which one to use
- check if the account has an existent authentication token
If not: open Login
- Add the Token to the request
- Call the request
- Check the result for 401 (this usually happens when the token expires)
If so, search for a refresh token and try to refresh. Continue with step 2
If there’s no refresh token: open Login
The Library is called retroauth (since it’s an authentication library based on retrofit) and you can find the current snapshot that supports retrofit2 on the jcenter snapshot repository.
Implement custom annotations
I will show you how I achieved this using retrofit2.
Defining the annotation
Well, 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 arguments, such as Account-Type and Token-Type. To make this as generic as possible, I decided to use a String array
Since we’re not able to intercept the request creation to modify it there (a couple of months ago, I created a PR on retrofit2 to do so, but it got declined and moved to some point in the future), we have to do a workaround. We read the annotation in the CallAdapter.Factory and when the request gets created in the CallAdapter, we will store some information for this kind of request within some map, to identify it later in some interceptor.
So we need some map that can store a request identifier and some information. As an identifier I chose an Integer and the information-part should be covered by a String just for simplicity.
Additionally we need an Interceptor that reads this map and applies modifications on the request.
Other than that, a wrapping CallAdapter is required. This CallAdapter reads the freshly created request and creates an identifier, which will be read in the previously created Interceptor, when the request is executed.
The Constructor arguments are the actual CallAdapter, the request identifier map described earlier. In case of retroauth, I used for Android some kind of token type, that includes the account and the token type. In the example we’ll use a simple String.
Now we need a factory to create this kind of CallAdapters.
This Factory takes a list of CallAdapter factories (we will come to that point later) and the map we created before.
We still need the Interceptor, which could look like this:
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.
Last but not least, we need the default CallAdapter.Factory from retrofit itself. Retrofit provides a default Factory, which will be applied, when there’s no other factories setup (in case of using Call<?> as response type). It’s created by retrofit itself and is package private. To workaround this, you can create a package called retrofit2 in your src folder and create a helper class which hands out whatever you need. For this case we only need the defaultCallAdapterFactory.
Merge it all together
So at this point, we should have a custom CallAdapter.Factory that can create a custom CallAdapter, a custom Interceptor and the retrofit2 Helper class Retrofit2Platform for the default CallAdapter.Factory.
So first, we create our request identifier map and a list of CallAdapter.Factory. This list must contain at least the retrofit default adapter in order to work with your custom annotations. Having those available we can create an instance of our custom CallAdapter.Factory, the Interceptor, the OkHttpClient and in the end the retrofit object.
To not let the actual developer do this, I created a wrapper class in the library for the Retrofit.Builder, which takes care of this.
In this article we discovered how to implement a custom annotation for retrofit2. There are still a couple of pain points. Especially the identify method’s I’ve been using may vary for your purpose. Let’s hope there will be a better solution soon.
I’d like to hear your opinion and suggestions.
For a full implementation have a look at the feature branch of my authentication library, which is still under development. In particular the core project, which should work for java only, too.
Here’s the project:
retroauth - A library build on top of retrofit, for simple handling of authenticated requestsgithub.com
Leave me a tweet, if you like.