We recently finished our refactoring from RxJava 1 to version 2. It was an interesting trip, and thankfully, less painful that we expected. On our way we learned few things that I will share here.
Observables or Single/Maybe/Completable/Flowable?
The first big question you will have is if you want to keep using Observable
or you will want to switch to the newly introduced Single
, Maybe
, Completable
or Flowable
. Some of them were available on RxJava 1 already but with limitations: For example, Completable
on RxJava 1 didn’t work with Retrofit so we could not use it.
Our decision was to switch to those when possible.
Most of those Retrofit calls that used Observable
now use Single
and Completable
.
Other data streams were switched to Flowable
from Observable
for better back pressure handling.
However, this came with the added cost of also modifying our implementation. Before you start your upgrade process, decide if you want to change this functionality or not. It may not be the best idea depending on your situation.
Is your codebase well tested?
The lack of unit tests will definitely affect your refactoring.
Switching our tests to RxJava 2 was easier than expected, here’s an example:
RxJava 1
val subscriber = TestSubscriber.create<Data>()usecase.request(data).subscribe(subscriber)
RxJava 2
val subscriber = usecase.request(data).test()
I would be very careful about refactoring a large codebase that is not well covered by tests. If that’s your case, I’d consider to at least cover the most important business critical use cases before attempting the refactor.
Do your Observables emit null values?
RxJava 2 no longer accepts nulls, and those will cause NullPointerException
errors.
If you are using nulls as part of your logic, you will have to switch to a different solution, for example:
Completable
: When you only care about the completion state.Single
withOptional
: emit a value that can be empty.Maybe
: emit only when you have a value, otherwise empty.
It will depend on your use cases on what you want to do.
Are you using Kotlin?
Refactoring to RxJava 2 with Kotlin has some clear advantages, specially if you are not using Retrolambda on Java. Instead, if you are using anonymous classes, you will have to migrate them manually.
For example, all Action1
anonymous classes will have to be migrated to Consumer
classes.
RxJava 1
.doOnNext(new Action1<Boolean>() { @Override public void call(Boolean isValid) { aMethod(isValid); }})
RxJava 2
.doOnNext(new Consumer<Boolean>() { @Override public void accept(Boolean isValid) { aMethod(isValid); }})
With Kotlin (or with Retrolambda) the change is transparent:
RxJava 1
.doOnNext { aMethod(it)}
RxJava 2
.doOnNext { aMethod(it)}
My recommendation is that if you are also migrating your codebase to Kotlin, do that first, then migrate to RxJava 2, because the task will be easier once your codebase is in Kotlin.
If you are also migrating your codebase to Kotlin, do that first, then migrate to RxJava 2.
However, be ready to deal with some surprises too. An example of that is BiFunction
in combineLatest
. Type inference won’t work in those cases anymore and you will have to specify that you are using a BiFunction
explicitly.
RxJava 1
Observable.combineLatest(email, password, { email, password -> email.isValid() && password.isValid()})
RxJava 2
Observable.combineLatest(email, password, BiFunction<Boolean, Boolean, Boolean> { email, password -> email.isValid() && password.isValid() })
Another dangerous one is andThen
.
Using andThen
like this:
.andThen { aSingle() }
Is not the same as doing it like this:
.andThen(aSingle())
The first example is creating a SingleSource that does nothing!
Be careful, just because it compiles it does not mean that it will work.
Do I have to migrate all my codebase at once?
No. Don’t do that! Instead, use the Interop library to connect the parts of your codebase that are still in version 1 to the parts migrated to version 2.
RxJavaInterop.toV1Single(myV2Single)
However, if you use Kotlin, I recommend you to create extension functions to make the task easier:
fun <T> Single<T>.toV1(): rx.Single<T> = RxJavaInterop.toV1Single(this)
Then, you can do the above operation like this:
myV2Single.toV1()
Here’s the full snippet with some of the extensions that we found useful:
This way, you can do the following:
fun myOldObservableRx1(): rx.Observable { return myV2Single .toV1() .toObservable()}
In this example a method that was returning an RxJava 1 Observable
can use inside a Single
that is already in version 2. There’s no need to switch the whole codebase at once.
One final tip: Imports
I’d advice you to disable automatic imports during the migration, to avoid collisions with the rx
or the java.util
packages. As well, avoid using wildcard imports (star) and always explicitly declare them.
Summary
- Decide beforehand if you want to switch to the new Observable types from RxJava 2.
- Invest time implementing tests around your version 1 codebase before the migration.
- Check whenever you are using nulls in your Rx streams.
- Consider migrating to Kotlin before performing the refactor.
- Make good use of the interop methods to migrate method by method safely.
- Be careful with the imports.