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 theActivity
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 anObservable
as its data source. It needs to subscribe to theObservable
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 theAdapter
, 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 theAdapter
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 theAdapter
wants to send it a new one, so it passes it a newObservable
. You want to unsubscribe from the lastObservable
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 usingunsubscribe()
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 toonComplete()
. 99% of the time this is fine, but it leaves open the door for developer mistakes due to subtle differences betweenonComplete()
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 exceptObservable
, 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 specializedActivity
orFragment
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 ontoSubscriptions
(or stuffing them into aCompositeSubscription
) then manually callingunsubscribe()
/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 anObservable
from its parent but does not know when to unsubscribe, it passes the resultingSubscription
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 functionfun listen(data: Observable<Data>): Subscription
. That way theAdapter
can listen to theObservable
, but is not responsible for knowing when it needs to stop listening; that responsibility is explicitly given to the owner of theAdapter
.This pattern can be applied repeatedly to as many layers as you want. You could have an
Activity
that creates a View that contains aRecyclerView
that creates anAdapter
that listens to anObservable
… but as long as you pass that Subscription upwards at each layer, it will eventually make its way back to a parent (possibly theActivity
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 callmySubscription?.unsubscribe()
. -
A simple
CompositeSubscription
operator extension lets you use+=
to addSubscriptions
. Otherwise you need to wrap your wholeObservable
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.