RxJava 1 → 2: Understanding the Changes

RxJava 1 → 2: Understanding the Changes
Cotopaxi by Frederic Edwin Church, 1862.

This is mainly a reply to Kaushik Gopal's article: "RxJava 1 -> RxJava 2 (Understanding the Changes)"

Disclaimer:

This is not a hate post or something but I think it could easily be treated as one, so that's why you're reading the disclaimer. I have no intentions to confront Kaushik because I don't see reasons for it.

But I have to say that I found article controversial enough to produce this reply (which initially was a comment for Reddit but quickly became too huge).

Nitpick 1

Bite the bullet and migrate it all in one shot.

Well, in our case it'll be hugest PR for review ever.

90%+ of our code (40k lines of code, 45k lines of tests, 100% Kotlin) is RxJava based and that means that this "one shot" will touch basically all the code in the project!

It's week(s) of work (obviously depends on the size of your project).

There are few major behavior changes:

  • Error handling (which makes your unit tests pass despite unhandled errors)
  • No support for nulls — hi there NullPointerExceptions, haven't seen you for a while, oh shi it's run time!
  • Confusing renames of doOn* operators.

Any mistake here can lead to crashes at runtime or even business logic changes.

That's why we do conversion in relatively small PRs, it's still in progress and after 3 months we've converted about 45% of the code.

RxJava 2 was designed to work near with RxJava 1, there is an interop library.

At the end of the day, conversion is a code change and it should be reviewed. Small PRs are much easier to make and review!

Nitpick 2

You want to be using Flowable everywhere now, not Observable. Use this as your default.

Why?

RxJava wiki has a great paragraph "Which type to use?". If you read it carefully you'll realize that there are not that many cases in Android development when you'd need Flowable:

Dealing with 10k+ of elements that are generated in some fashion somewhere and thus the chain can tell the source to limit the amount it generates.

Not the case with Android. And if you're close to that, you might want to port that to Observable because you're operating near to the hardware limits and Observable is more lightweight :)

Reading (parsing) files from disk is inherently blocking and pull-based which works well with backpressure as you control, for example, how many lines you read from this for a specified request amount).

Important part here: "with backpressure as you control". Which usually not the case.

What many people probably don't realize about backpressure is that it's not only about handling "uncontrolled spikes with too much data for subscriber to process" situation, but also that backpressure is actually a request-produce mechanism: only produce data when it was requested. And it's not like two-way paging mechanism, you have no way to go back with backpressure, only forward. Unfortunately, not that many APIs fit such use case.

Reading from a database through JDBC is also blocking and pull-based and is controlled by you by calling ResultSet.next() for likely each downstream request.

Basically the same thing as in the previous case, however we can definitely find equivalent to ResultSet.next() in Android Development: Cursor.moveToNext(). But, usually, we don't read Cursors in such a manner.

Btw, SQLBrite 2.x which now supports RxJava 2 is based on Observable.

sqlbrite2

Network (Streaming) IO where either the network helps or the protocol used supports requesting some logical amount.

Important part here: "supports requesting some logical amount". Again, it's not really about IO/networking, but more about request-produce flow.

Nitpick 3

Observables are still available for use, but unless you really understand backpressure, you probably don’t want to be using them anymore.

It looks like we should treat every RxJava stream as a stream which can uncontrollably produce "too many events to process", but usually it's not the case. And even if the stream has such a tendency, you can keep it under control with Observable's debounce/window/buffer/etc operators.

Nitpick 4

Article didn't mention episode 11 of The Context Podcast about RxJava 1 → 2 migration with Hannes Dorfman and Artur Dryomov 😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿😿


My take on "Which type to use?"

Flowable is a general-purpose and most capable type in RxJava 2. It can emit 0, 1, 2, … n items. It can produce error or complete normally, or be infinite. It has backpressure support. It conforms to Reactive Streams specification and can be used for interop between different Reactive Streams implementations. It's a multitool.

For the same reason RxJava 1 added Single and Completable at some point (which was pretty hot discussion at the time), RxJava 2 has Flowable, Single, Completable, Maybe and Observableput more information about stream behavior into the type system.

  • When I see Single — I know it can either produce value or error.

  • When I see Completable — I know it either completes without value or produces error.

  • When I see Maybe — I know that maybe, value will arrive, maybe not, or maybe I'll get an error — I don't know!

  • When I see RxJava 2 Observable — I know it does not support backpressure. Which means that it either generates not that much data per second or does not support request-produce model or both.

  • When I see Flowable — I know that either it can be source of "too much data to process" or it supports request-produce model. And it can be used for interop with other Reactive Streams implementations.

Many thanks to Artur Dryomov for review.