Retrofit Error Handling Simplified with Kotlin Coroutines

Master effective error handling in Retrofit with Kotlin Coroutine and Result API.
Feb 16 2022 · 4 min read

Introduction 

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!

How does CallAdapter work?

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.

Kotlin Result API

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.

Result Call Implementation

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.

Creating CallAdapter

Basically for creating CallAdapter we need to implement only two methods:

  1. responseType: It returns the value type that this adapter uses when converting the HTTP response body to a Java object.
  2. adapt: It returns an instance of T which delegates to call, here we will use our Result Call that we already implemented.

  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.

Create ResultCallAdapter Factory

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.

Make retrofit aware of our Call Adapter

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.

Sample Project

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)


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
canopas-logo
We build products that customers can't help but love!
Get in touch

Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Follow us on
2024 Canopas Software LLP. All rights reserved.