Activities get destroyed when you rotate the screen, and they have a complex lifecycle. Using fragments is error-prone, and their lifecycle is even worse. How would you like a screen that doesn’t get destroyed on rotation, and a navigation that’s as simple as calling goTo(screen)
? That’s what modern Android development should look like.
Introduction (0:00)
Hi, I’m Fabien, and I’m the Android lead at Wealthfront. In this post, I’ll take you on a journy of how my team at Wealthfront went from the old school View, to fragments, to what I call “modern Android.”
Lifecycle (0:50)
The lifecycle is a basic tenet of Android development. First, an activity is created, then started. Then it’s resumed, and that means it’s visible. It can also be paused, and that means it’s partially visible. It can be stopped and then destroyed.
That sounds great, but in practice, there are all kinds of edge cases.
For example, it turns out that onDestroy
might not be called in certain cases. At any given point in the process of destroying a new activity, the activity can be killed by the process being killed. The guarantee is onPause
and onStop
, but onDestroy
is not guaranteed to be called.
And it gets worse. There are all the optional methods too, for edge cases.
Then we have fragments, which are just insane. In some edge cases, it not only doesn’t behave as you would expect, but also it changes. The lifecycle has been changing over the different versions of Android.
I decided that with the new project I was starting at Wealthfront, I wanted something simpler. I decided to try another way. Wouldn’t it be nice if we had something that looked way more like the original, simple lifecycle we first learned?
Problems (3:54)
Complex lifecycle
You can try to solve certain aspects of it, but there is so much to be understood that there are always multiple problems.
Passing objects requires serialization
It depends on your app, but I don’t always want my objects to be serialized. Sometimes I have them there in memory and I just want to pass that to the next screen.
Destroyed on rotation
I know you can set the flag and everything, but that creates its own set of problems. It’s not really a solution.
Poor control of the backstack
You want to be able to say, “this is my app, I want to go to that screen,” or “forget that one screen, that was a temporary screen and I don’t need it in the backstack and all.” That stuff should be very straightforward, but it’s not.
Solution (5:42)
There are a lot of great projects out there trying to create similar solutions, and the community is really heading in that direction.
Here’s my proposed solution:
- Single activity architecture
- Separate the logic from the views
- “Screens” survive rotation, but not views,
- Write our own navigation system
I only need one activity, and I’ll deal with the view myself. It’s very important to separate your logic from the view in a good architecture. I also. want my screen to survive rotation, but not my views. When you rotate, I’m expecting the logic, and the data part stays. The view is just destroyed and recreated in order to be redrawn at the right dimension. Finally, we’re going to have to write our own navigation system; there’s no way around that.
Single activity architecture (7:12)
With a single activity architecture, you simply have one activity. You have your view for screen A, and then instead of having a new activity for screen B, you simply swap the view.
Screens and views (7:39)
In my architecture, I’ll refer to screens as the logic part of the app. You’ll implement something like a Model View Presenter (MVP) pattern, which is what inspired me. You’ll have a presenter in the middle of a model and a view: the model talks to the presenter, and the presenter talks to the view.
This is similar to a very old pattern of architecture that we’ve all seen for ages: the onion model. You design your code with layers, and each layer only talks to the layer below it or above it.
In my terms, though, we’ll throw away the titles and instead have a Data Screen View pattern. There’s data with logic in the middle, and there is your view to present that data.
The logic lives in the screens, which means the views are dumb. I repeat, the views are dumb. Don’t put logic in your views, otherwise you are violating this single responsibility principle and the separation of concerns. Having views be dumb is a very important thing to make the model work.
You can build your screens out of regular Java objects, which in turn means that screens are easy to test. They’re not tied to the Android framework; it’s simple Java objects that you can instantiate and test.
That has the side benefit of making the screens injection friendly, if you want to use dependency injection.
Navigation (11:04)
When I designed the system for Wealthfront, I had some core goals in mind. I really wanted full control of the backstack, I wanted to handle animations automatically, and I wanted my screen to be connected to the activity lifecycle.
I wanted a single activity architecture because I can’t throw the activity away. I need the activity to draw something on screen and to integrate with the rest of the Android system. I still need an activity, and I need my screens to be tied to the lifecycle.
I want navigation to be as simple as calling goTo(thisScreen)
. How hard can that be?
We decided to call the project Magellan, because the main objective was to build a navigator. The navigator’s role is to keep track of screens and animate between their views.
Screens survive rotation, but not views. The navigator has a stack of screens, and is connected to your main activity. The navigator can ask the screen to get a view, so there is a view for your screen that lives in the main activity. When you rotate, you let the main activity be destroyed by the system, and the view gets destroyed and redrawn as well.
But the screen survives. There’s a new main activity and a new view, but not a new screen. Navigating from one screen to another involves pushing on the stack. On the navigator, I push a new screen. This new screen is going to be capable of giving you a new view. I get that view, I do a nice transition, and I replace the view. If I want to navigate back, I just ask the screen currently on top of the stack to give me a view.
We actually use this in production, this isn’t a hypothetical.
navigator.goTo(new MySimpleScreen(stuff));
You might notice that this is a regular Java object with a regular constructor, and that means you can pass a reference to an object. You can pass the data you need to the screen directly just using plain Java.
If you want to do fancy stuff, you can override the transition. This allows you to do all the cool stuff that you might want to do.
navigator.overrideTransition(transition);
To obtain the navigator, you set a root screen. There’s always one screen at all times in your stack, otherwise the navigator will throw. The API enforces that.
Navigator
.withRoot(new HomeScreen())
.loggingEnabled(BuildConfig.DEBUG)
.build();
There are multiple methods on those. You can call goBack
to go back to a specific screen.
navigator.goBack();
navigator.goBackTo(screen);
You can replace a screen.
navigator.replace(screen);
If the user is not logged in, I want a log in screen. Maybe if they are logged in, I want a home screen.
navigator.rewriteHistory(new HistoryRewriter() {
@Override
public void rewriteHistory(Deque<Screen> history) {
if (!authenticator.isUserLoggedIn()) {
history.clear();
history.push(loginScreen);
} else {
history.push(homeScreen);
}
}
}
);
How do you implement the screen?
public class MySimpleScreen extends Screen<MySimpleView> {
@Override
protected MySimpleView createView(Context context) {
return new MySimpleView(context);
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.my_screen_title);
}
@Override
protected void onViewCreated() {
getView().displayStuff(stuff);
}
}
You inherit from Screen
, and you have a view type in order to be type safe.
You create a view which you can instantiate and flood wherever you want. We have a whole bunch of utility things too, like getTitle
which will automatically set the title on your action bar.
Of course, we still need to be tied to the lifecycle of the activity, but we limited that to the methods we really truly needed like onResume
and onPause
.
public class MySimpleScreen extends Screen<MySimpleView> {
@Override
protected void onResume(Context context) {
}
@Override
public String onPause(Context context) {
}
...
}
The onResume
and onPause
are actually called at the right time, so we will call them when you transition from one screen to another, even though the real onResume
is not called in your activity.
Implementation (18:18)
Implementation is the problematic part, because what we’ve seen is just the API. This is where it gets little bit tricky, but I have a few tips and tricks for you.
There should only be one navigator
Multiple navigators aren’t great. Inject the navigator as a Singleton, or use onRetainNonConfigurationInstance()
in order to keep your navigator around. For this model to work the navigator has to survive. If it gets destroyed with the activity, that defeats the purpose.
Use a simple Dequeue (basically a Stack) for the backstack
This way, you can pop and push on it.
Do not let the user change the backstack in the middle of a transition
This is probably why there was originally not a very good way of touching your history, because it’s kind of tricky. You don’t want the user to do stuff that will put the system out of balance.
Always keep the lifecycle balanced
If I call onViewCreated
, I need to make sure that onViewDestroyed
is called too. This is not the case for the regular activity, because onDestroyed
is not guaranteed to be called, but we do want that behavior. Make sure that you have tests for it and keep track of what you’re doing.
Use functional programming for backstack operations
You don’t have to use those fancy transformations if you don’t need them, but even in the implementation details, when you go to a screen there’s actually a function that simply pushes a new screen on the stack. When you go back, there is a function that just pops off the stack. It makes the implementation much nicer if you think about it in terms of functional programming.
Everything should be synchronous, but post when navigating
There is no way around it, I already tried. In particular, if you’re in a dialogue and you press a button to go to a different screen, you need to post because you’re going to start to make the view move around. That can actually crash because the system is trying to dismiss the pop up for you.
Use a ViewTreeObserver
to wait for the view to be measured
It is this global thing that’s kind of nasty, but it’s needed to start animating. As long as you remember to remove it, there is no problem with it.
There is no guarantee that onDestroy
will be called
One solution is to use a WeakReference to the activity. You can not rely on the onDestroy
to do the clean up.
Future (22:56)
You may be thinking that this is way too much trouble. It’s not perfect. For example, we have no automated way of restoring the backstack if the app is killed, and support for multiple screens is not currently supported out-of-the-box. We don’t have the material animation out-of-the-box either, but we wrote one.
However, we’re going to open source this soon. You don’t have to use it as a library; you can just fork it if you really want some customization, or if we’re not taking pull requests fast enough for you.
In our actual app, we have support for a lot of things that probably won’t be there for the first version that we open source. We created very simple things like an Rx screen that automatically unregisters your RxJava observables for you, or a tab screen that automatically uses tab layout if you have tabs.
Receive news and updates from Realm straight to your inbox