RxJava to Coroutines: end-to-end feature migration

Kotlin coroutines are much more than just lightweight threads — they are a new paradigm that helps developers to deal with concurrency in a structured and idiomatic way.
When developing an Android app one should consider many different things: taking long-running operations off the UI thread, handling lifecycle events, cancelling subscriptions, switching back to the UI thread to update the user interface. In the last couple of years RxJava became one of the most commonly used frameworks to solve this set of problems. In this article I’m going to guide you through the end-to-end feature migration from RxJava to coroutines.
Feature
The feature we are going to convert to coroutines is fairly simple: when user submits a country we make an API call to check if the country is eligible for a business details lookup via a provider like Companies House. If the call was successful we show the response, if not — the error message.
Migration

We are going to migrate our code in a bottom-up approach starting with Retrofit service, moving up to a Repository layer, then to an Interactor layer and finally to a ViewModel.
Functions that currently return Single
should become suspending functions and functions that return Observable
should return Flow
. In this particular example we are not going to do anything with Flows.
Retrofit Service
Let’s jump straight into the code and refactor the businessLookupEligibility
method in BusinessLookupService
to coroutines. This is how it looks like now.
Refactoring steps:
- Starting with version 2.6.0 Retrofit supports the
suspend
modifier. Let’s turn thebusinessLookupEligibility
method into a suspending function. - Remove the
Single
wrapper from the return type.
NetworkResponse
is a sealed class that represents BusinessLookupEligibilityResponse
or ErrorResponse
. NetworkResponse
is constructed in a custom Retrofit call adapter. In this way we restrict data flow to only two possible cases — success or error, so consumers of BusinessLookupService
don’t need to worry about exception handling.
Repository
Let’s move on and see what we have in BusinessLookupRepository
. In the businessLookupEligibility
method body we call businessLookupService.businessLookupEligibility
(the one we have just refactored) and use RxJava’s map
operator to transform NetworkResponse
to a Result
and map response model to domain model. Result
is another sealed class that represents Result.Success
and contains theBusinessLookupEligibility
object in case if the network call was successful. If there was an error in the network call, deserialization exception or something else went wrong we construct Result.Failure
with a meaningful error message (ErrorMessage
is typealias for String).
Refactoring steps:
businessLookupEligibility
becomes asuspend
function.- Remove the
Single
wrapper from the return type. - Methods in the repository are usually performing long-running tasks such as network calls or db queries. It is a responsibility of the repository to specify on which thread this work should be done. By
subscribeOn(schedulerProvider.io())
we are telling RxJava that work should be done on theio
thread. How could the same be achieved with coroutines? We are going to usewithContext
with a specific dispatcher to shift execution of the block to the different thread and back to the original dispatcher when the execution completes. It’s a good practice to make sure that a function is main-safe by usingwithContext
. Consumers ofBusinessLookupRepository
shouldn’t think about which thread they should use to execute thebusinessLookupEligibility
method, it should be safe to call it from the main thread. - We don’t need the
map
operator anymore as we can use the result ofbusinessLookupService.businessLookupEligibility
in a body of asuspend
function.
Interactor
In this specific example BusinessLookupEligibilityInteractor
doesn’t contain any additional logic and serves as a proxy to BusinessLookupRepository
. We use invoke operator overloading so the interactor could be invoked as a function.
Refactoring steps:
operator fun invoke
becomessuspend operator fun invoke
.- Remove the
Single
wrapper from the return type.
ViewModel
In BusinessProfileViewModel
we call BusinessLookupEligibilityInteractor
that returns Single
. We subscribe to the stream and observe it on the UI thread by specifying the UI scheduler. In case of Success
we assign the value from a domain model to a businessViewState
LiveData. In case of Failure
we assign an error message.
We add every subscription to a CompositeDisposable
and dispose them in the onCleared()
method of a ViewModel’s lifecycle.
Refactoring steps:
- In the beginning of the article I’ve mentioned one of the main advantages of coroutines — structured concurrency. And this is where it comes into play. Every coroutine has a scope. The scope has control over a coroutine via its job. If a job is cancelled then all the coroutines in the corresponding scope will be cancelled as well. You are free to create your own scopes, but in this case we are going leverage the
ViewModel
lifecycle-awareviewModelScope
. We will start a new coroutine in aviewModelScope
usingviewModelScope.launch
. The coroutine will be launched in the main thread asviewModelScope
has a default dispatcher —Dispatchers.Main
. A coroutine started onDispatchers.Main
will not block the main thread while suspended. As we have just launched a coroutine, we can invokebusinessLookupEligibilityInteractor
suspending operator and get the result.businessLookupEligibilityInteractor
callsBusinessLookupRepository.businessLookupEligibility
what shifts execution toDispatchers.IO
and back toDispatchers.Main
. As we are in the UI thread we can updatebusinessViewState
LiveData by assigning a value. - We can get rid of
disposables
asviewModelScope
is bound to aViewModel
lifecycle. Any coroutine launched in this scope is automatically canceled if theViewModel
is cleared.
Key takeaways
Reading and understanding code written with coroutines is quite easy, nonetheless it’s a paradigm shift that requires some effort to learn how to approach writing code with coroutines.
In this article I didn’t cover testing. I used the mockk library as I had issues testing coroutines using Mockito.
Everything I have written with RxJava I found quite easy to implement with coroutines, Flows and Channels. One of advantages of coroutines is that they are a Kotlin language feature and are evolving together with the language.