DEV Community

loading...

Making safe API calls with Retrofit and Coroutines

eagskunst profile image Emmanuel Guerra ・3 min read

After the 2.6.0 release, Retrofit has official support for coroutines. The migration from other adapters (like RxJava) it simple. Just change your interface from:

@GET("movie/{category}")
fun getMovieListForCategory(@Path("category") category: String) : Single<MovieListResponse>

to:

@GET("movie/{category}")
suspend fun getMovieListForCategory(@Path("category") category: String) : MovieListResponse

An make your call within a CoroutineContext:

GlobalScope.launch {
   val response = movieListApi.getMovieListForCategory(category)
}

You can see that instead of using a wrapper around the object, you receive the object itself, which it's pretty helpful but has one disanventage: this does not provide a way to determine an error.

Of course you could use the classic try-catch, but writting this for every single call is not optimal and it's a bad solution in the long term. Also, who wants to write a try-catch for every request?

That's why I'm showing you the solution I made for my work and personal projects!

First of all, let's define the problems we have:

  1. We need to make safe calls, so every call needs to be around a try-catch block.
  2. We need it to work for every call.
  3. We need to return something in case the call fails
  4. We need to somehow emit an error or a message if the call fails.

For the first two problems, we can create an static class or an abstract class that handles the calls. But how do we know what to return?
Since every object is different and we are trying to find a solution for every one, we could use a generic function.

suspend fun <T> safeApiCall() : T

We have now what will be our function. The problem we face now: how to exactly make it work for every interface?
After a lot of thinking, reading from the Retrofit repository and articles in the internet, I remember the powerfulness of Kotlin's higher order functions!
Passing a function as a parameter make the solution work for any interface.
So now, we have this:

suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T

Now, the only thing we need to do is to call responseFunction around a try-catch and return it as a parameter. But now we face the 3rd problem. The solution is simple: return a nullable object instead of the non-nullable:

suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T?{
    return try{
            responseFunction.invoke()//Or responseFunction()
           }catch (e: Exception){
            e.printStackTrace()
            null
           }
}

Done! We have the function that will handle every single call safely. Now, let's see what optimizations we can do and how to inform other classes about the error.

We will handle 3 types of error: HttpExceptions, SocketTimeoutException and IOExceptions. For this, we will create an enum for easy handling. You could also use Int values if you want to.

enum class ErrorType {
    NETWORK, // IO
    TIMEOUT, // Socket
    UNKNOWN //Anything else
}

For the HttpExceptions, let's create a function that gets the error message from the API. Here, you need to check what kind of JSON object use the API for error response and the error codes.

companion object {
    private const val MESSAGE_KEY = "message"
    private const val ERROR_KEY = "error"
}

fun getErrorMessage(responseBody: ResponseBody?): String {
    return try {
        val jsonObject = JSONObject(responseBody!!.string())
        when {
            jsonObject.has(MESSAGE_KEY) -> jsonObject.getString(MESSAGE_KEY)
            jsonObject.has(ERROR_KEY) -> jsonObject.getString(ERROR_KEY)
            else -> "Something wrong happened"
        }
    } catch (e: Exception) {
        "Something wrong happened"
    }
}

Now, we are going to create an interface that will be in charge of notifying the errors to the classes that implement it:

interface RemoteErrorEmitter {
    fun onError(msg: String)
    fun onError(errorType: ErrorType)
}

Finally, let's modify our try-catch block. For optimizations, we would change to coroutine context to Dispatchers.IO when calling responseFunction and changing the context to Dispatchers.Main on the catch block. We use the Main dispatcher so there are no exceptions from modifying the UI thread.

suspend inline fun <T> safeApiCall(emitter: RemoteErrorEmitter, crossinline responseFunction: suspend () -> T): T? {
    return try{
        val response = withContext(Dispatchers.IO){ responseFunction.invoke() }
        response
    }catch (e: Exception){
        withContext(Dispatchers.Main){
            e.printStackTrace()
            Log.e("ApiCalls", "Call error: ${e.localizedMessage}", e.cause)
            when(e){
                is HttpException -> {
                    val body = e.response()?.errorBody()
                    emitter.onError(getErrorMessage(body))
                }
                is SocketTimeoutException -> emitter.onError(ErrorType.TIMEOUT)
                is IOException -> emitter.onError(ErrorType.NETWORK)
                else -> emitter.onError(ErrorType.UNKNOWN)
            }
        }
        null
    }
}

We are finally done! Now you have a class that can handle exception on Http calls make with coroutines.
Here's an example on how to use this from a ViewModel:

class MovieListViewModel(api: MovieListApi): ViewModel(), RemoteErrorEmitter {
  val movieListLiveData = liveData {
      val response = ApiCallsHandler.safeApiCall(this@MovieListViewModel){
          api.getMovieListForCategory("popular")
       }
      emit(response)
  }
  //RemoteErrorEmitter implementation...
}

You can see more examples on a recent app I made:

GitHub logo eagskunst / MoviesApp

An application that show a list of categories, a list of movies,the details of the movie and let the user save it on his own list.

Discussion

pic
Editor guide