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.
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
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!
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-
Activitycomponent, then you have to give it access to the
Activitylifecycle 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
Adapterthat you give an
Observableas its data source. It needs to subscribe to the
Observableand (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
Adapterlistener 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
Adapterexample above. You're listening to one data source, but then whoever is controlling the
Adapterwants to send it a new one, so it passes it a new
Observable. You want to unsubscribe from the last
Observablebefore 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
Subscriptionsanyways 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
RxLifecycle throws exceptions for
Completable. Again, because we can only simulate the stream ending.
Completableeither 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
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
Fragmentand 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
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.
Here’s what I’ve started doing instead of using RxLifecycle.
Subscriptions. That means hanging onto
Subscriptions(or stuffing them into a
CompositeSubscription) then manually calling
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
Subscriptionsupwards until someone handles it. In other words, if a component is given an
Observablefrom its parent but does not know when to unsubscribe, it passes the resulting
Subscriptionupwards to the parent, since the parent should have a better grasp of the lifecycle.
Let’s look at that
Adapterexample from before. We now provide a function
fun listen(data: Observable<Data>): Subscription. That way the
Adaptercan 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
This pattern can be applied repeatedly to as many layers as you want. You could have an
Activitythat creates a View that contains a
RecyclerViewthat creates an
Adapterthat 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
Activityitself) 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
Subscriptionsis a simple one-liner. Before you had to check for nullability (or use a one-liner utility function). Annoying. Now you can just call
CompositeSubscriptionoperator extension lets you use
Subscriptions. Otherwise you need to wrap your whole
Observablechain 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()
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.