Thorben Primke introduces Flow and Mortar, two Android libraries written by Square, at the Bay Area Android Dev Group. He will not only discuss them, but also go through a sample application, as well as helper classes that he leverages to reduce boilerplate code and handle lifecycle events.
Background (0:00)
This talk is about Flow and Mortar, two libraries by Square. I started using those almost a year ago, when I started working at Jelly. First, I’ll talk why I started using them and how I was introduced to them. For a quick overview of Flow, I’ll talk about fragments versus regular views. Then, I’ll give a little bit of detail about Flow and Mortar. I’ll do a little deep dive into some of the components of the libraries, and see what might be interesting and how it works. Finally, I’ll point out a few things that these libraries don’t include, but are still quite useful for building projects.
Jelly / Super (1:00)
The original app, Jelly, was a Q&A app. I worked with the previous engineer to build it using Flow and Mortar. Then we started to look at Super, the next iteration of the product, and so we set out to build an app that was more fun. We could have returned to using fragments and a few activities. But, I had been using Flow and Mortar for about a month, and I liked the separation that they offered between your views.
Fragments vs Views (2:10)
A lot of times, when you look at fragments, you mingle code for your view logic and your business logic. A few years ago, I worked on an app and increased its speed by converting some activities to fragments. We wanted to make the transition from the thread view very fast, without the overhead of the activities getting created. We moved more detail, some map views, and started using nested fragments. There are more steps in the Android fragment lifecycle than in the activity lifecycle, so we were caught in some places. The fragment manager would help us with the order of the fragments. The order in which we added them might not be the order in which they should be shown. So, you would have to manage that.
Flow / Mortar
Flow (3:37)
Flow provides us a way to describe screens and handle the back stack for navigation. With Fragments, you would use the FragmentManager to manage your fragments. Here, its Flow that manages your screens and determine which screens should be shown. Flow also manages screen history, and persists state if the app goes to the background. Upon returning to your app, Flow will nicely rebuild the history and screens as you left them.
A single flow can also contain subflows. In the same way you can have nested fragments, you may also have nested flows. A particular case that I use it for is to have a main flow, as well as a compose flow and login flow. These flows are quite separate, and do not pollute each other. With one flow inside another, we can manage the state well.
Mortar (5:05)
Mortar is an overlay on the application lifecycle that integrates with the Context’s getSystemService
. It really makes working with dependency injection quite nice. Mortar uses MortarScope
s, which don’t necessarily have to work with Dagger or other systems. In my case, I am leveraging Dagger’s dependency injection together with Mortar to provide any dependencies to my presenters, views, and everything else in my application. If you have everything hosted within a single activity, some of your classes might also need a persistent state. BundleService
is something that I added to expose any Activity’s Bundle
in your application to any class that has access to the Context.
The other appeal of Mortar is the Presenter, or ViewPresenter. These are integrated with the Bundler, and will handle the persistence of any view data that you have. We will see an example later that shows how it keeps state within screens, and persists or injects those to a variety of screens in the flow.
Flow + Mortar (6:46)
Together, Flow and Mortar will make your screens all extend a path. Each screen has its own module. For example, if you go into the detail view, you can pass to the screen the user ID. That user ID lives within the screen and can provide a module injected to the presenter. This way, if the application is backgrounded, it gets written to a Bundle
in onSaveInstanceState
. All we have to save is the ID. The next time your screen is recreated, the same ID is still in the screen and can be re-injected to the presenter.
This leads nicely into a Model-View-Presenter concept. The view and the presenter talk to each other for different things, but they are nicely separated. In this example, we have a chat list screen and its view. The module adds the view and itself to the root module.
@Layout(R.layout.chat_list_view)
@WithModule(ChatListScreen.Module.class)
public class ChatListScreen extends Path {
@dagger.Module(
injects = ChatListView.class,
addsTo = RootModule.class)
public static class Module {
@Provides List<Chat> provideConversations(Chats chats) {
return chats.getAll();
}
}
}
In the view, we have an instance of the presenter. In onDetachedfromWindow
, it provides a call back to let the presenter know that the view has been created. On the presenter side, we can now set up the view based on the chat information that was injected. This is in the onLoad
method. In the view, we also have a click handler on one of the items. It calls back to the presenter, which then uses Flow to do the transition to the next screen. It uses the position that is passed in, which is the ID that was mentioned earlier.
public class ChatListView extends ListView {
@Inject ChatListScreen.Presenter presenter;
...
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
presenter.dropView(this);
}
}
Deep Dive
Setting up the MortarScope
(9:45)
How do you actually set up? MortarScope is set in getSystemService
. There is a service for a Dagger1 injection. The root module @Provides
certain things. The module first includes something in the ActionBar
. In addition, you can expose things like GSON serialization. This is mostly the set up. Nothing else is needed to go in the application class.
public class MortarDemoApplication extends Application {
private MortarScope rootScope;
@Override public Object getSystemService(String name) {
if (rootScope == null) {
rootScope = MortarScope.buildRootScope()
.withService(ObjectGraphService.SERVICE_NAME,
ObjectGraph.create(new RootModule()))
.build("Root");
}
if (rootScope.hasService(name)) return rootScope.getService(name);
return super.getSystemService(name);
}
}
@Module(
includes = {
ActionBarOwner.ActionBarModule.class,
Chats.Module.Class
},
injects = MortarDemoActivity.class,
library = true)
public class RootModule {
@Provides
@Singleton
Gson provideGson() { return new GsonBuilder().create; }
}
Setting up the Activity (11:01)
I moved a bit of code to the Activity, as this is where the BundleService
provides the persistence bundle to classes that are added on to the MortarScope. The onCreate
method is where Flow gets set up, and flowDelegate
integrates some of the activity’s lifecycle events to talk to the history of your view stack in order to get persisted. The path container is also set up here, and is the overarching container that handles the transitions. We’ll see later that the transitions aren’t actually provided. Any UI animation is up to you to implement, so you can get quite unique with setting up animations as you want.
```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
...
ObjectGraphService.inject(this, this);
getBundleServiceRunner(activityScope).onCreate(savedInstanceState);
actionBarOwner.takeView(this);
...
flowDelegate = FlowDelegate.onCreate(
nonConfig,
getIntent(),
savedInstanceState,
parceler,
History.single(new ChatListScreen()), this);
) } ```
We can call to flowDelegate
for onResume
, onSaveInstanceState
, and onDestroy
. At the bottom, dispatch
can add the view to the container and set up the action bar on top. Another thing to notice is that we only have the one activity. The presenters control what is being displayed. To access the action bar in an injectable way, we also have an actionBarOwner
, which is similar to the bundler and hooks into the lifecycle after being created. Any of your presenters can then inject this actionBarOwner
to modify anything necessary for a particular screen, whether that be the action bars, title, or buttons.
```java @Override protected void onResume() { super.onResume(); flowDelegate.onResume(); }
…
@Override public void dispatch(Flow.Traversal traversal, Flow.TraversalCallback callback) { path newScreen = traversal.destination.top(); String title = newScreen.getClass().getSimpleName(); ActionBarOwner.MenuAction menu = new ActionBarOwner.MenuAction(“Friends”, new Action0() { @Override public void call() { Flow.get(MortarDemoActivity.this).set(new FriendListScreen()); } }); actionBarOwner.setConfig( new ActionBarOwner.Config(false, !(newScreen instanceof ChatListScreen), title, menu));
container.dispatch(traversal, callback); } ```
Flow Navigation (13:32)
Flow is responsible for doing your navigation and keeping track of the history of screens. There are a few ways that Flow does this. In the latest release of Flow, there is a set
method. If you want to go to a screen that doesn’t exist on this stack, it gets added on top. If it already exists somewhere in the history, we go backwards. All the screens above it are popped off, and it becomes the latest screen. You can also completely rebuild your history using the helper History.single
. It simple puts a single screen back on your history. Flow has some integration in the activity as far as handling the back button, but if you want to trigger it yourself because the user takes some action in the view, you can use Flow.goBack
.
```java //Moves forward to this screen - top of history Flow.get(getView()).set(new ChatScreen(position));
// Replaces the history with the new history Flow.get(getView()).setHistory( History .emptyBuilder() .push(new ChatListScreen()) .push(new ChatScreen(SOME_VALUE)) .build(), Flow.Direction.REPLACE);
// Single screen history builder Flow.get(getView()).setHistory( History.single(new ChatListScreen()), Flow.Direction.FORWARD);
Flow.get(getView()).goBack(); ```
Screen Transitions (15:14)
As I mentioned earlier, there are screen transitions, but no animations. The Flow’s PathContainer
needs to be extended in order to implement those. The performTraversal
method must be implemented. In this block of code, the performTraversal
method looks at the current screen and executes the animation. In the end, it actually removes the old view, so we add a new one. After the animation, we remove the old screen from the graph as well as from the container. In this way, each of your screens stays within the context object graph only for as long as necessary. Each screen can provide additional things to the object graph within its module, so as soon as you move a step back and remove the screen, its context is destroyed. Anything provided by that module is now no longer around. The memory stays clean.
```java @Override protected void performTraversal(final ViewGroup containerView, final TraversalState traversalState, final Flow.Direction direction, final Flow.TraversalCallback callback) {
final PathContext context; final PathContext oldPath; if (containerView.getChildCount() > 0) { oldPath = PathContext.get(containerView.getChildAt(0).getContext()); } else { oldPath = PathContext.root(containerView.getContext()); }
… } ```
Flow History / State Persistence (16:44)
The Flow history might be something that I am using differently than intended. It keeps track of the states. You can store particular values within your screen which are persisted in the screen’s history. They are persisted to the bundle when they go through a parceler, and you can provide your own custom parceler. In my case, I’m using the JSON parceler to serialize the screen. I can convert my objects from something complicated to a simple JSON string that can be written to the bundle. That makes it quite nice when you have a compose flow, because this compose flow has a state. onSaveInstance
is called for anything in the history.
```java /** * Used by History to convert your state objects to and from instances of * {@link android.os.Parcelable}. */ public interface StateParceler { Parcelable wrap(Object instance); Object unwrap(Parcelable parcelable); }
public void onSaveInstanceState(Bundle outState) { checkArgument(outState != null, “outState may not be null”); Parcelable parcelable = flow.getHistory().getParcelable(parceler, (state) -> { return !state.getClass().isAnnotatioNPresent(NotPersistent.class); }); if (parcelable != null) { //noinspection ConstantConditions outState.putParcelable(HISTORY_KEY, parcelable); } } ```
Subflow Hierachy (19:47)
On the top level, my application is hosted by one flow. That flow is extended by the MainFlow
, but has its own history as far as how you navigate with it. It opens the first screen, which might be a ProfileScreen
, When you can compose, you get dropped into the ComposeFlow
. There, I have added the ComposeState
. From there, you can pass to both the ComposeScreen
as well as the RefineScreen
. If I go backwards, ComposeState will still be the same since I added it in the object graph at the ComposeFlow
level.
To illustrate that, here is the actual ComposeFlow
screen. I have the ComposeState
object on top, which gets created whenever you enter that flow. In the presenter, it is injected as the first argument. This lets me very easily keep track of the state for a particular flow within the overarching flow. The same flow can be injected in multiple places.
```java @Layout(R.layout.compose_layout) public class ComposeScreen extends DefaultScopeNameBlueprintScreen { public ComposeScreen() { }
@Override
public Object getDaggerModule() {
return new Module();
}
@dagger.Module(
injects = {
ComposeView.class,
SuperlativeStatePicker,class,
FaceTaggingUserItemView.class
},
addsTo = ComposeFlowScreen.Module.class
)
@Singleton
public static class Module {...} } ```
Lifecycle Events (21:46)
You can make lifecycles easier by just living within one activity and handling your transitions through Flow, and your dependency injection through Mortar. Oftentimes, you may need access to lifecycle events. However, this is not actually within Flow and Mortar. You may need lifecycle events if you want to integrate to other services for single sign-on, or if you are using your own camera integration. An easy route would be to inject your activity into your presenter and send a callback, but that is not very clean.
A better solution that I have found is to use a lifecycle manager. The activity has the one instance of the LifecycleOwner
that gets hooked into onResume
, onPause
, and onActivityResult
events. Within your presenter, you can get an instance of this LifecycleOwner
and register for it. Now that index carries your ViewPresenter
and implements the ActivityLifecycleListener
. Since there are many different events, you might only need on onActivityResult
. So, we subclass the LifecycleViewPresenter
to include the interface.
In this example, the ViewCycleOwner
is hooked into the presenter. We do a registration in onLoad
and we do an unregistration cell in onExitScope
. I also use this in my CameraCaptureView
. I have an integrated camera, and its state sometimes needs to be constructed. If you pause your application but keep the camera alive, you block the camera for your other apps. I have a persisted state that tells me whether I need to reactivate the camera within the presenter.
```java @Override protected void onLoad(Bundle savedInstanceState) { super.onLoad(savedInstanceState); lifecycleOwner.register(this); onActivityResume(); }
@Override protected void onExitScope() { super.onExitScope(); lifecycleOwner.unregister(this); } ```
Settings (25:02)
The combination of Mortar and Dagger gives us nice dependency injection. We want to leverage that and actually inject all the settings. Instead of a settings manager, we can inject individual settings into presenters as needed. How does that work? I have an additional SettingsModule
with a StringLocalSetting
that extends
AbstractLocalSetting
. I also added providesConfig
with an annotation of “config”. These settings get stored into the shared preferences.
```java public class StringLocalSetting extends AbstractLocalSetting
public class SettingsModule { …
@Provides
@Config
StringLocalSetting providesConfig(SharedPreferences preferences) {
return new StringLocalSetting(preferences, "config");
} } ```
The SettingsModule
gets added to my root module, which in my example we will call ApplicationModule
. I can inject my local string setting via the annotation “config”, instead of having to pass another manager. Since these are provided everytime I do a get
, I get the most recent ones. In addition, to provide hooks back to the presenter, we call the presenter’s dropView
and takeView
to provide the notifications when the view is ready in order to set it up. Another helper that I built takes care of injecting Mortar, and I also use the object graph and Butter Knife to make life easier. I also have less repetitive code.
Good (and Bad) (27:40)
What’s the good or bad? I found that there was no support for new Material style view animations. I only have one activity, and so I would have to build that myself. But, given the flexibility of Flow and animations, it is certainly possible. Both Flow and Mortar are still actively changing. This is good because that moves the project forward. Active development shows that people are still working and thinking about it. The downside is that newly released versions of libraries can cause a lot of API breakage. It’s hard to go from the concept that you are familiar with to a new one. Everything that we saw today was based on the latest template application. A lot of the times only the names might change, while the concepts remain mostly the same. Something that I have been meaning to do is learn how to test presenters. The core is already nicely separated. With Mortar dependency, injection is quite easy around screens and path presenters on my views. I think all of that is a really good benefit.
Q&A (29:15)
Q: Are there any books or resouces that describe this in more detail?
Thorben: Not really. Both Flow and Mortar are repos that have a small readme and somewhat documented code. That’s about all the documentation.
Q: Why do we spend so much time implemented state persistency, when we could just restart the app from scratch? I understand that there is value if your app is killed in the background. In your case, what is the value of saving state?
Thorben: I can think of a lot of examples where you should try to recreate the previous state of your application. For example, if somebody is in the compose flow of creating new posts in Super. If they look up a link in the browser, the app goes to the background. When they return to the app, you will want them to see the state to actually add that link to the post. With any app I build, I try to always recreate it.
Stephan: One reason you want to recreate state is data input. Let’s say you stop typing something and then open something else. When you return to the app, it’s nice to see that your text is still there. Another situation to save state would be when you get a phone call, or listen to an audiobook. There are a lot of good games made by good developers, but they don’t understand lifecycle. You notice it right away when you pause to do something else, and then you have to restart the game. It does make a difference.
Q: Is there any data, or do you have any idea on how often an app is killed while just taking a call? I would not expect my app to be killed by something like just a phone call.
Audience member 1: Your phone might not have enough RAM, so the moment any other app comes up everything in the background is killed. If you have a high-end phone, you won’t notice it. But on a low-end phone, it becomes a really, really big deal.
Stephan: You’re right. On high-end phones, playing OpenGL games would make me kill other applications, but almost nothing else.
Audience member 2: When you rotate your phone, the activity gets killed. So if you’re not saving your bundle on the outstate and then recreating it in the onCreate
, you’re going to lose the whole view.
Thorben: Mortar and Flow help you work with persistence, so recreating your screen is easier. It’s also just a different way to architecture your applications. You have a nice separation between your views and your presenter, as well as a separation in the logic. You also might want to go backwards. For example, if you go from a list to a detail, you might want to create that list in the same position when you go back.
Q: When you downloaded an example, did it contain Mortar?
Thorben: There is an example on Flow just by itself, as well one in the Mortar repo that includes both. There is one for Dagger1 and another for Dagger2.
Q: I think this is really interesting for a new project because you are going to change the structure of your views. Is this really integrated your project?
Thorben: I started with a project from scratch, but it depends on how large your project is. If you want to convert your views, it will be easier or harder depending on how your views are already structured. It’s also worth using this project because there are a group of people working on it, and more people are adopting it. It’s probably not worth trying to do this own your own. But, I don’t know if it’s worth switching your product over if you are already pretty far along in the development cycle.
Q: There are libraries out there that are trying to deal with lifecycle methods and make it easier on Android, one is RoboBinding. It seems like you’re building a big dependency on Flow and Mortar, and losing a lot of control for the future.
Thorben: I think this always happens when you go with a particular solution. I’m still using older versions, and the APIs have changed. It’s quite the undertaking to move this project over, but I need to do that eventually. I think using these libraries was good for the original Jelly app, and then we stuck with using them because it seemed attractive. There is definitely some technical debt that has now been accumlated to the effect that the latest versions are changed quite a bit as far as classes, methods, and naming.
Q: Can you go a little bit more in depth on why this approach is better than fragments? Instead of MVC, I know there is MVP, and Dagger makes your code more abstract. Besides architecture, is there any performance benefit?
Thorben: One reason that I mentioned earlier is if you actually have these nested fragment situations and have to manage them yourself. This solution was attractive because of the problems that I have run into in the past. I’m not really encountering any problems around my state or recreating my application anymore. Now, people are used to using activities and fragments. With faster devices, the overhead of creating them isn’t so bad anymore.
Audience member 1: Square doesn’t like fragments, and they don’t use any of the UI that comes with that framework. They just like to do things their own way, and they have awesome projects. But this one seems like a radical approach.
Audience member 2: We have to decided to move from fragments to custom views just to test easily. Part of our problem comes from testing fragments, and testing our custom views is super easy. In our application, at least 30% of the crashes happened because we finish our task in the background. In a fragment our app would crash, but in a view, probably not.
Q: How do you set up the layout files? Is it just like a regular linear layout?
Thorben: Yes, it would be whatever you normally would write. On the top of your screen, you would include whatever your layer file would be. It handles that inflation underneath. That was removed from the original Mortar Flow Repo, but it’s now part of the sample project. You can now annotate your screens with whatever your layout file is. So, just regular Android layout xml.
Q: RoboBindings was mentioned as an alternative library, in comparison to Mortar. What else is out there for when you are looking for alternatives to fragments?
Audience member 1: Nucleus.
Audience member 2: I don’t think of any other. RoboBinding is an MVVM library pattern for Android. It has been around for a couple of years. It handles all the callbacks to the view. It is fairly radical in the way that you have to architecture your app.
Q: I saw you had a Dagger module marked as singleton scope for the presenter. How long does it live? Does it live over the entire application?
Thorben: No. Those should be torn down or destroyed whenever the screen goes away.
Q: If you wanted to, can you put a view inside one fragment?
Thorben: Yes, your view could have fragments inside. If you’re converting an application and you have it in fragments, you can still switch to this and just have your views be wrapped around your fragments.
Q: How do screens work for tablets? Do you inject different views on them?
Thorben: You can. I suppose it would be the same as if you were just doing that with other layer files. I actually haven’t developed for the tablet, or used it for that. But, I wouldn’t see it working any differently with regards to how you build your layout files and include them in different resource folders. Since this screen is annotated with a particular layout, it would create a specific view that should reflect whatever device you want it on.
Q: Is there support for dialogues?
Thorben: There is support for those as well. Besides the regular view presenter, there is a pop-up presenter. We use it in pretty much the same way, navigating to and away from it.
Receive news and updates from Realm straight to your inbox