Create Retrofit CallAdapter for Coroutines to handle response as states

In the past, we used to use JakeWharton/retrofit2-kotlin-coroutines-adapter in order to use Retrofit with Coroutines.
But since Retrofit supports the suspend
modifier on functions for Kotlin now, we don’t have to do this anymore.
See Retrofit version 2.6.0 (2019–06–05)
So now using Retrofit with Coroutines is as simple as this
@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User
Enough talking about using Retrofit and Coroutines because there are tons of articles about this and let’s talk about error handling.
In this article I will show how can we create our own Retrofit CallAdapter to handle the API calls errors and success states.
By the end of the article you should be able to do the following:
Create Network Response Sealed Class
First, let’s model our responses states by creating a sealed class that represents the API call response states.
Most probably we need 4 states:
Success
which is a data class that should contain the body of the success state of the request.ApiError
which represents the non-2xx responses, it also contains the error body and the response status code.NetworkError
which represents network failure such as no internet connection cases.UnknownError
which represents unexpected exceptions occurred creating the request or processing the response, for example parsing issues.
Create our own Call transformer
In order to make Retrofit return NetworkResponse
when topic()
API call is triggered, we need to write a custom CallAdapter
First step to create our own CallAdapter
is implementing Call
interface from Retrofit
The method that has most of the logic in our implementation of the Call
interface is enqueue
.
What is the enqueue
method?
Asynchronously send the request and notify callback of its response or if an error occurred talking to the server, creating the request, or processing the response.
So we will implement enqueue
method and check the response, then return the correct callback.
enqueue
takes a callback which has two methods to implement:
onResponse
: which is invoked for a received HTTP response, this response could be success response or failure one.
So we have to check here if the response is successful, we return the success state of ourNetworkResponse
sealed class
If it’s not a success response, we try to parse the error body as the expected error data class we provide as a type, if the parse succeeded we return the error asApiError
state, otherwise it’sUnknownError
.onFailure
: which is invoked when a network exception occurred talking to the server or when an unexpected exception occurred creating the request or processing the response.
Here we can simply check if the exception isIOException
then we return theNetworkError
state, otherwise it should beUnknownError
state.
enqueue method
The rest of the methods of Call
interface are simple, we will just delegate them to the original call.
So the our Call
implementation should look like this
Create your own CallAdapter
Now it’s time to create our CallAdapter
Creating CallAdapter
is very straight forward, we will need to implement only two methods
responseType
Returns the value type that this adapter uses when converting the HTTP response body to a Java object
adapt
Returns an instance of T which delegates to call, here we will use our NetworkResponseCall
that we just created.
Create your own CallAdapter Factory
Next step is creating our CallAdapter.Factory
CallAdapter.Factory
has only one abstract method that we should implement which is get
.
get
method in the CallAdapter.Factory
should return a callback adapter for interface methods that it could handle or null if it’s can’t be handled by this factory.
So simply our get
method in our custom CallAdapter.Factory
should check if the returnType
is our sealed class for the API response calls, and then handle it.
If the caller isn’t asking for our Sealed Class, return null, this isn’t the right adapter.
What does this mean?
But there is something we need to know first about how suspend
functions work with Retrofit.
@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User
Behind the scenes this behaves as if defined as fun user(...): Call<User>
and then invoked with Call.enqueue
So this means when we have a suspend function like this
suspend fun user(): ApiResponse<User, Error>
It’s actually
fun user(): Call<ApiResponse<User, Error>>
So after removing the Call
type we have to make sure that the inner type is ApiResponse
Now we reached a point, where we have the response as ApiResponse<Success, Error>
Next, we need extract the success and error types from the ApiResponse
parameterized type.
The base CallAdapter.Factory
in Retrofit has a function named getParameterUpperBound
which should help us getting the success/error types from the parameterized type ApiResponse
Then get the error converter and return
Make retrofit aware of your Call Adapter
Finally you need to add our custom CallAdapterFactory to Retrofit while initializing it
Protip:
If you happen to have a generic error model that your API uses for most/all of the endpoints you can save yourself sometimes and code by creating a typealias
that wraps this generic error model and the success type like this
typealias GenericResponse<S> = NetworkResponse<S, GenericApiError>
Sample Project:
I created a sample App, you can check it out on GitHub