Image by Ich bin dann mal raus hier. from Pixabay

Retrofit CallAdapter for Either type

Lukasz Kalnik
ProAndroidDev
Published in
3 min readJun 18, 2020

--

The Arrow.kt Either type is very practical for handling domain errors in the app.

What if we could get the type returned directly from our REST API calls made with Retrofit and use it consistently in the whole application?

Retrofit gives us two kinds of errors:

  1. Http errors, i.e. responses with Http codes 4xx-5xx (client/server errors). They are returned in the same onResponse() callback as successful responses (with codes 2xx). They can be distinguished from each other by checking Response.isSuccessful() .
  2. Network errors (network not available etc.) which are returned in the onFailure() callback as IOExceptions.

To make Retrofit use a custom Callback<T> converting the possible success/error cases to the Either type we need to wrap the callback in a CallAdapter and pass Retrofit a CallAdapterFactory capable of returning this adapter. Let’s see how we can do it.

First, let’s create a sealed class hierarchy of possible error cases.

HttpError will contain the error code (4xx-5xx) and body of a possible Http error response.

NetworkError will contain possible IOException in case of lack of connectivity and similar errors.

UnknownApiError is for all unexpected types of errors.

Our example Retrofit API is using coroutines. It is defined using suspend functions returning the anticipated Either type.

The first Either type argument is always our ApiError sealed hierarchy, the second type argument is the expected domain type (by convention in Either error is associated with the left side and success with the right).

When our Retrofit API is defined using suspend functions, under the hood Retrofit is wrapping our API calls in its Call<T> type. See how it is implemented in Retrofit Kotlin extensions.

Because of that, our CallAdapter will have to convert the original Call to our special EitherCall which does all the wrapping in the Either type.

You can find the ready implementation in my example Moovis project. Let’s go through it step by step.

First, we need to create the EitherCall class extending Call with the type argument Either<ApiError, R>. It will take the original call as delegate and delegate all the standard work to it which we don’t want to customize. It will also take the expected successType to check if it is a Unit (this is useful for responses like 204 No content).

The only non-trivial method we are implementing is enqueue() . There we pass our callback implementing the onResponse() and onFailure() methods.

Note that the data is wrapped into a synthetic Response.success() call. This is the only way to return success/failure cases consistently in the same Either data type from Retrofit. Also note that all the onResponse() logic of checking if response is successful, if it had a body etc. is moved to a private extension function Response.toEither().

The onFailure() callback just checks if the error is an IOException (i.e. a network problem on the client) or an unknown error.

This was already actually the hardest part of our work!

The next step is to create an EitherCallAdapter extending CallAdapter. It has our successType as property and only implements two methods: adapt() which wraps the original Call into our EitherCall and responseType() which simply returns the passed successType.

In order to pass the newly created EitherCallAdapter to your Retrofit instance, you need to create an EitherCallAdapterFactory extending CallAdapter.Factory. There the overridden get() method performs several checks and returns null if it thinks the expected return type is not compatible with our EitherCallAdapter or otherwise an adapter instance with the expected success type.

As you see above, first the raw return type is checked if it is a Call and also if it is a parameterized (generic) type.

If yes, its type parameter upper bound on position 0 has to be Either and it also has to be parameterized.

Again we check Either type parameter upper bound on position 0 (left type). It has to be ApiError (we only support this type).

The type on position 1 is the rightType which we pass to EitherCallAdapter as the expected successType.

The last step is passing the EitherCallAdapterFactory to your Retrofit instance.

You can now call your API methods like this out of the box:

Like mentioned above, the whole example implementation you can find in my sample Moovis project.

--

--

Android Developer making the Internet of Things accessible @grandcentrix