A common question most android developers have when using RxJava is: how do I cache or persist the work done by my observables over a configuration change? If you start a network call and the user decides to rotate the screen, the work spent in executing that network call would have been in vain (considering the OS would re-create your activity).
There are two widely accepted solutions to this problem:
- store the observable somewhere as a singleton, possibly apply the cache operator and re-subscribe on activity recreation
- house your observable in a retained “worker” fragments 1
I whipped up a quick example on github to demonstrate the second technique (which is generally my choice of poison). Now I’ve used the technique of worker fragments (successfully) a bunch of times before so I was a little surprised to see the example not work.
Let’s go over the use case again:
You have an observable that executes a long running network call. Before the call completes, you perform an activity rotation. After the activity is recreated, you continue the network call from where it left off or just use the result if it completes before your activity recreation process.
Instead of simulating this network call use case I decided to just fake it with a “hot” observable instead (which makes the use case a tad bit different but would help demonstrate the solution equally well). If you’re looking for a quick simple example that demonstrates the difference between a hot and cold observable, I strongly recommend watching this egghead.io video on the subject.
In fact, I used the exact same concoction of operators for my example:
Observable
.interval(1, TimeUnit.SECONDS)
.map(new Func1<Long, Integer>() {
@Override
public Integer call(Long aLong) {
return aLong.intValue();
}
})
.take(20)
.share();
A little more investigation revealed that the share operator I used to fake the source stream was not really hot but “warm”. This is easier explained with Marble diagrams:
Here’s how we expect share to behave (and it does):
But owing to the activity recreation, what really happens is that the first subscriber (S1) unsubscribes from the source observable (O1 - housed in the worker fragment), after which a similar subscriber (S2) from the recreated activity subscribes again to O1 from the same worker fragment. So the marble diagram really lands up looking like this:
Notice that re-subscription? That changes things a little.
In this way, the share operator is “warm”. It behaves cold to first time subscribers but hot to subsequent ones.
the share operator behaves cold to first time subscribers but hot to subsequent ones
Epilogue:
So how did I circumvent the problem? I added a fake subscriber that never unsubscribes.
storeObservable =
Observable.interval(1, TimeUnit.SECONDS)
.map(new Func1<Long, Integer>() {
@Override
public Integer call(Long aLong) {
return aLong.intValue();
}
})
.take(20)
.share();
// Do not do this in production!
// this is a hack!
storeObservable = storeObservable.subscribe();
Clever but very hacky2. Think carefully before you write code like this in production.
Epilogue (Part 2):
@fabioCollini on twitter said he preferred a replay().connect() approach over the publish().refcount() one which is what the share operator really is.
This reminded me of a tip that Dan mentioned once, replay is similar to share in that it’s hot to the first subscriber and cold to every other subscriber after the first item emits3.
replay is hot to the first subscriber and cold to every other subscriber after the first item emits
Because marble diagrams are amazing:
I’ve since modified the example to use a ConnectableObservable instead and it works pretty much the same way.
-
Alex Lockwood wrote about this approach here. ↩︎
-
I can’t take credit for this idea. The comments in the code reveal the identity of the troublemaker. ↩︎
-
Depending on what it’s replaying, it could also be cold to the first subscriber (he is quick to point out) ↩︎