Now to be honest, before I got into the practical side of coroutines I thought there were going to be a lot of changes. However, the process in transferring to coroutines really does involve some of the same concepts that we are already used to dealing with when it comes to reactive flows with RxJava. For example, let’s take a simple RxJava setup for making a network request from one of my apps.
- Define a networking interface for retrofit using an Rx friendly retrofit adapter. The functions here will be returning components from the Rx framework — such as Single or Observable
- Call a defined function from another class (such as a repository or an activity for example sake, depending on your setup)
- Define the threading configuration for how the subscription is to be subscribed and observed
- Keep a reference to the subscription so that it can be disposed of
- Subscribe to the stream of events
- Dispose of the subscription during specific lifecycle events
When working with Rx, if we disregard the mapping functions and other data manipulation details then this is a common flow that we will operate. When it comes to coroutines, the flow isn’t really too different. The concepts are the same, but the terminology just changes slightly.
- Define a networking interface for retrofit using a Coroutines friendly retrofit adapter. The functions here will be returning Deferred components from the coroutines API
- Call a defined function from another class (such as a repository or an activity for example sake, depending on your setup). One difference is that this function must be labelled as suspending because
- Define the dispatcher to be used for the coroutine
- Keep a reference to the Job so that it can be disposed of
- Run the coroutine in the desired fashion
- Cancel the coroutine during specific lifecycle events
As you can see from the above flows, the process for executing Rx and coroutine flows are very similar. Regardless of the implementation details, this means that we can keep the same approach which we currently have in place — we just need to swap out a few things to make our implementation coroutine friendly.
So the first step that we need to take here is configure our retrofit setup to allow the return of what are known as a Deferred. This Deferred type is a non-blocking future that can be cancelled if required, this essentially represents a coroutine Job which contains a value for the corresponding work. Using a Deferred type allows us to incorporate the same ideas as a Job, with the addition of being able to retrieve extra states such as the success and failure of the Job — which makes it perfect for network requests.
If you’re using retrofit and RxJava, chances are that you’re using an RxJava Call Adapter Factory — luckily for us there is an equivalent for coroutines:
https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter
We can then add this call adapter to our Retrofit Builder process and then we will be able to implement our Retrofit interface in pretty much the same way that we have been with RxJava.
private fun makeService(okHttpClient: OkHttpClient): MyService { val retrofit = Retrofit.Builder() .baseUrl("some_api") .client(okHttpClient) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build() return retrofit.create(MyService::class.java) }
So when it comes to the MyService interface that is used above, we can now change that retrofit interface to now return us Deferrable types instead of Observable types. So where we may have previously had:
@GET("some_endpoint") fun getData(): Observable<List<MyData>>
We can simply change this to:
@GET("some_endpoint") fun getData(): Deferred<List<MyData>>
At this point, any time we call this getData() function we will be returned a Deferred instance — this is the Job for the network request that is taking place. When we previously may have called this function with RxJava we might have had something like this within our calling class:
override fun getData(): Observable<List<MyData>> { return myService.getData() .map { result -> result.map { myDataMapper.mapFromRemote(it) } } }
In this RxJava flow we are calling our service function, then performing a map operation from the RxJava API, followed by mapping our data classes from the result of our our request to some format that is used by our UI layer. This changes slightly when we make the switch over to a coroutines implementation — to begin with, our function needs to become a suspend function. This is because we are calling going to be making a suspending operation within the function body, and in-order to do this the calling function must also be a suspending function. A suspending function is non-blocking and can be managed once triggered — such as being started, paused, resumed and cancelled.
override suspend fun getData(): List<MyData> { ... }
Next we need to call our service function, at a first glance this looks like we are carrying out the same thing but remember we are now receiving an instance of a Deferred rather than an Observable:
override suspend fun getData(): List<MyData> { val result = myService.getData() ... }
Because of this change, we can no longer use our chained map operation from the RxJava API — and in fact at this point, we don’t even have our data available as we only have the Deferred instance and not the value that represents it. What we need to do here is use the await() function to wait for the result of our request and then continue with our function body once a value has been received:
override suspend fun getData(): List<MyData> { val result = myService.getData().await() ... }
At this point our request would have been completed and we will have our data available for use. Because of this we can now perform our mapping operation that we were carrying out previously and return the result from our function:
override suspend fun getData(): List<MyData> { val result = myService.getData().await() return result.map { myDataMapper.mapFromRemote(it) } }
At this point we’ve taken our retrofit interface, along with the calling class, and converted the operations to use coroutines — that wasn’t so bad was it! At this point now, we will want to call this from our activity / fragment and make use of that data that we are retrieving.
In our activity we want to start by creating a reference to a Job, this is so that we can assign our coroutine operation to it and manage that Job if required — e.g. cancel the job when onDestroy() is triggered.
private var myJob: Job? = null
override fun onDestroy() { myJob?.cancel() super.onDestroy() }
Now that this in place, we need to actually assign something to this Job instance. Let’s go ahead and kick off our request with coroutines:
myJob = CoroutineScope(Dispatchers.IO).launch { val result = repo.getLeagues()
withContext(Dispatchers.Main) { //do something with result } }
In this post I don’t want to really go too much in Dispatchers or the execution of coroutine operations as these are topics for another post. In a nutshell, here we are:
- Creating a CoroutineScope instance using the IO Dispatcher as its context — this dispatcher is used for performing blocking IO tasks, such as network requests.
- Launching our coroutine using launch — this launches a new coroutine and returns a reference to it as a Job instance.
- We then use our repository instance to fetch the data by performing our network request
- Finally, we use the Main dispatcher to perform work on the Main thread — this is so that we can update our UI with the retrieved data
In following posts I will look further into these details, but for now this should be enough to get started with exploring coroutines.
In this post we’ve replaced our retrofit RxJava responses with instances of Deferred from the coroutines API — calling the functions used to fetch this data and finally accessing it from our activity. So far I hope you can see how little work is required to get up and running with coroutines, as well as the simplicity of the API when it comes to both reading and writing the implementation that we have. If you have any questions or comments then please feel free to reach out!
https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter