Queueing state changes and handling onPause() — Creating a Flow-like custom backstack (Part 3)
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
onCreate(), and unregister in
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:
- initialization when we set the new
- a boolean flag that shows whether we are currently changing states or not, and we say
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
initializeit, or just
reattachit (without running the initializing state change)
- the ability to queue the state changes when a
StateChangerisn’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
initialization. With these parameters, we can easily queue them up in what is essentially a
A state change can now be in three states:
We will call these pending state changes that we can enqueue as
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
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
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
- handle queueing up
StateChanges if a state change occurs during another active state change, or after
Source code for this article can be found here.
The next part will show how to handle view-state persistence.