zsmb.coEst. 2017



Retrofit meets coroutines

2019.02.15. 22h • Márton Braun

Jake Wharton’s retrofit2-kotlin-coroutines-adapter has been the go-to solution for bridging the coroutine world with Retrofit for a little while. But now, a much-anticipated PR has finally been merged, officially bringing coroutine support to Retrofit 2. There are a couple different ways make use of this, so let’s look at them all now!

Setup

To get started, you’ll need Retrofit 2.6.0 or later in your dependencies.

dependencies {
    def retrofit_version = '2.6.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
}

In addition to the base library, I’ve also included the Gson converter dependency for serialization.

The API I’ll be using for this short demo is the SpaceX REST API, specifically the /rockets endpoint, which will return a list of rockets that I’ll represent with this simple model:

data class Rocket(
    val id: Int, 
    @SerializedName("rocket_name") 
    val name: String
)

My Retrofit initialization will be fairly trivial, just setting the base URL and the Gson converter:

val retrofit = Retrofit.Builder()
        .baseUrl("https://api.spacexdata.com/v3/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

Call extensions

The simplest way to get started with coroutines is to reuse an API we might have had already, returning Call instances from each method:

interface SpaceXApi {
    @GET("rockets")
    fun getRockets(): Call<List<Rocket>>
}

We can get an implementation of this API from Retrofit using the new reified create extension method which can either make use of type inference, or it can be called with a type parameter:

val api: SpaceXApi = retrofit.create()
val api = retrofit.create<SpaceXApi>()

Awaiting calls

For simplicity’s sake, I’ll start my example coroutines with the plain old runBlocking builder. Inside the coroutine, we can get our Call instance, and then call await() on it!

runBlocking {
    val rockets: List<Rocket> = api.getRockets().await()
    rockets.forEach(::println)
}

This is an extension on Call from the retrofit package, and internally, it uses enqueue to execute the Call asynchronously, returning the results at the suspension point when done. It also provides two very important features:

  • Exception propagation: if an error occurs during the request, an appropriate exception will be thrown from await, which you can catch inside your coroutine:

    runBlocking {
        val rockets: List<Rocket> = try {
            api.getRockets().await()
        } catch (e: Exception) {
            println("Network error :[")
            return@runBlocking
        }
        rockets.forEach(::println)
    }
    
  • Cancellation support: on the other hand, if the coroutine is cancelled, the network call is cancelled immediately as well (in-flight, if possible). This is preferable to the often used practice of completing a call in this situation anyway, and then throwing away its result. And you get it basically for free!

Variants

The await extension used above is defined on the Call<T> type, where T is constrained to be non-nullable. In this scenario, a null body being returned from the call will result in an exception (a stylish KotlinNullPointerException, no less!).

However, there is an another await extension defined on the Call<T?> type, which will instead let null body values be returned at the suspension point, should you have the need to do so.

Finally, if you need to process more than just the body of the result, you can use the awaitResponse function instead, which will return the entire Reponse object:

runBlocking {
    val response: Response<List<Rocket>> = api.getRockets().awaitResponse()
    if (response.code() == 200) {
        response.body()?.forEach(::println)
    }
}

Suspending methods

If we can make changes to our API definition (essentially, if there’s not too much existing code to refactor that uses it while not in a coroutine), we can make our network calls even more Kotlin friendly.

The suspend keyword may now be used for request methods, and as usual with coroutines, the return type of the method in this case can just be the model itself, without any wrappers:

interface SpaceXApi {
    @GET("rockets")
    suspend fun getRockets(): List<Rocket>
}

This results in the following, super clean syntax at the call site:

runBlocking {
    val rockets: List<Rocket> = api.getRockets()
    rockets.forEach(::println)
}

And again, there’s also the option of getting the entire Reponse object by just changing the return type in the interface, while keeping the suspend keyword:

interface SpaceXApi {
    @GET("rockets")
    suspend fun getRockets(): Response<List<Rocket>>
}

This updated interface can now be used like this:

runBlocking {
    val response: Response<List<Rocket>> = api.getRockets()
    if (response.code() == 200) {
        response.body()?.forEach(::println)
    }
}

As for the implementation, these suspending functions will end up delegating to the very same await extensions that we’ve covered above! This means that the same behaviour can be expected, with all the neat cancellation and error handling support.

Conclusion

Retrofit’s new coroutine support works just as expected, and makes it virtually seamless to integrate it in an application that’s powered by coroutines. You can find a repository with all the example code shown above here.



You might also like...

Fragment Lifecycles in the Age of Jetpack

Fragments have... Complicated lifecycles, to say the least. Let's take a look at these, and how they all fit into the world of Jetpack today, with LifecycleOwners, LiveData, and coroutines.

The conflation problem of testing StateFlows

StateFlow behaves as a state holder and a Flow of values at the same time. Due to conflation, a collector of a StateFlow might not receive all values that it holds over time. This article covers what that means for your tests.

Wrap-up 2021

Another year over, a new one's almost begun. Here's a brief summary of what I've done in this one.

@JvmOverloads for Android Views

The @JvmOverloads annotation is a convenience feature in Kotlin for interoperating with Java code, but there is one specific use case on Android where it shouldn’t be used carelessly.