Example: From RxJava to Coroutines
By Keane Quibilan and Shaurya Arora
Introduction
This is part 2 of our series on Coroutines in Kotlin. If you missed part 1, check out our Coroutines Primer first. In this blog post, we demonstrate the usage of coroutines. To do so, we convert a simple project using RxJava and Retrofit. Our end goal is to replace RxJava with Coroutines. We can definitely do this in a single shot, but let’s break it down into steps. This way, even if you’re not familiar with RxJava, you can pick up from one of the intermediary steps. In the final section, we have a comparison of our code before (RxJava) and after (Coroutines).
Step 0 — Initial State (commit)
This is the starting point. The project is a simple application that hits a Mocky endpoint to return a JSON object.
It uses the NetworkClient
and the NetworkAPI
to convert the JSON object into a User
and displays it in the UserInformationActivity
using the UserInformationPresenter
.
UserInformationActivity
— This activity is very simple. It has a “loading”TextView
as well as an “info”TextView
. The latter is used to display the downloaded information once it has loaded. Tests for this class are located inUserInformationActivityTest
. In this tutorial, we may refer toUserInformationActivity
as the Activity or the view.UserInformationPresenter
— This is a single-method presenter that is triggered by the Activity. The presenter’s only method isloadUserInfo()
. That method’s job is to use RxJava and Retrofit to obtain a user’s info from the Mocky API. Tests for this class are located inUserInformationPresenterTest
. In this tutorial, we may refer to theUserInformationPresenter
as the presenter.NetworkClient
— This singleton is a Kotlin object that builds our Retrofit client that implements the Network API. The only public method,getUser()
, wraps the client and itsgetUser()
method.NetworkAPI
— This is an interface that defines the network calls that our Retrofit client will make, and what they will return. It currently has a single method,getUser()
, that will return an RxJavaSingle
that emits the user downloaded.
These are the important files for this task. We won’t be modifying the other files.
Step 1 — RxJava to Retrofit’s Call (commit)
This step involves converting the reactive-style chain currently being used in the presenter to a callback-style paradigm that is typical of Retrofit.
- In
NetworkClient
, we remove the call adapter factory that converts our calls to RxJava objects. We also update thegetUser()
method to return a RetrofitCall
object instead of an RxJavaSingle
. We updateNetworkAPI
to also return aCall
. - In the presenter, we remove the
Single
’s subscribe invocation and replace it with anenqueue()
method call. The enqueue method takes aCallback
object which implements callback methods foronFailure()
andonResponse()
for errors and successes respectively. UserPresenterTest
has to be updated to return mockCall
objects where we were previously returning fakeSingle
s. We then use Mockito’sCaptor
class to mock the success and failure responses of the network call.- Finally, we remove the RxJava, RxAndroid, and the Retrofit RxJava Adapter as a dependency and delete the
BaseRxTest
helper class that we will no longer need.
This is a simple step, but now that we have removed RxJava, we are ready to bring in Coroutines.
Step 2 — Upgrade Kotlin and Coroutine Libraries (commit)
We upgrade to the most recent version of Kotlin because, beginning with Kotlin 1.3, Coroutines are no longer an experimental feature — they are finally considered stable. Depending on when you read this, the version numbers may be different.
Upgrading Kotlin required migrating from Kotlin Standard Library JRE 7 to Kotlin Standard Library JDK 7.
We also add the Coroutines library as a dependency.
Step 3 — Convert from Call to Coroutines with Launch (commit)
This step is the bulk of our work. It maintains the Retrofit Call
class, but instead of using the asynchronous enqueue()
method, we rely on Coroutines for all asynchronous code.
Inside the presenter:
- We remove the
enqueue()
method from theCall
object and replace it with a synchronousexecute()
invocation. Sinceexecute()
can throw an exception, we wrap the invocation in a try-catch and use the catch clause to pass any exceptions to the view. - We wrap the entire network call in a Coroutine launch directive. We set the scope’s context to
IOContext
— a context passed in from the Activity that is understood to be a background thread. - Since the calls are now being done on the
IOContext
, we need to ensure that any view methods are called on the UI thread. We do this by wrapping the view invocations with thewithContext(mainContext)
method. ThemainContext
variable is another context passed in from the Activity, understood to be the foreground thread.
In the Activity, we now need to pass our IOContext
and our mainContext
to the presenter.
IOContext
is provided byDispatchers.IO
. This is a CoroutinesDispatcher
that will offload tasks to a thread pool that automatically starts and shuts down threads on demand.- The
mainContext
is provided byDispatchers.Main + job
.Dispatchers.Main
is a CoroutinesDispatcher
that is confined to the main thread operating with UI objects. This allows coroutines to interact with the Android view hierarchy. The+ job
part has the effect of tying coroutine execution to the lifecycle of the Activity. Notice thatjob
is alateinit
variable that is initialized inonCreate()
and canceled inonDestroy()
. This ensures that, if the activity is destroyed before the coroutine (usingmainContext
) is finished, the execution of the coroutine is canceled as well.
Our tests are now updated to match the switch to Coroutines as well. We remove the argument matchers that the enqueue()
method required, and replace them with mocks for execute()
. This simplifies mocking the success and failure cases.
Step 4 — Switch to Coroutines with Async (commit)
In the previous step, we converted our Call
's asynchronous enqueue()
function to a synchronous execute()
call. We made the call in the background by using the launch()
method.
In this step, we convert from the coroutine launch()
method, to the coroutine async()
method. This will allow us to create non-blocking network calls that each return a value. For more complex jobs, async()
coroutines will allow us the versatility of parallel and sequential tasks. We won’t cover that here, but let’s go over how to convert to the async-await pattern.
- In
NetworkClient
, instead of returning the RetrofitCall
, we synchronously execute the call and return its body. By wrapping all this in aCoroutineScope
'sasync()
method, our method will now return aDeferred<User?>
object.
The presenter will have a few changes
- Our
loadUserInfo()
method can now run on the main UI context. We can get rid of thewithContext()
calls in the success and failure clauses. - Since we’re no longer getting a Retrofit
Call
object from thegetUser()
method, we can pass it to the view withoutCall
’s methods:execute()
andbody()
. - Because the returned object is a
Deferred<User?>
, we call theawait()
method to suspend this coroutine until the network call is completed. - In the presenter’s test, we replace the mock
Call
with a mockDeferred<User?>
. Because we now need to pass a coroutine context to the network client, and we want the test to run immediately, we pass in theDispatchers.Unconfined
context.
In the presenter’s loadUserInfo()
method, we start a coroutine with mainContext
, and inside this coroutine, we call the network client’s getUser()
method. The getUser()
method performs the network operation in another coroutine with IOContext
. Note that, as far as the presenter’s coroutine is concerned, no suspension happens until you call await()
on the Deferred
object. That is, if we had more lines of code between getUser()
and await()
, they would be executed without any delay. It’s only when we call await()
that the presenter’s coroutine is suspended.
In short, any calls that are written between the async()
and await()
will happen immediately. Any calls that are written after the await()
call will wait for the network call to complete.
Before and After
Now that we’ve switched from RxJava to Coroutines, the user experience should not be any different. But looking at the presenter, it’s easy to see the change from a reactive paradigm, to the async-await pattern.
We also pass the coroutine context down from the activity. If you’re using dependency injection, you won’t need to do this; it can be provided by a module.
Before:
After:
Overall, coroutines still provide us with idiomatic, readable, and testable code. Combined with the functional aspects of Kotlin, we have much of the same basic functionality of RxJava but without the learning curve and complexity of RxJava’s many operators and constructs.
Keane Quibilan is an r2d2 developer.
Shaurya Arora is a Toronto-based Android developer, drummer and prog metal/djent lover.
Join our fast growing team and connect with us on Twitter, LinkedIn, Instagram& Facebook! Learn more about us on our website.