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 allimports
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.