Retrofit meets coroutines
2019-02-15 • 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...
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.
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.
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.