Why Not RxLifecycle?

Hello. This is Dan Lew. You may or may not know me as the author of RxLifecycle.

Here’s why I’ve started pulling back from using my creation.

Origins

When Trello first started using RxJava, we were dismayed with how easy it was to leak memory when using it. Seemingly any Subscription you setup would leak unless you explicitly cleared it. As such, we were constantly juggling Subscriptions and unsubscribing when we were done using them.

Manually handling subscriptions turned out to be rather tedious, so we wanted something that took the thought out of it. For the most part, we simply wanted all our Subscriptions to end when the Fragment or Activity lifecycle ended. Thus was born RxLifecycle.

With RxLifecycle, you just slap a compose() call onto any stream and it automatically completes the stream when certain lifecycle events happen. It was back to the old days of not having to worry about memory leaks!

Problems

There have been some lingering problems with RxLifecycle that over time have gnawed at my mind more and more. Roughly in order of importance, here they are:

  • Automatic lifecycle detection leads to confusing and sometimes non-deterministic code. The code is trying to detect where in the lifecycle you are and when to unsubscribe. If you’re subscribing in, say, onStart() then it’s not really a big deal. But if you’re inside some non-Activity component, then you have to give it access to the Activity lifecycle and then hope it is subscribing at the right time in the lifecycle, which is not guaranteed to be the case. Worse still, it is often obscure when subscription go awry.

    For example, suppose you’ve got an Adapter that you give an Observable as its data source. It needs to subscribe to the Observable and (at some point later) unsubscribe. The key problem with RxLifecycle here is: how do you know that your automatic unsubscription will happen at the right moment? Inside of the Adapter, there's no way to verify when in the lifecycle you're starting the subscription, and you have even less of a clue of when it's ending. Even if the code works now, if someone moves the Adapter listener code around it could change when it automatically unsubscribes. That’s messy.

    Over time I’ve grown weary of automatic code that sometimes breaks. I much prefer code that is rock-solid and never breaks, even if it means writing more boilerplate.

    (Using the more explicit bindUntilEvent() instead of automatic detection somewhat avoids this problem, but lessens the utility of RxLifecycle.)

  • Often times you end up manually handling the Subscription anyways. Let's extend the Adapter example above. You're listening to one data source, but then whoever is controlling the Adapter wants to send it a new one, so it passes it a new Observable. You want to unsubscribe from the last Observable before subscribing to the new one. None of this has anything to do with the lifecycle, and thus must be handled manually.

    Having to manually handle Subscriptions anyways means that RxLifecycle is just an extra headache. It’s confusing to developers - why are we using unsubscribe() in one place and RxLifecycle in another?

  • RxLifecycle can only simulate Subscription.unsubscribe(). Because of RxJava 1 limitations, it can (at most) simulate the stream ending due to onComplete(). 99% of the time this is fine, but it leaves open the door for developer mistakes due to subtle differences between onComplete() vs unsubscription.

  • RxLifecycle throws exceptions for Single / Completable. Again, because we can only simulate the stream ending. Single/Completable either emit or error, so there’s no other choice. For a while we weren’t using anything except Observable, but now that we’re using other types this can cause problems.

  • Subtle timing bugs require calling RxLifecycle late in the stream. It’s an avoidable issue, but again can lead to developer mistakes that are best avoided.

  • RxLint cannot detect when you’re using RxLifecycle bindings. RxLint is a handy tool and using RxLifecycle lessens its utility.

  • It generally requires subclassing Activity / Fragment. While not a requirement (since it’s implemented using interfaces), not subclassing leads to a lot of busywork reproducing what the library does. That’s fine most of the time, but every once in a while we need to use a specialized Activity or Fragment and that causes pain.

    (Note that this minor problem can soon be fixed via Google's lifecycle-aware components.)

What it all boils down to is that the automatic nature of RxLifecycle can have complex, unintended consequences. While the goal of RxLifecycle was to make life easier, it often ended having the opposite effect.

Some of these problems are solved by Uber’s AutoDispose library, which was born out of years of discussion between Zac Sweers and I on how to better write RxLifecycle. In particular, it uses true disposal instead of a simulacrum, does not throw exceptions for Single and Completable, and has fewer restrictions on when it can be used in a stream. I have not, however, just switched to AutoDispose because it doesn't solve all the above problems.

Better Patterns

Here’s what I’ve started doing instead of using RxLifecycle.

  • Manually manage Subscriptions. That means hanging onto Subscriptions (or stuffing them into a CompositeSubscription) then manually calling unsubscribe() / clear() when appropriate.

    Now that I’m used to the idea it’s not so bad. Its explicit nature makes code easier to reason about. It doesn't require me to think through a complex flow of logic or anticipate unexpected consequences. The extra boilerplate is worth the simplicity.

  • Components pass their Subscriptions upwards until someone handles it. In other words, if a component is given an Observable from its parent but does not know when to unsubscribe, it passes the resulting Subscription upwards to the parent, since the parent should have a better grasp of the lifecycle.

    Let’s look at that Adapter example from before. We now provide a function fun listen(data: Observable<Data>): Subscription. That way the Adapter can listen to the Observable, but is not responsible for knowing when it needs to stop listening; that responsibility is explicitly given to the owner of the Adapter.

    This pattern can be applied repeatedly to as many layers as you want. You could have an Activity that creates a View that contains a RecyclerView that creates an Adapter that listens to an Observable… but as long as you pass that Subscription upwards at each layer, it will eventually make its way back to a parent (possibly the Activity itself) who knows when to unsubscribe.

Another subtle reason for the switch away from RxLifecycle is our adoption of Kotlin. Kotlin makes manually handling Subscriptions easier for two reasons:

  • Unsubscribing from nullable Subscriptions is a simple one-liner. Before you had to check for nullability (or use a one-liner utility function). Annoying. Now you can just call mySubscription?.unsubscribe().

  • A simple CompositeSubscription operator extension lets you use += to add Subscriptions. Otherwise you need to wrap your whole Observable chain in parentheses, which is a huge pain formatting-wise.

    Here’s the extension in all its glory:

    operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)

    As a result, you can simply use a CompositeSubscription like so:

    compositeSubscription += Observable.just().etc().subscribe()

Mea Culpa

It's not a great feeling when you have built and supported a framework which you no longer believe in. But admitting you were wrong is far more valuable than steadfastly sticking with a subpar solution.

So... what now? Well, I'm going to keep maintaining RxLifecycle because people are still using it (including Trello), but in the long term I'm pulling away from it. For those still wanting this sort of library, I would suggest people look into AutoDispose, since I think it is better architecturally than RxLifecycle.


Many thanks to Zac Sweers for helping with this article.