Error handling is important, but if it obscures logic, it’s wrong
Custom callAdapter in retrofit allows us to filter out various API error responses at a centralized level which reduces boilerplate code effectively. We will create our own Retrofit callAdapter to handle the API call Success and Error states.
We will use Kotlin Result API consistently in the whole application as a response of API calls.
At the end of this article, we will be able to simply error handling. (Obviously using Kotlin Result API):
apiService.getMovies().onSuccess {
movieListResponse = it
movieState.emit(MovieState.SUCCESS)
}
.onFailure {
movieState.emit(MovieState.FAILURE(it.localizedMessage))
}
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
When the OkHttp client receives a response from the server, it passes the response back to the Retrofit. Retrofit then pushes the meaningless response bytes through converters and wraps them into response with meaningful Java objects. This process is still done on a background thread. Lastly, when everything is ready Retrofit needs to return the result to the UI thread of app. This process of returning from the background thread, which receives and prepares the result to the Android UI thread is a Call Adapter.
To make retrofit use a Callback<T> converting the possible success/errors cases to the Result type we need to wrap the callback in a callAdapter
and pass Retrofit ResultCallAdapterFactory capable of returning this adapter.
Our example is using coroutines. It is defined using suspend functions returning the Kotlin Result API type.
interface ApiService {
@GET("movielist.json")
suspend fun getMovies(): Result<List<Movie>>
}
Kotlin Result, a type in the Kotlin standard library that is effectively a discriminated union between the successful and failed outcome of the execution of Kotlin function.
In order to make Retrofit return Result
as a response when movie()
API is called we need to have CallAdapter. First, we will implement the Call interface from Retrofit.
enqueue
method basically takes care of Asynchronously sending the request and notifying the callback of its response or if an error occurred talking to the server, creating the request, or processing the response.
enqueue
method has a callback which has two methods:
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
callback.onResponse(
this@ResultCall,
Response.success(
response.code(),
Result.success(response.body()!!)
)
)
} else {
callback.onResponse(
this@ResultCall,
Response.success(
Result.failure(
HttpException(response)
)
)
)
}
}
onResponse
: This callback basically deals with an HTTP response, but the response may be a success (2xx) or a failure one. So we have to check if the response is successful, we will return success
the state of our Result API, otherwise, it will return failure
state.
override fun onFailure(call: Call<T>, t: Throwable) {
val errorMessage = when (t) {
is IOException -> "No internet connection"
is HttpException -> "Something went wrong!"
else -> t.localizedMessage
}
callback.onResponse(
this@ResultCall,
Response.success(Result.failure(RuntimeException(errorMessage, t)))
)
}
onFailure
: This callback deals with exceptions related to the network when talking to the server or when any exceptions occurred at the time of creating the request or processing the response.
One thing here to notice, is we have wrapped our failure result in Response success which means now our upstream never throw any exceptions as we always considered it as a success response, but we still receive errors in our onFailure
callback of Result.
As the remaining methods of the Call interface are simple, we will delegate them to the original call.
Here’s the Gist of our schema implementation.
Basically for creating CallAdapter we need to implement only two methods:
val upperBound = getParameterUpperBound(0, returnType)
return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) {
object : CallAdapter<Any, Call<Result<*>>> {
override fun responseType(): Type = getParameterUpperBound(0, upperBound)
override fun adapt(call: Call<Any>): Call<Result<*>> =
ResultCall(call) as Call<Result<*>>
}
} else {
null
}
Here, getParameterUpperBound
will help us get success/error types from the parameterized type ApiResponse.
Our CallAdapter will filter out if the response that we received is Actually Result
API response and if so it will do ResultCall otherwise simply return null.
CallAdapter.Factory
has one abstract method that we need to implement.
get: get method returns a call adapter for interface methods that return returnType, or null if it cannot be handled by this factory.
class ResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) {
return null
}
val upperBound = getParameterUpperBound(0, returnType)
return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) {
object : CallAdapter<Any, Call<Result<*>>> {
override fun responseType(): Type = getParameterUpperBound(0, upperBound)
override fun adapt(call: Call<Any>): Call<Result<*>> =
ResultCall(call) as Call<Result<*>>
}
} else {
null
}
}
}
In simpler terms get
method in our ResultCallAdapterFactory
will check if the return type is our Kotlin Result class for the API response if so it will handle it otherwise simply return null.
For that, we need to add our custom ResultCallAdapter Factory to retrofit at the time of initializing it.
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://howtodoandroid.com/apis/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ResultCallAdapterFactory())
.build()
}
That’s it.
Didn’t get all the stuff, nothing to worry about!
You can find the sample project here on github. (It assumes you have a basic idea of Hilt, Coroutines, Jetpack compose)