Example: From RxJava to Coroutines
By Shaurya Arora and Keane Quibilan
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 in UserInformationActivityTest. In this tutorial, we may refer to UserInformationActivity as the Activity or the view.UserInformationPresenter — This is a single-method presenter that is triggered by the Activity. The presenter’s only method is loadUserInfo(). 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 in UserInformationPresenterTest. In this tutorial, we may refer to the UserInformationPresenter 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 its getUser() 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 RxJava Single 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 the getUser() method to return a Retrofit Call object instead of an RxJava Single. We update NetworkAPIto also return a Call.In the presenter, we remove the Single’s subscribe invocation and replace it with an enqueue() method call. The enqueue method takes a Callbackobject which implements callback methods for onFailure() and onResponse() for errors and successes respectively.UserPresenterTest has to be updated to return mock Call objects where we were previously returning fake Singles. We then use Mockito’s Captor 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 the Call object and replace it with a synchronousexecute() invocation. Since execute() 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 the withContext(mainContext) method. The mainContext 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 by Dispatchers.IO. This is a Coroutines Dispatcher that will offload tasks to a thread pool that automatically starts and shuts down threads on demand.The mainContext is provided by Dispatchers.Main + job. Dispatchers.Main is a Coroutines Dispatcher that is confined to the main thread operating with UI objects. This allows coroutines to interact with the Android view hierarchy. The + jobpart has the effect of tying coroutine execution to the lifecycle of the Activity. Notice that job is a lateinitvariable that is initialized in onCreate() and canceled in onDestroy(). This ensures that, if the activity is destroyed before the coroutine (using mainContext) 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 Retrofit Call, we synchronously execute the call and return its body. By wrapping all this in a CoroutineScope's async() method, our method will now return a DeferredUser? object.
The presenter will have a few changesOur loadUserInfo() method can now run on the main UI context. We can get rid of the withContext() calls in the success and failure clauses.Since we’re no longer getting a Retrofit Call object from the getUser()method, we can pass it to the view without Call’s methods: execute()and body().Because the returned object is a DeferredUser?, we call the await()method to suspend this coroutine until the network call is completed.In the presenter’s test, we replace the mock Call with a mock DeferredUser?. Because we now need to pass a coroutine context to the network client, and we want the test to run immediately, we pass in the Dispatchers.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.
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.