Managing State with RxJava

Config Change Edition

Benoît Quenaudon
5 min readApr 25, 2017

Not really worth anything but you can check the preface if you feel like so.

Starting Point

The pattern used here is mostly based on some blog posts from Hannes Dorfman and on a recent talk by Jake Wharton.

Anyway I already had my unidirectional data flow working fine. I was missing one small detail: my flow would not survive a config change.

Not gonna be able to do it!

The code looks something like the following, and I will disclaim firsthand that I am going to to focus only on how to keep my flow alive so a lot of code is omitted. Check the links above for the other implementation details.

The binder purpose is summarized in the compose method.

Observable<ViewState>
compose(Observable<Intent> intents) {
return intents
.map(this::actionFromIntent) // A
.compose(actionToResultTransformer) // B
.scan(ViewState.idle(), reducer); // C
}

The view emits intents, The binder translates those intents into actions (A). These actions hit the business logic through transformers which return some results (B), results used to finally update the view state (C). That’s it, that is the flow we care about.

Secondly, the activity can be summarized as:

public class Activity {
private void bind() {
binder.compose( // 2
intents() // 1
).subscribe(this::render); // 3
}
}

The activity is in charge of (1) providing its intents, (2) pass them to the binder, and (3) subscribes to the returning view state observable so it can render it and update the view.

Caching the State

Inside the binder, we are using scan to cache the latest state. But once config changes, the activity has to unsubscribe to the view state observable, and our flow has no where to live on. The flow, in order to stay alive, needs not to depend on the activity lifecycle and has be hot. A hot observable that does not care about being subscribed to to do some work; an egoless observable. This means the binder, without any concern of what the activity is doing, has to provide a kind of never ending observable the activity can subscribe to at anytime, including after some config changes.

Subjects

We will need two subjects here. Anyway that is what I ended up with. Both will live inside the binder and are perfectly bound to its lifecycle.

The first subject, the intentsSubject, will represent the source of all intents the view emits. Why using it? Because, the view’s intents, the source of the flow, would otherwise be discarded with the activity’s onDestroy. By using this subject, we created a source the activity would just need to forward its intents to via the binder.

The second subject, the statesSubject, will subscribe right away to the view state observable sourcing from the above intentsSubject, making it hot. Why using it? Because otherwise, with a cold flow, the activity would create a new flow on every subscription. As you and I know well, we do not want a new flow, we want the flow that cached our latest state! By using the statesSubject, the flow is now alive right from the start and independent of the activity’s doing.

The binder now, looks like this

class Binder {
private PublishSubject<Intent> intentsSubject;
private PublishSubject<ViewState> statesSubject;
Binder() {
intentsSubject = PublishSubject.create();
statesSubject = PublishSubject.create();
compose().subscribe(state -> statesSubject.onNext(state));
}
public void forwardIntents(Observable<Intent> intents) {
intents.subscribe(intent -> intentsSubject.onNext(intent));
}
public Observable<ViewState> getStatesAsObservable() {
return statesSubject;
}
private Observable<ViewState> compose() {
return intentsSubject
.map(this::actionFromIntent)
.compose(actionToResultTransformer)
.scan(ViewState.idle(), reducer);
}
}

Back to the Activity

The activity has now only two things to do: (1) forward its intents to the binder to feed the intentsSubject and (2) subscribe to the statesSubjects to render the view.

public class Activity {
private void bind() {
binder.getStatesAsObservable().subscribe(this::render); // 2
binder.forwardIntents(intents()); // 1
}
}

The order matters here. We need to subscribe to it before we forward any intents otherwise all events emitted before subscription will be lost in space.

Initial Intent

There is one more piece I had to implement to satisfy the flow. The activity when forwarding its intents, need to convey that it have bound itself to the binder. I created a Initial intent for this purpose. The problem the binder has to address is how to assess whether the activity bound for the first time in the flow lifetime. We would not want to load again the list data after a config change if the data is within the latest state. We just want to get this last state and render its data.

The activity only needs to emit this intent on creation, nothing funky.

How is the binder gonna know? One thing is sure, we don’t want some isMyActivityOkay flag to mess up with our flow!

Not gonna be able to do it!

Scan is back

There is actually two versions of scan in RxJava2. (1) One with an initial value of the type of the accumulator and some values of some other type. We are already using this one to update our state with a result. (2) The other one does not take any initial value, the accumulator and the values are of the same type, and the function is skipped on the first iteration; the first item is simply passed along without modification. So… if I am inside the scan function, that my intent is instanceOf InitialIntent, then I know it is not the first time the activity bound to the binder and that we just got back from some config change. Yes, that’s right.

Accordingly, we can update our binder#compose as so

class Binder {
private Observable<ViewState> compose() {
return intentsSubject
.scan(intentReducer) // Hiring naming engineer
.map(this::actionFromIntent)
.compose(actionToResultTransformer)
.scan(ViewState.idle(), reducer);
}
private BiFunction<Intent, Intent, Intent> intentReducer =
(previousIntent, newIntent) -> {
if (newIntent instanceof MatchesIntent.InitialIntent) {
return MatchesIntent.GetLastState.create();
} else {
return newIntent;
}
};
}

How About the Binder Lifecycle?

The binder needs not to depend on the activity but its purpose is to be used by it. How does it translate within the activity lifecycle? The solution I came up with is something I have been doing with Dagger components already for some times.

Fact: we want the binder to be instantiated on the activity creation, to be destroyed when the activity is itself destroyed, but with one exception: none of it can happen on config change―even though the activity gets destroyed and created. Fix: to do this, we can use onRetainNonConfigurationInstance or since we all are using app compat, onRetainCustomNonConfigurationInstance. You set the object you want to retain, and can access it right from the activity’s onCreate via getLastCustomNonConfigurationInstance after a config change. This will allow the binder’s instance to bypass the activity re-creation. We can then create a binder instance only when needed and it will be destroyed only when the activity really gets destroyed.

Ending Point

We have a nice flow that survives config changes and there is no state whatsoever outside of our flow!

Fin.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Responses (6)

Write a response