The beautiful story of Android developers, multiple Activities, and the chained elephant

Gabor Varadi
The Startup
Published in
8 min readJun 9, 2020

We are often bound to do certain things out of habit, based on what we’ve learned, or have trained ourselves to do over time. A day-by-day routine, we might rely on the same pattern over and over again, each day, because that’s what we did yesterday. There’s a good chance we’ll do the same thing tomorrow.

But why?

Sometimes, some of our habits aren’t particularly helpful, nor effective. Maybe you’re still spending time on a game despite “end of service” coming in June. Maybe you’re still clicking on /r/androiddev out of habit instead of /r/android_devs.

Maybe you’re still using multiple Activities to model multiple screens in your Android application.

What is an Activity anyway?

As originally outlined in the “how should I model my Android application?” document by Dianne Hackborn on the now-defunct Google+,

Activity is the entry into an application for interacting with the user. From the system’s perspective, the key interactions it provides with the app are:

• Keep track of what the user currently cares about (what is on screen) to ensure the process hosting that is kept running.
• Know that previously used processes contain things the user may return to (stopped activities), and thus more highly prioritize keeping those processes around.
• Help the application deal with the situation where its process is killed so the user can return to activities with their previous state restored [to the state they had before they were stopped].

An Activity is a top-level OS component that serves as an entry point into an Android application, launched by the OS in response to a given type of “intent” (that may or may not contain data), and is associated with a Window so that we can show a layout in its content frame.

Even the very first “Hello world!” tutorial you see ends with “how do I start a second Activity?”. You’ve learned that this is the way to do things on Android.

It is definitely one particular way to do things. But is it really the best way?

The mystery of the authentication flow

If you look around and see what developers think about “single-activity applications”, you may find something along the lines of this:

“I still like using two activities. One for login and startup authentication then one for the actual app.”

Definitely an improvement over “an Activity per Screen”, especially if you wish to avoid complexities that arise from concepts such as “task reparenting” or “intent flags”, and instead prefer to have more control over your own application’s behavior.

After all, one could argue that you still reap most benefits of a Single-Activity application, if you can ensure that there is only ever 1 Activity on the task stack at the same time.

But what makes the authentication flow so special that it would need its own OS-level entry point? Can it be started by other applications directly via a special intent? Is it an entirely separate flow exposed to other applications (like ACTION_SHARE, or taking a picture with a camera), independent from the regular flow of the application? Is it supposed to be an independent screen that can render itself even in PIP? Does the OS need to care that this new entry point exists?

If not, (and the answer is “not”), then there can only be one reason: habit.

To model an application differently, we have to abandon the old chains that latch onto us from outdated tutorials, tutorials that preach outdated practices, and the practices we feel compelled to follow even when there are other alternatives.

What else can you do?

As also outlined in the original “how should I model my Android application?” docs:

Once we have gotten in to this entry-point to your UI, we really don’t care how you organize the flow inside. Make it all one activity with manual changes to its views, use fragments (a convenience framework we provide) or some other framework, or split it into additional internal activities. Or do all three as needed. As long as you are following the high-level contact of activity (it launches in the proper state, and saves/restores in the current state), it doesn’t matter to the system.

Which says the system doesn’t care how you organize your flows. But you probably do!

The article outlines some options:

  • Single activity with views
  • Single activity with fragments
  • Single activity with another framework (Conductor, RIBs, Shards, etc.)
  • Multiple activities if really needed

There had been attempts to make creating single-activity applications easy since early 2014 especially by Square (advocating for Views instead of Fragments using Flow and Mortar, some shiny libraries of the time, though not maintained nor popular anymore in their original form).

In Droidcon NYC 2017, Jake Wharton explicitly says:

”One Activity for the whole app. You can use Fragments, just don’t use the backstack of Fragments, because it’s bad.” — Jake Wharton

In May of 2018, the Jetpack Navigation Component was announced, and with that, Google’s official recommendation shifted towards single-activity applications as well.

Today we are introducing the Navigation component as a framework for structuring your in-app UI, with a focus on making a single-Activity app the preferred architecture.

And Fragments have had long-standing issues fixed, including “slide on top of other fragment” type animations that are now handled by the FragmentContainerView.

The Navigation Component offers to hide any and all FragmentTransactions you’d manually need to write, and simplifies access to a NavController from any view.

Yet, people still wonder “if they should make the plunge”. So what’s the issue?

What keeps people from shifting to a Single Activity?

Activity navigation is extremely intrusive. It’s the “easiest to add”, yet it’s the hardest to remove and replace. Once you have startActivity(intent) or especially startActivityForResult(intent, 0) in your code, it’s really tricky to rip it out.

In multi-Activity applications, Fragments communicate through their Activity, commonly directly cast the “one kind of Activity” that can host them. The Activity exists as the shared superscope for Fragments, and they are all coupled. What if we wanted to re-use the Fragments, without their host? They depend on an Activity to handle their communication!

So by opting into using multiple Activities, we’re opting to move towards a rigid architecture that is hard to change. Let’s say I need these Fragments in a ViewPager with tabs. Now I have to move all the logic from the Activity to a Fragment, move all those Fragments to be child fragments, and make the Activity host be the parent Fragment instead (replace any call to activity with parentFragment). There’s a lot of moving parts, it’s easy to break things. Imagine doing that for each Activity-Fragment relation. It’s easier not to do it.

What if we just didn’t get into this mess in the first place?

Shared scopes, result callbacks, and simplified navigation

There are actually multiple approaches to creating shared scopes between screens, and simplifying navigation. Shared scopes can serve as a “subscope” between screens, without relying on the Activity itself to share data or events between them.

Views are inherently nestable (but are not easily lifecycle-aware), Fragments can be nested, and Jetpack Compose will support nesting of scopes through ambients, but currently existing frameworks like either RIBs (by Uber, then Badoo), or the navigation library I maintain (Simple-Stack), or Jetpack Navigation (2.2.0+) can build explicit shared scopes.

Simple-Stack provides “scoped services”, while Jetpack Navigation provides support for “NavGraph-scoped ViewModels” (using the NavBackStackEntry of a <navigation block as the ViewModelStoreOwner and SavedStateRegistryOwner of a ViewModel with a SavedStateHandle).

Once you have shared scopes, result callbacks are fairly straightforward. Just put a pending result in your shared scope, and read it on the previous screen as you navigate back (BehaviorRelay, MutableLiveData, EventEmitter, LinkedListChannel, etc — or maybe just a regular mutable field, saved/restored into Bundle, potentially using the SavedStateHandle).

No need for result codes, bundles, or inter-process communication (IPC), as the shared scope is guaranteed to exist even on back navigation.

And once you have any of these frameworks in place, navigation should be sufficiently abstracted away that all you need to care about is either backstack.goTo(SomeScreen()), or navController.navigate(SomeGraphDirections.someScreen()).

Isn’t that easier than FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK, or FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_SINGLE_TOP | FLAG_ACTIVITY_REORDER_TO_FRONT?

When do you still need multiple Activities?

There are actually a few perfectly valid use-cases for using multiple Activities, where you don’t really have other options.

Multi-window using both panels in a split-screen scenario, or multi-process applications.

It can also make sense for flows that are independent of the main application and are exposed only for other apps, such as ACTION_SHARE, or for the upcoming Bubble API.

But you generally don’t need them just to show a second screen.

What does a Single-Activity app look like?

I’ve set up two fairly simple samples that still show the general idea, both based on the original Jetpack Navigation Component “conditional navigation” documentation (that, surprisingly enough, makes no mention of either popUpTo or popUpToInclusive="true", the two <action properties that actually allow you to do conditional navigation).

Nonetheless, the “First Time User Experience” is an easy-to-grasp example for how to NOT use a second activity just to show a login screen.

Here are the samples:

(There is also the navigation framework called https://github.com/nhaarman/acorn that promises to fully abstract away Android, but unfortunately, I haven’t taken it for a spin. It’s still definitely worth taking a look at, though.)

Conclusion

I hope this article provides some insight on how using a Single Activity can in most cases easily replace what “convenience” a Multi-Activity approach seemingly offers (that being “simpler scoping”, and “lower friction navigation”), without forcing us into one particular pattern that later results in trickier navigation between screens, and otherwise noticeably slower screen transitions or flickering animations.

Why let the system own the state of our app, if we could own it ourselves? Why make IPC roundtrips and ask the system to open a new Window, if we could just swap two views in our layout? Why complicate the lifecycle and make onStop ambiguous, or duplicate views just to share them between screens?

Why get stuck in old habits, when you can choose to make reasoning about your application state easier?

Using Activities to model inter-dependent screen flows is like the chained elephant tied down to a stake, no longer aware that it could be free. Free of Activity intent flags, and unpredictable task stack behavior. But instead, frolicking in the world of shared scopes that still provide proper state persistence across process death (low memory condition).

Special thanks to anemomylos for showing me The Beautiful Story of the Chained Elephant, which served as an inspiration for this article.

Join the discussion thread on /r/android_devs.

--

--

Gabor Varadi
The Startup

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.