aSaying “No” to Fragments (and Activities): Creating View-driven applications with Flow
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.
In fact, this article probably isn’t even worth reading anymore.
Read this one instead: https://medium.com/@Zhuinden/simplified-fragment-navigation-using-a-custom-backstack-552e06961257
— — — — — — — — — — — — — — — — — — — — — — — — —
I’ve actually written a previous article before about Flow (and Flowless), but it was rather bland. It’s time for a new and better article, a bit more straight to the point. I had also written about using Flow 0.12 and Mortar, but that should be considered obsolete — the architecture of Flow 1.0-alpha is much better and preferable (because it manages state persistence, while the previous version did not).
The basics
For those who haven’t been keeping track of just how much Fragments have been hated for quite a while now — well, nobody likes the lifecycle, and nobody likes how they work strangely with the CoordinatorLayout, and nobody likes how the FragmentManager transactions are a chaotic mess, and reading the source code doesn’t really help.
So with that in mind, Flow was created to take over what the FragmentManager intended to do — be a backstack (like what addToBackStack(null)
gives you), make sure the views properly get restored on configuration change and process death; but without the weird bugs that nobody really understands.
Why would there be no such weird bugs, you ask? Because all transition from “State A” to “State B” is handled by you — the user of Flow.
Some history, and what’s new
Flow is actually quite old, although due to its lack of documentation and scary sample codes, it never really got adapted. There were even articles about it (well, Square kinda left that hanging at Flow 0.8).
In order to understand how Flow has changed over time, let’s look at an example from that article for Flow 0.8 , and how we’d do the same thing with the latest Flow.
State representation
Back in Flow 0.8, every view that was shown was described by an Object
, where this object was annotated with @Screen
. This screen contains the layout that should be shown when this object is set to the backstack.
@Screen(layout = R.layout.album_view)
public class AlbumScreen {
private final int albumId; public AlbumScreen(int albumId) { this.albumId = albumId; }
public int getAlbumId() {
return albumId;
}
}@Screen(layout = R.layout.track_view)
public class TrackScreen implements HasParent<AlbumScreen> {
private final AlbumScreen albumScreen;
private final int trackId; public TrackScreen(AlbumScreen albumScreen, int trackId) {
this.albumScreen = albumScreen;
this.trackId = trackId;
} @Override public AlbumScreen getParent() {
return albumScreen;
} public AlbumScreen getAlbumScreen() {
return albumScreen;
} public int getTrackId() {
return trackId;
}
}
This actually hasn’t changed much, other than that @Screen
(and HasParent
) isn’t provided to you by the library anymore. You’re just told to use any Object, whatever Object you want; as long as it has equals()
and hashCode()
methods.
Of course, it makes most sense for the key to still tell you what layout you’d like to build. It’s also easiest if you make your keys Parcelable (or provide a KeyParceler
that can turn them into Parcelable, but that takes more effort).
Without further ado, the way you’d do this now in Flow 1.0 is the following:
You could also choose to provide the view’s layout identifier using an annotation if you want, you just need to also provide the logic that would extract the value from it.
(Note: With current Flow 1.0-alpha, it might also be a good idea to extend TreeKey
instead of making a HasParent
annotation, if we want to make Flow handle “managed services” and reference counting — although I personally don’t use that, because I’ve had issues with it.)
Navigation handling
Back in Flow 0.8, you had to implement the Flow.Listener
interface, which gave you the newBackstack
of objects (annotated with @Screen
), and the Direction
as to whether you went backward, forward or replace.
@Override public void go(Backstack backstack, Direction direction) {
Object screen = backstack.current().getScreen();
setContentView(Screens.createView(this, screen));
}
This has changed a bit, because Screens
and @Screen
no longer exist, and more importantly, Flow is no longer synchronous — it’s callback-based. This is to allow animations to be handled, and state change would be “committed” only once the animation is actually complete. And even more importantly, you also receive both the previous and the new stack of objects.
In Flow 1.0-alpha, you need to implement the Dispatcher
interface (previously Flow.Dispatcher
), with the following signature:
public interface Dispatcher {
/**
* Called when the history is about to change.
* Note that Flow does not consider the Traversal to be finished,
* and will not actually update the history,
* until the callback is triggered.
* Traversals cannot be canceled. * @param callback - Must be called to indicate completion of the traversal.
*/ void dispatch(@NonNull Traversal traversal,
@NonNull TraversalCallback callback);
}
Which, if we wanted to stay as simple as the article was, would look like this:
As for the Context magic in the middle, that’s exactly what Screens
used to do.
— — — — — — — — — — — — — — — — — — — — — — — — — — — —
However, it’s worth noting that back in the day, Flow
did not handle viewstate persistence. You were entirely on your own in that regard. Now, Flow provides a State
class to which you can persist the state of your view, which is then stored into Flow’s very own History
stack.
With that in mind, we ought to create a view group that contains our view which we’re currently showing (which I’ll call root
), so that we can easily access the view directly, and obtain/restore its state.
With that in mind, now not only do we have a backstack, but we also have state persistence for the views. Rotation and navigating away from our view won’t remove our viewstate anymore, and Flow handles most of the work internally. Neat! :)
This is also where animating between the two views would be handled — for example, we could use TransitionManager.beginDelayedTransition()
if the previousView
exists.
Switching between application states
Remember this?
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, FeedFragment.newInstance());
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
Well, back in Flow 0.8, it used to be this:
flow.goTo(new Track(albumScreen, trackId));
And now in Flow 1.0-alpha, it’s this:
Flow.get(this).set(new Track(albumScreen, trackId));
Using set
makes Flow check if the given key already exists (with equals
), and if it does, then it sets the state back to that view — and all views afterwards have their history cleared. If it doesn’t exist, then it’s a new state, and appends it to the end of the history stack. It’s essentially CLEAR_TOP, except it actually works. That’s what determines the destination
History stack you see in the traversal.
If you want a bit more fine-grained history modifying, Flow gives you that as well:
The scary stuff from the old versions: Mortar/Dagger
The old Square “big picture”
The examples for Flow 0.8 looked scary primarily because of Mortar and Dagger1. If you ask me, Mortar’s state persistence through the ViewPresenter was conceptually flawed, and Dagger1 is superceded by Dagger2. Still, it might be worth understanding to know what problems we’re trying to solve, even though it’s pretty much obsolete, or handled externally by the Dispatcher implementation.
@Screen(layout = R.layout.album_view)
public class AlbumScreen implements Blueprint {
final int albumId; public AlbumScreen(int albumId) { this.albumId = albumId; } @Override String getMortarScopeName() {
return "AlbumScreen";
} @Override Object getDaggerModule() {
return new Module();
} @dagger.Module(addsTo = AppModule.class)
static class Module {
@Provides Album provideAlbum(JukeBox jukebox) {
return jukebox.getAlbum(albumId);
}
}
}
And afterwards, a Presenter is defined, which receives its dependencies injected from Dagger’s ObjectGraph, retained by Mortar’s scope:
@Singleton
public static class Presenter extends ViewPresenter<AlbumView> {
private final Album album; @Inject Presenter(Album album) { this.album = album; }
And the view:
public class AlbumView extends FrameLayout {
@Inject AlbumScreen.Presenter presenter; private final TextView newNameView; public AlbumView(Context context, AttributeSet attrs) {
super(context, attrs);
Mortar.inject(context, this);
Of course, this Blueprint
could have also been an annotation like @Blueprint(scope = "AlbumScreen", module = AlbumScreen.Module.class)
. Later, it was removed from Mortar as a library.
What is Mortar?
BluePrint was back before the Mortar 0.17 API quake, when Mortar was bundled with Dagger1 — it was essentially a service locator where a scope for the LinkedHashMap
was identified by a String key, this scope survived configuration change, and by overriding getSystemService()
, it used to provide the hierarchical scoping of dependencies (think @Singleton
and @ActivityScope
with Dagger2) and seeing them within the entire view hierarchy, while also preserving them between rotations.
Mortar also took upon itself to be a “Presenter” for the views (the ViewPresenter<V>
), and handle the delegation of onSaveInstanceState()
from the Activity in order to persist the state of the presenter into the Activity’s Bundle.
The problems of Mortar
Mortar did NOT integrate with Flow at all — you navigated forward in Flow and navigated back; and Mortar could not persist the state of the view because it never received a callback from activity.onSaveInstanceState(Bundle)
. All view state was lost between navigation changes.
As a result, Mortar’s ViewPresenter was flawed for multiple reasons — and this is exactly why Flow itself started handling state persistence.
Flow and state persistence (beyond viewstate)
But Flow only provides a direct way to save viewstate — even though it has a slot in history that would allow saving a Bundle along with the view state. While Flow doesn’t provide a way (yet?) to access this Bundle, it’s possible to use this with some package-internal logic, with which you can manually save the presenter state through the view to a Bundle.
And then you can call
Now your View is also able to persist its state into a Bundle on rotation.
Mortar as a service locator, and “ManagedServices”
Mortar also provided a way to retrieve the Dagger object graph from the scope bound to the context, and the scope could be created and destroyed manually, independent from the Activity lifespan itself.
This was replaced by Square’s Flow 1.0-alpha as the ManagedServices
and ServiceFactory
, I personally don’t use these at all. They would solve the issue of creating and destroying services, and sharing them between view states if the keys belong to the same “flow”, thus sharing resources between the views.
If this worked it would be great, but I’ve run into cryptic bugs with the “node reference counting” and TreeKey
— so I skipped it.
But yes, the ServiceFactory
is what would replace Mortar.
The new big picture using Flow
Now that we have a backstack with state persistence, we’re able to create views that represent our active window, rather than having to rely on Activities to do it.
We also have fine-grained manipulation of this backstack, rather than trying to make Android do what we want it to do with magical intent flags.
Our intents are replaced by key parameters, and fragment transactions are also replaced by the keys themselves. All state change is managed by the Dispatcher, which provides predictable behavior rather than having “unattached stale fragments”. Our Activities and our Fragments are replaced by Custom Viewgroups, eliminating complexity and providing better performance.
With minimal configuration, we can now create view-driven applications, like so.
- Activity
- Key
- View
- Presenter
Fairly simple separation between the view and presentation — cleaner than either Activity or Fragment.
Why are we still fighting Fragments, when all we really need is a separate state representation that survives configuration changes?
While you can use Conductor, I have not used it. I prefer the fine-grained control that Flow provides — and directly understanding what’s happening underneath.
(Reddit discussion: https://www.reddit.com/r/androiddev/comments/5awo5r/saying_no_to_fragments_and_activities_creating/ )
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!