Modern Android: Ditching Activities and Fragments

Architecture Patterns in the Realm SDK

You've bravely re-architected your app and brought it into the world of modern Android! Realm's logic and separation of concerns follow a similar pattern.

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.

Fabien-Android-Lifecycles

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:

  1. Single activity architecture
  2. Separate the logic from the views
  3. “Screens” survive rotation, but not views,
  4. 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.

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.


Fabien Devos

Fabien Devos

Fabien Devos has been developing Android apps since before Android 1.0 was released. In Paris, he worked on some of the top European apps (Dailymotion, Le Monde, AlloCiné). He then moved to London to join Lightbox (acquired by Facebook), which brought him to Menlo Park to work on Facebook's core Android team. After working on Upthere's Android app for a while, he is now Android Lead at Wealthfront. He also created Hacked with Joaquim Vergès, a mobile coding game with more than 600,000 downloads.

Transcribed by Hilary Fosdal
Edited by Billy Leet