Dissecting Square’s Flow: Understanding the backstack

NOTE: I’ve deprecated Flowless (and have abandoned using Flow) in favor of what I wrote based on that from scratch: Simple-Stack.

So you should keep in mind that I am no longer using this, because Simple-Stack solves quirks that I didn’t like about Flow’s design.

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

If you’re here, you’ve probably worked with Fragments. If you haven’t worked with Fragments, you’ve at least probably worked with Activities.

With Fragments, you’re forced to tinker with the FragmentManager, and methods like `popBackstackImmediate()` and other similar constructors.

With Activities, you try to make the system do what you tell it to do via setting “intent flags” like CLEAR_TOP, REORDER_TO_FRONT, etc.

In both cases, we do not have direct control over the backstack. Sure, FragmentTransactions allow you to *manipulate* the backstack, but it’s still difficult to get it in the state you want.

Let’s say you want to log in, and after you log in, you should forget the `LoginActivity` and proceed to the next view. But instead of to the next view, you want to first open a tutorial activity that shows a tutorial, from which you can navigate back to the “Main” activity once done.

With Flow, you can do this with the following setup:

No need for manually calling “startActivityForResult()” from MainActivity, and “finish()” in onActivityResult() from LoginActivity. State is managed by Flow, and state transition is managed by your Dispatcher implementation.

But how does it work?

There are a few public APIs worth understanding, and there are a few internals that make all the magic work as intended.

Flow

The Flow instance is responsible for holding two things:

  • the History, which contains the list that represents the application state
  • the KeyManager, which manages the State that belongs to each key history element (state meaning the viewstate and an optional Bundle).

Other than that, it also has the Dispatcher instance, that manages “what should happen if the history is changed”, and Flow manages currently active (“pending”) traversals.

Essentially, the Flow class is the core of the backstack behavior in the Flow library, because it’s responsible for containing current state, and managing change from the current state to a new state. What actually happens during a state change is up to the Dispatcher.

It’s important that the Flow instance survives configuration change. As a result, it can queue a PendingTraversal while the Dispatcher is currently unavailable — most notably either before `onResume()` (unless explicitly set), or after `onPause()` — and execute the pending traversal only when the dispatcher is available, thus eliminating the dreaded “IllegalStateException: Cannot execute this action after onSaveInstanceState()” seen in Fragments.

I wonder why the FragmentManager doesn’t queue transactions like this?

Anyways, Flow exposes its History with `getHistory()`, and a few utility methods for manipulating said history such as `set()`, or `replaceTop()`. Afterwards, this history manipulation is queued as a traversal, and these are executed in order when a dispatcher is available. Setting a dispatcher starts any pending traversal that hasn’t been executed yet.

To allow Flow to be accessible within any view within the hierarchy that belongs to a given key, the Traversal is able to create a new ContextWrapper that contains the Flow instance and the Key, and makes it accessible through getSystemService() and therefore Flow.get(context).

Dispatcher

This is an interface we must implement when using Flow. It is responsible for handling state change. Its comment is quite specific on its behavior.

This is also the ugliest part of any Flow logic, because it has to essentially handle any change to the global backstack, and change the active views accordingly. Of course, it’s nowhere nearly as bad as the FragmentManagerImpl. If you’ve ever looked at its source, it’s 2000 lines of code. So compared to that, Dispatchers aren’t nearly as complicated.

The problem with the original Flow implementation provided by Square is that despite the fact that traversals cannot be cancelled, they also don’t get completed when the Activity dies. This can be an issue if you have a dispatcher that handles animation like the one above (based on the previous Flow’s SimplePathContainer), because there are cases when a view’s `onMeasure()` is never called, and the traversal gets stuck.

This is something worth taking into consideration: should the dispatcher completion be bound to animation? Should a traversal be forced to complete when the surrounding Activity dies? In my fork, it was forced — and it resolved certain “deadlocks”.

Anyways, a dispatcher that revolves around swapping custom viewgroups needs to do a few things:

  • Persist view state for outgoing view
  • Inflate new view
  • Restore view state for incoming view (primarily a concern for handling “backwards” direction)
  • Swap them out (remove old view, add new view) ~ and animate if need be

In reality, a Dispatcher doesn’t force you to use these schematics. You just get the “origin” history, and the “destination” history. What you do with it is up to you.

Traversal

Traversals contain the new (destination) and the old (origin) history, and the direction between them. It also allows accessing the container of view state within the KeyManager, so that view state can be persisted by the dispatcher.

Traversals also provide a method that creates a new context wrapper. This wrapper contains the key and Flow instance for the views within the hierarchy.

TraversalCallback

Calling `onTraversalCompleted()` signals to Flow that the Traversal is complete, and if another traversal is enqueued, then that one is executed (if the dispatcher is available).

When there are no more pending traversals left queued up, then the state for the new history stack is preserved, and all other state is cleared.

History

History is just a fancy wrapper around a List of Object. Really.

This list is what represents the state of the application. These objects can be anything, as long as they can be converted to Parcelable in some way or another; and as long as their hashCode()/equals() implementations are reliable.

KeyParceler

When an app with Flow goes into background, then the library listens to `onSaveInstanceState()` event to store the current history stack into a Bundle. To store the keys within the history stack, a KeyParceler is provided.

The simplest KeyParceler returns the key itself as a Parcelable.

KeyManager

The KeyManager is a class managed internally within Flow, never seen directly in the API. It is responsible for storing the `State` instances, mapped to their corresponding `Key`. As mentioned previously, State contains the view state that belongs to the key that belongs to a given view.

In Square’s Flow, it also hosts the ServiceFactories, and does the reference counting for the usage of a given key, to specify from when until when these managed services should exist. Setting up Flow to know the service factory for a given key at Flow’s installation is a pain though, so I’ve never used it. I assume it’d work primarily with annotations.

InternalLifecycleIntegration

The InternalLifecycleIntegration is yet another internal element of Flow. It’s in fact a retained fragment, that is configured via the Installer, then installed into the Activity via an Application.ActivityLifecycleCallbacks once the Activity actually exists (after onCreate()). This fragment therefore survives configuration change — and hosts the Flow instance, and the KeyManager instance. It acts as a lifecycle listener that survives configuration change — and therefore listens in to onSaveInstanceState().

When onSaveInstanceState() occurs, the List of State (which also contains their corresponding key) from the KeyManager is saved into a Bundle.

Theoretically, it can also set the current state based on an Intent so that you can set state based on PendingIntent from a notification (using `onNewIntent()`) if a history is added to an Intent, but this feature is severely broken in the current version of Square‘s Flow :) Then again, this could be done manually anyways.

How does it compare to Fragments?

As you might know, Flow is often regarded an alternative to Fragments. But we must first understand what Fragments give us.

Fragments have:

  • lifecycle callbacks
  • arguments bundle
  • onSaveInstanceState(Bundle)
  • backstack

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

Flow gives us:

  • arguments bundle (via Keys that are parcelable and persisted to Bundle)
  • backstack (via the History stack)

Square’s Flow also gives us:

  • “managed services” (based on reference counting, created by pre-defined service factories) — essentially, scoping of dependencies for a given state of the application

Flow doesn’t give us:

  • onSaveInstanceState(Bundle): the option to add a Bundle to a State managed by the KeyManager for a given Key is there, but it’s not exposed.
  • lifecycle callbacks: Flow does not handle event delegation, despite the fact that the InternalLifecycleIntegration receives most callbacks (onViewCreated, onStart, onResume, onPause, onStop, onDestroyView), and doesn’t receive some others by default because the Activity hogs it (onActivityResult, onRequestPermissionsResult).

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

Technically, this is what I solved in Flowless, originally a fork of Flow.

  • removed managed services, because it didn’t work as I wanted it to work
  • added lifecycle callbacks (onViewRestored / onViewDestroyed, and create/start/resume and onActivityResult / onRequestPermissionResult)
  • added onSaveInstanceState(Bundle)

This is done via event delegation to the Dispatcher from the lifecycle callbacks in the InternalLifecycleIntegration.

I also added some bugfixes regarding using the Design support library, and using the Context in Flow’s views as an Activity (ability to use `getContext().startActivity()` for example).

Although I consider it a work in progress due to lack of proper animation customization, and lacking simple master/detail support. Still, I figured it’s worth a mention.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

Conclusion

Flow allows a simple way to integrate a custom backstack into your view hierarchy. It doesn’t give you lifecycle integration out of the box, but adding it isn’t too difficult either.

As a result of having a list of (immutable and parcelable) objects that represent your state, you gain full control of your application — no more tinkering with the fragment backstack, or hoping that CLEAR_TOP does exactly what you intend.

You can reach any state at any time by setting the intended state, and your dispatcher implementation will handle this as you intend for it to happen. And if you choose so, previous “stopped activities that aren’t yet destroyed” won’t be plaguing the memory when they ought to no longer exist. It’s up to you.

(Reddit discussion:

https://m.reddit.com/r/androiddev/comments/56banf/dissecting_squares_flow_understanding_the/)