From RxJava to LiveData (and back?)

This photo has nothing to do with either RX or LiveData

Disclaimer: I'm no Reactive Extensions (RX) expert.

I'm simply a happy user with some (~6 years) experience with it. I've seen people struggling with RX. They say it has a steep learning curve. In that light, I was super excited when I first heard about Google's Android Architecture Components (AAC) and LiveData. A simplified version of RX with automatic lifecycle management! I immediately tried to use it in a pet project and it seemed simple indeed. The next real project I started, I chose LiveData without any hesitation: it is part of AAC, so future maintainers of the code should already be familiar with it. Fast forward 3-4 months and my excitement has been somewhat diminished. The familiarity point still sticks, but the simplicity part ... not so much. Here are some things that might be useful for someone else who has experience with RX and starts using LiveData for the first time.

LiveData has no error channel

LiveData is designed for the happy path, a stream of successful results. But things tend to fail and especially so on mobile devices (connectivity issues, limited power and hardware resources, etc). So how do you handle these? One simple solution would be to split it up to two separate LiveData streams:

results: LiveData<Foo>
errorMessages: LiveData<String>

Another option would be to introduce a helper class Result and wrap the results in it:

sealed class FooResult {
    data class Success(val data: Foo) : FooResult()
    data class Error(val error: Throwable) : FooResult()
}

results: LiveData<FooResult>

LiveData is sticky

The first sentence from the official documentation for LiveData says:

LiveData is an observable data holder class.

So naturally enough, people coming from RX world think it's a simplified Observable. But actually, it is more similar to BehaviourSubject. It holds the last value and new observers would get that first. This fact makes error handling very cumbersome. Errors should be one time events, it makes no sense to cache them. Yes, it's nice to re-populate your RecyclerView with cached data on screen orientation changes, but showing the last REST API call error at the same place? Should I show the error or have I done it already? Since there's no API contract for error handling, it's unclear whether an error is fatal or not. Would there be more successful results after an error?

LiveData has no Future<> like APIs

RX provides nice API for special cases of "streams of events" where there are either no results (Completable), a possible result (Maybe), or a result (Single). I think it makes the API very clear to understand. With LiveData, the caller would somehow have to know how many results to expect and stop observing at the right time, because the "stream" never ends.

LiveData has (almost) no operators

LiveData has only two operators (called "transformations"): map() and switchMap() (flatMap() in RX). I keep missing RX operators like zip(), combineLatest(), distinctUntilChanged(), etc. Sure, I have written my own versions of these for LiveData. But I'd always prefer the quality of RX operators over my home grown ones.

LiveData has no operator chaining

I've heard complaints about RX being hard to read. But compare these two code samples:

fun allPosts(): LiveData<Post> {
    val userIds = Transformations.map(allUsers()) { user: User? ->
        user!!.id
    }

    return Transformations.switchMap(userIds) { id: String? ->
        userPosts(id!!)
    }
}

vs:

fun allPosts(): Observable<Post> =
    allUsers()
        .map { user -> user.id }
        .flatMap { userId -> userPosts(userId) }

Beware of converting RX streams to LiveData

There is a handy helper class to turn RX Publisher to LiveData:

fun LiveDataReactiveStreams.fromPublisher(publisher: Publisher)

Your RX chain needs to make sure to handle errors. Otherwise, the app dies with RuntimeException:

LiveData does not handle errors. Errors from publishers should be handled upstream and propagated as state

Another thing, what would you expect this code to print:

fun bar(): LiveData<Int> {
    val p = Flowable.fromArray(1, 2, 3)
    return LiveDataReactiveStreams.fromPublisher(p)
}

bar().observe(this, Observer {
    Log.i("foobar", "got $it")
})

1, 2, 3, right? Wrong. It prints just 3.

CONCLUSIONS


LiveData tries to be a simplified version of RX but fails because you need to handle the hard parts yourself. Yes, it has small API. Yes, writing a zip() operator is not very hard. But it sounds like the reasoning of a novice developer - "I don't understand the hard parts of this code, so I'll rewrite it, it'll be so simple!". Error handling is complicated. Corner cases are complicated. Code that looks complicated is (hopefully) complicated because it needs to handle these conditions (https://twitter.com/havocp/status/1032632650165616645). It's much simpler to ignore the RX operators you don't know about than to write the ones you do need yourself.

Note that I've completely ignored automatic lifecycle handling. It's great, no argument against that. I've just never understood the complaints (and projects like RxLifeCycle or its "successor" AutoDispose). People say it requires manual work that is easy to forget. In my opinion the manual work is trivial, makes the intention very clear, and the editor already reminds you when you don't keep track of your Disposables. I feel it's the perfect case of Simple Made Easy (Side note: every programmer should be required to watch that talk at least once in their life).

But familiarity is also important. If your team has experience with LiveData and no experience with RX, the choice is already made for you.