
Retrofit CallAdapter for Either type
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:
- 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 checkingResponse.isSuccessful()
. - Network errors (network not available etc.) which are returned in the
onFailure()
callback asIOException
s.
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.