Our experience in migrating to coroutines from RxJava

Jasmine Villadarez
Weeronline
Published in
4 min readMay 5, 2021

--

One of the challenges in developing Android applications is performing background tasks. As Android developers, we are given multiple choices — there are AsyncTasks, Services, Jobs, etc. Then there is RxJava — a library for composing asynchronous observable sequences.

For years, RxJava has been the holy grail for doing background tasks in Android. It made it easier to switch between threads and to propagate errors. It also prevented callback hell. However, most Android applications will only ever need to do API calls which means that most applications will not maximize RxJava’s full potential.

Our RxJava usage

Our usage of RxJava was simple. It was used for making sure API calls were running in the background thread and were not blocking the UI. There were no reactive streams usage. We call an API, wait for its response and display it on the screen.

Why Switch?

There are various advantages to using coroutines like having less method count, being relatively easier to grasp for junior developers and its Jetpack integration. Our major drive, however, is to have our data source or data module be Kotlin multiplatform ready. With Kotlin coroutines, we remove our dependency to RxJava which is only available for JVM platforms.

Migration Process

Our code base is relatively small. Still, switching from one library to another is not something you do in one go, especially with the way we integrated RxJava. Our usage of RxJava may be simple but all our API interfaces, unit tests and view models have Observables. We needed some way to make sure that we are able to rollout coroutines safely without worrying about things breaking in production.

That is where Flow comes in.Flow has a lot of extension functions that can make switching from RxJava painless. It enabled us to change just parts of our code to workable coroutines and have Flow bridge it to RxJava.

For instance, we have a PlaceRepository with a function getPlace that returns an Observable<Place>

override fun getPlace(lat: Double, lon: Double): Observable<Place> {
return flow {
val place = placeApi.getPlaceInfo(
lat.roundTo(MAX_DECIMAL_PLACES),
lon.roundTo(MAX_DECIMAL_PLACES)
)
emit(place)
}.asObservable()
}

As you can see from the sample above, the asObservable() extension function transforms Flow into an RxJavaObservable. getPlace invokers will be unaware that it is already using Flow .

Another approach is to immediately have Flow as the return type and call asObservable in the invoker code

override fun getPlace(lat: Double, lon: Double): Flow<Place> {
return flow {
val place = placeApi.getPlaceInfo(
lat.roundTo(MAX_DECIMAL_PLACES),
lon.roundTo(MAX_DECIMAL_PLACES)
)
emit(place)
}
}
private fun someCaller(lat: Double, lon: Double): Observable<Place>{
return placeRepository.getPlace(lat, lon).asObservable()
}

This allowed us to already change snippets of code, rollout to production bit by bit and see if there are problems before completely switching out RxJava to Flow. High risks of breaking things in production were mitigated. Flow also has the advantage of having similar functions as RxJava which made adaptation relatively easy.

fun getPlace(lat: Double, lon: Double) {
viewModelScope.launch {
placeRepository.getPlace(lat, lon)
.flowOn(Dispatchers.IO)
.collect {
updatePlace()
}.catch { emitError() }
}
}

Suspending functions all the way

Flow allowed us to remove RxJava dependencies smoothly. But the primary usage of Flow is still to return a sequence of results much like RxJava Observable. Our usage of Flowor Observable does not require a sequence of results — we make an API call and wait for its response. Thus, to be intentional in our code, we removed Flow and made our code simpler by using suspending functions.

suspend fun getPlace(lat: Double, lon: Double): Place {
return placeApi.getPlaceInfo(lat, lon)
}

Caller:

fun getPlace(lat: Double, lon: Double) {
viewModelScope.launch(Dispatchers.IO) {
val place = placeRepository.getPlace(lat, lon)
updatePlace(place)
}
}

Hurdles Encountered

Migration from one technology to another always involves some challenges. It can be overwhelming especially with migrating from tightly-coupled codebases. In our migration, we encountered a few but the most notable ones are managing dispatchers and propagating errors.

Switching Dispatchers and Unit testing with them

In unit testing RxJava, there are functions to set all the schedulers to immediate. But, with coroutine dispatchers, this does not exist. Our solution for this is to create a DispatcherProvider class in which we override its implementation during testing. It’s not that much of a hassle but it is something to keep in mind.

Propagating errors

Perhaps one of the most challenging part for us in this migration is the propagation of errors. For RxJava, most errors can be caught and handled by onError function. Moreover, there is no need to worry about other jobs cancelling because of other jobs’ exception. It is a different story with using coroutines. There has to be awareness on where errors could occur and that those errors are properly handled to prevent cancellation of all jobs running in background.

Exceptions in coroutines by Martin Vivo helped us in understanding how errors are passed from parent jobs to children jobs and vice versa.

Although this held us back a bit, we find that this caused us to be intentional in our code and be more aware of exceptions in our code.

Conclusion

Using RxJava, Flow or suspending functions still depends on how you use them. There is always more than one way to do things. Our code became more readable after migrating to coroutines and in my opinion, more accessible to junior Android developers because RxJava’s steep learning curve is out of the picture. We were also able to increase our crash-free rate because switching to coroutines increased our awareness of where exceptions can occur. Most importantly, our code is one step closer to being Kotlin multiplatform ready!

--

--

Jasmine Villadarez
Weeronline

Android Developer | Amateur Astronomer | Hobbyist Photographer