Our migration story to RxJava2
To migrate or not to migrate, that is the question!
At BlaBlaCar, we adopted the reactive programming paradigm for the development of our Android application early, using RxJava.
After the initial learning curve and a few bumps in the road, we started reaping the benefits that a reactive library like RxJava provides.
Hard tasks like scheduling background tasks to perform I/O operations became easy, with using a more declarative programming style.
Besides, the ability to compose functional transformations to streams of data helped us write more concise code.
By the time RxJava2 came along we already had a humongous amount of code that depended on RxJava1. Because of that, it wasn’t easy to decide whether and when we should start doing the upgrade to RxJava2.
- No features were added to RxJava1 since June 1, 2017.
- After March 31, 2018, RxJava1 won’t be maintained anymore, meaning no more development and more importantly no more fixes.
- RxJava2 follows the Reactive-Streams architecture.
- The development of RxJava3 had already begun. Staying put with RxJava1 would have made that transition harder in the future.
- We always do our best to keep our dependencies up to date. Our experience has shown that leaving too many dependencies in their old versions leads to weird bugs and crashes. It also leads to a broken window situation making the overall quality of the application quickly deteriorate.
Main differences between RxJava1 and RxJava2
Before diving into the migration itself, we had to understand the main differences between the two versions of RxJava. Particularly, the ones that could affect us directly.
Behavior around nullability : RxJava2 no longer accepts null values. For example, doing Observable.just(null) is no longer allowed and will throw immediately a NullPointerException. This new behavior can have a big impact especially because we used to allow emitting null values. Besides, this change also means that Observable<Void> can no longer emit values and can only terminate with and without an error (onError, onComplete).
New primitive types: RxJava2 provides new types in addition to modifying existing ones. In particular:
- In RxJava1, Observable was backpressured but in RxJava2 there are two separate classes: Observable (without backpressure) and Flowable (with backpressure). Backpressure is useful in situations in which an observer consumes a stream of events more slowly than the rate at which they are produced.
- The Maybe primitive can either emit one single value (onSuccess), an error (onError) or finish without emitting a value (onComplete).
You can find more detailed information of the changes that were made on the official Wiki Page, What’s different in 2.0.
First failed attempt
We first thought of approaching this migration with a naive strategy that would consist in tackling it all at once in a single Pull Request.
However, we quickly realized the challenges that we could have faced when going down that route. In particular, the Pull Request would have been huge and very hard to review, which would have increased the probability of regressions.
To remedy this problem and ease the migration, we relied on two strategies:
- Make it possible for RxJava1 and RxJava2 to coexist in the same code base. So, we had to find a way to transform streams of RxJava1 to the ones of RxJava2. A quick exploration led us to the RxJavaInterop library from Dávid Karnok.
- Migrate each screen independently and proceed cautiously to mitigate the risk of introducing any regressions.
To test this strategy, we decided to start with the least risky page of the application, which happened to be the notification settings section in the user profile.
Our implementation started with adding a bunch of new dependencies to our Gradle build file. Here is an idea of what was needed:
To migrate the user notifications screen, we proceeded this way:
- First, we made the Repository expose RxJava2 Observables.
- Then we made the presenter use RxJava2. For instance, the most frequent change across all our presenters was to replace CompositeSubscription with CompositeDisposable.
At BlaBlaCar, we love and value testing. The unit tests that covered code that deals with RxJava relied on using the immediate scheduler to simulate a synchronous consumption of events. Unfortunately, that scheduler vanished with RxJava2 as explained in the documentation:
immediatescheduler is not present in 2.x. It was frequently misused and didn't implement the
Schedulerspecification correctly anyway; it contained blocking sleep for delayed action and didn't support recursive scheduling at all. Use
Wait, is this how we should write tests now?
Trampoline schedulers returns a default, shared
Scheduler.Workerinstances queue work and execute them in a FIFO manner on one of the participating threads.
By queuing work on the current thread, the trampoline scheduler could indeed fulfill the same role as did the immediate scheduler in our tests.
By updating the repository, the presenters and the tests, the user notification settings was fully migrated. By then, we were able to ship an entirely migrated screen and verify that everything was working as expected before pursuing the effort. We didn’t notice any regressions or crashes so we kept going with this strategy to migrate all the other screens.
Here are a few tips that we hope could be useful if, like us, you are contemplating the idea of doing a similar migration:
- We recommend doing the migration in small batches, where each screen is migrated independently.
- We recommend using RxJavaInterop to make RxJava1 and RxJava2 coexist in the same code base. This is instrumental in making it possible to migrate each screen independently.
- A lot of the errors that we faced were the consequence of emitting null values. So we recommend anticipating such scenarios by writing appropriate tests.
- We recommend using the appropriate primitives (Observable, Flowable, Single, Maybe) that meet the needs of the situation rather than defaulting to Observable every time.
- When you think all the work is done, we recommend making sure that no imports of RxJava1 are left. This includes imports like rx.Observable or rx.Single.
I hope this article brought some interesting insights into how to easily migrate from RxJava1 to RxJava2. If you have done this yourself, feel free to share some comments and feedback.
By the way, we are hiring, if you want to work with us on solving exciting challenges!