Our way of migrating from RxJava to Kotlin Coroutines

All of the following decisions and thoughts about moving from RxJava to Kotlin coroutines originated from a meeting where one of us said:
RxJava 2 will be deprecated in one week”.

So we discussed what we want to do. In the short-term, but also in the long run. Thoughts like the following came up:

  • Staying at RxJava 2?
    Because it’s stable. There is no need to do something.
  • Bulk updating to RxJava 3?
    Meaning we hopefully only change all imports and hope that there were no behavior changes.
  • A mix of both worlds?
    Staying at RxJava 2 in the short-term but migrating to RxJava 3 or even coroutines in the long run?

As you might already have guessed. We decided to go with point three. Stay with RxJava 2 for now and migrate to coroutines slowly.

But why did we take this decision? To understand it, you first need to know a little bit about our architecture. So, let’s dive into it!

Luckily, our architecture is already pretty nicely abstracted. We have three layers. The ui layer includes ViewModels and Fragments. We have a domain layer where we have so called Actions which contain our business logic. In the clean architecture world, you would name those Actions UseCases. So if you are familiar with this concept, then you understand also our Actions. And finally, we have our data layer which is here to communicate with our backend via an HTTP API. This layer contain also a few Repository classes for caching or other data-driven behavior.

Okay, nice. But where is RxJava involved in this?
Well, basically RxJava is a thin layer between all of those layers mentioned above to connect everything with each other. For every class we have (Action, Repository and ViewModel) we have an interface with only a few methods in it. All of them return an Rx-Observable Observable , Flowable, Single or an Maybe with a value of course. Each layer “connects” with another via one of those Rx-Observables.

Since we now have a rough overview of our architecture and how RxJava is used here, let’s discuss how we finally plan to move to Kotlin coroutines.

Obviously, it is pretty hard or even impossible to migrate completely from RxJava to Kotlin Coroutines in one big-fat pull request. It’s not only time consuming, it also brings no direct value to the project.

So we decided that we move our domain layer first. Starting by replacing our Action interface methods to return either a Flow or the value directly, using the suspend modifier of course. Here, we also decided that we do not migrate every Action class immediately. We migrate only those classes which have to change/touched anyways as well as new implementations.

But what does this mean exactly? Again, new implementations will use coroutines from the beginning, which is I guess clear for everyone. But what should we do as soon as we touch or at least have to interact with other Actions? Unfortunately, I have to say: It depends. Each time we get in touch with such a class we have to think about a few things:

  • Is it easy to refactor?
  • Is it a complex class that requires a bunch of changes for their dependencies too?
  • Would the change help with the current (new) implementation? Or is this only a “nice to have” change?
  • How big is the feature or bug fix I currently work on already? Does the refactoring bring nothing than noise to the upcoming code review?
  • … And maybe more 🙂

If we decided that it’s not worth to migrate, we can luckily make use of the RxJava interoperability library. Simply use rxSingle {}, rxObservable {}, etc. whatever you need, for the Kotlin coroutines to RxJava interop as well as the Flow interop functions. Similarly, using await() and siblings for the RxJava to Kotlin coroutines interop.

But what about the other, ui and data, layers?

For the data layer the rules are basically the same as for the domain layer:
New implementations, which are rare anyways, will be done with coroutines. Existing classes will only be changed if interacted with it and “if it makes sense” (see “rules” above).

But we have one addition here. In case we want to use a coroutines based API (for the domain to data layer communication) for our current implementation, but do not want to change all other classes which depend on this class in the data layer, we decided that we simply add a new coroutines based function to those classes and deprecate the RxJava version of it.

With that solution, we are able to create a 100% coroutine based domain layer while the classes in the data layer provide the interop functionality.

The ui layer is another story. This is because we heavily depend on a reactive state state container library called knot which uses RxJava. Since we are happy with it as well as we don’t see any required action to migrate to Kotlin coroutines as soon as possible, we decided that we leave the ui layer like it is and using the Kotlin coroutines to RxJava interop functions there.

Maybe, we migrate here from RxJava 2 to RxJava 3 before we finally switch to a new coroutines based state machine library. But this needs to be evaluated in the future. When we have made our first experiences with coroutines in our code base. Finding or maybe even creating a coroutine based reactive state container library may also take some time.

In the long run, however, it is of course planned to move the ui layer to coroutines too. But we don’t see the need to rush it. The mentioned library works for us quite well and migrating our ui layer to Kotlin coroutines is probably the heaviest task in this migration journey.

I hope I could give you a few insights into how we migrated from RxJava to Kotlin coroutines. Maybe it will help you to find a nice way to migrate your code base too.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store