Queueing state changes and handling onPause() — Creating a Flow-like custom backstack (Part 3)

Gabor Varadi
5 min readJan 21, 2017

--

In Part 1, we’ve created a very basic backstack which allows changing state from one state to another, one at a time. We also made the assumption that we register the Activity as a StateChanger in onCreate(), and unregister in onDestroy().

In Part 2, we’ve made it possible that the Custom Viewgroups we inflate have access to our Backstack via getSystemService(), and the current “state” is bound to the view that it belongs to using a custom ContextWrapper, thus enabling the ability to provide parameters to our custom views.

I’m not the first one to use this approach, it originates from Mortar, and you can read about it from actual Google Developer Experts as well.

In Part 3, we will allow queueing up multiple state changes — in case for example an asynchronous operation completes in the background, the event is delivered to the Activity, but it happens during synchronous view animation and the state change is already in progress.

Technically, we need this to make our Backstack more stable.

The original version of our Backstack

I’m going to copy the complete code of the initial Backstack here, so that we have a backup of it and state the issues about it (you can skip this if you want).

So what we have here is the following:

  • working goBack(), goTo(), setHistory() methods
  • initialization when we set the new stateChanger
  • a boolean flag that shows whether we are currently changing states or not, and we say IllegalStateException (or just return true; in case of goBack()) if there’s already one in progress

However, this can cause problems long-term:

  • if the state change is asynchronous (for example has a ViewUtils.waitForMeasure(View, OnMeasuredCallback) call in it to support view animation), then it is possible to schedule multiple state changes at the same time
  • if the app is in background, but we receive an asynchronous callback/event after onPause() which would for example open a dialog, then we would crash with IllegalStateException: Cannot execute this action after onSaveInstanceState().

So what we need is:

  • the ability to temporarily detach and re-attach our StateChanger, with the option to either initialize it, or just reattach it (without running the initializing state change)
  • the ability to queue the state changes when a StateChanger isn’t available, or is already executing a state change

Adding the ability to detach/reattach the StateChanger

We must add a way to attach the StateChanger to the Backstack without initializing it. This is fairly straightforward:

But now it is possible to receive state changes while the Activity is open, but it is not registered as a StateChanger. We must queue events that we cannot execute immediately.

Adding the ability to queue state changes

Currently we’re just executing this and creating a callback that will eventually complete the state change, right?

Our parameters are newHistory, direction, and initialization. With these parameters, we can easily queue them up in what is essentially a LinkedList.

A state change can now be in three states: ENQUEUED, IN_PROGRESS, and COMPLETED.

We will call these pending state changes that we can enqueue as PendingStateChange.

But we have to handle a whole bunch of cases that arise from the ability to detach and attach the state changer. For this, we use Flow’s ReentranceTest as reference.

— — — — — — — — — — — — — — — — — — — —

Reference reentrance tests

Reentrant Go

During active state change, it should be able to queue the next state that should occur once this current operation is complete.

Reentrant Go Then Back

During active state change, it should be able to queue the next state, then queue an operation that goes back from that new state.

Reentrant Forward Then Go

During active state change, it should be able to queue goTo() properly during setHistory().

Reentrance Wait For Callback

During active state change, each state change occurs only when its corresponding state change completion callback is called. So state changes cannot get skipped.

Calling state change completion callback twice should throw

…an illegal state exception.

Initializing state change (“bootstrap traversal”)

If there’s no other queued up state change, then calling setStateChanger(INITIALIZE) should begin an initializing state change.

Pending traversal replaces bootstrap(!)

If there is an enqueued state change, then it should be used as initialization instead of a bootstrap traversal.

All pending traversals fire

If there are multiple traversals queued up before the state changer is set, then they should all execute.

Clearing dispatcher mid-traversal pauses

If a second state change is queued up, but at its execution the state changer is not available, then it should only be executed when a new state changer is available.

Handle state changer set in mid-flight waits for bootstrap(!)

If the state changer is swapped out while a state change is in progress, then the bootstrap of the new state changer waits until the previous state change is finished.

Handle state changer set in mid-flight with big queue needs no bootstrap(!)

If the state changer is set to a new StateChanger while a state change is in progress, then when setting a new state changer, and there is an additional enqueued state change, then the enqueued state change will handle the initialization, and therefore no bootstrap is needed.

Traversals queued after dispatcher is removed then bootstrap the next one (!)

If the state changer is set to a new StateChanger while a traversal is in progress (there is only one active state change, that state change is in progress, and no other state changes are enqueued, and the state changer is swapped out), then a bootstrap traversal is needed after that state change is finished.

The final result

When that’s all done, we can see that the tests complete with success!

And with that, our new code looks like this:

And with that, we’re actually one step ahead — we can now handle the following problem:

  • how to make sure you cannot have state changes after onPause()
  • handle queueing up StateChanges if a state change occurs during another active state change, or after onPause()

Source code for this article can be found here.

The next part will show how to handle view-state persistence.

--

--

Gabor Varadi

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.