Master-Detail Revisited: Converting Flow’s sample to Simple-Stack
Back in the day, when Flow 0.9 came out, there was only one flow-sample
. Well, apart from the one in the square/mortar
repository, anyways. Later, flow-sample
was removed from the square/flow
repository.
You may ask, “what’s the point of talking about something that was deleted and forgotten since 1.5 years ago?”
There’s a pretty good reason for that, actually. It is, to date, the only master-detail implementation publicly available and open-sourced, written by Square, using Flow.
I’ve ported this flow-sample
to use Dagger2 and Simple-Stack
instead, because Simple-Stack is easier to understand than Flow 0.12 — as honestly, PathContainer
never made sense.
The repository for the new source is available here.
— — — — — — — — — — — — — — — — — — — — —
Despite the title, I’m not really going to talk about the actual conversion process, it’s all available in this commit. And the terminology between Flow and Simple-Stack (for example, Dispatcher
vs StateChanger
) is explained fairly well in this comment.
Instead, I’m here to tell you about how this master-detail setup works.
Project architecture and components
What we have is essentially:
- data classes (
Conversation
andUser
) - Containers which are essentially custom viewgroups, which can handle a state change in a particular way — which are inflated depending on current configuration (
FramePathContainerView
,MasterPathContainerView
, andDetailPathContainerView
— but alsoTabletMasterDetailRoot
) - A state changer implementation that can swap out and add a new view, while persisting the previous view’s and restoring the new view’s state (
SimpleStateChanger
, originally the SimplePathContainer) - custom view groups for each particular state of our app (
COnversationListView
,ConversationView
,FriendListView
,FriendView
, andMessageView
) - custom application
DemoApp
,that sets up the Dagger2 componentSingletonComponent
- the main activity which hosts the backstack
Paths
, which essentially contains every single key in our application as an inner class. For some reason. I’ve honestly never considered putting all my keys in a single file, but okay!
State representation: Path
Back in Flow 0.9 to 0.12, your state was represented by a Path
. This essentially contained the Key
of the application, and its ViewState
.
In Flow 1.0-alpha (and in Simple-Stack), these two were detached, greatly simplifying the API. So in our case, Path is just a Parcelable
with a title
.
public final class Paths {
public abstract static class Path implements Parcelable {
public abstract String getTitle();
}
Beyond that, our states are all just immutable parcelable data classes generated by @AutoValue
, as usual.
@Layout(R.layout.friend_view)
@AutoValue
public abstract static class Friend
extends FriendPath {
public static Friend create(int position) {
return new AutoValue_Paths_Friend(position);
}
@Override
public String getTitle() {
return "Friend";
}
}
However, what’s interesting is that most keys (except NoDetailsPath
) extend from a class named MasterDetailPath
, which returns its parent, and whether it is a master key.
public abstract static class MasterDetailPath
extends Path {
public abstract MasterDetailPath getMaster();
public final boolean isMaster() {
return equals(getMaster());
}
}
And with that, it describes the parent-child relationship per a given “feature set” in the application.
Master/Detail and handling orientation
Layouts
In order to handle a different view according to different orientation and device size, we can use Android’s built-in resource qualifier system to define a tablet layout for sw600dp-land
, and a phone layout.
These two layouts will however display two completely different view hierarchies: one that handles portrait, and one that handles master/detail flow.
Viewgroups
This is actually where all the magic is hidden, MasterPathContainerView
and DetailPathContainerView
.
They are associated with a subclass of SimpleStateChanger
, which is responsible for preserving/restoring state, and inflating the custom views. But it also selects what layout to inflate based on the key.
In portrait, this is based on the @Layout
annotation.
In landscape, this is a bit more complicated.
- Master Container
The master container does two things:
- it short-circuits (says its state change is complete) if it detects that the master container already contains the view that is associated with this given master key
- selects the master layout from the current top key
- Detail Container
The detail container selects the Paths.NoDetails
path as the source of its layout, if the current key is a master.
- Tablet Master Detail Root
The two viewgroups mentioned above are managed by the TabletMasterDetailRoot
. It holds the two container views, and delegates the state change to them — and waits for them to finish.
This is actually the most complicated class in the whole sample.
The method updateSelection
is what is responsible for making sure our selected item in the master stays selected, according to the index of the selected detail — even after rotation or process death, directly from the top state.
Otherwise, it is responsible for delegating back, making sure that the right view gets the opportunity to handle it if need be. (This is actually not used in the sample.)
The most important part is that it delegates the state change to the inner containers, which can handle it as necessary.
Conclusion
With this, we can finally understand how the original square/flow-sample handled master-detail layouts.
As to whether this is less or more complicated than using only one state changer that manages the views based on current orientation inside of normal viewgroups, without nested delegation — that’s an interesting question.
The repository for the old flow-sample is available here.
The new (and revamped) master-detail Flow sample is available here.