The Case for Single Activity Android Apps

Lisa Watkins
Code With Lisa
Published in
4 min readFeb 11, 2021

Whether you have already adopted single Activity architecture in your Android apps or not, it can be a useful exercise to remind ourselves why this pattern works. To keep things interesting, I’d like to dig a little bit deeper than “because Google said so,” which they did (at Google I/O in 2018).

Before digging in, what is single Activity architecture? As the name suggests, your app has one activity (usually called MainActivity). It’s important to note that this Activity is as lean as possible and completely devoid of business logic. Each individual screen in your app is then represented by a Fragment or a custom View. Newer Android API’s have been built to support this architecture, including the new Navigation Component.

So why only one Activity? Android Activity’s are one of the four Android app components. Although historically this hasn’t been enforced, philosophically speaking, an Android app component exists as an entry-point to your application. An Activity’s siblings include Broadcast Receivers, Services, and Content Providers. All of these app components must be registered in AppManifest.xml. With this observation alone, it’s probably feeling intuitive that every screen in your app does not need to be represented by an Activity, but let’s keep digging.

The logical direct parent of the four app components is the application itself. This application level is the most global scope you can reach in your Android app. Dependencies provided at this level are global to all other instances in your application. This means that if you have multiple Activity’s that each represent a different screen in your app, any shared data would have to be made available to this global scope. Not only do your Activity instances have access to this scope, but so do Broadcast Receivers, Services, and Content Providers. This violates a golden rule in Software Engineering, simply put, “Avoid global state.” Furthermore, if you have a single Activity, the Activity scope becomes a useful, more contained scope for containing dependencies that need to be shared across all UI code. The same Activity context can also be used across all screens in your application with a single Activity architecture, avoiding unintended bugs.

We see this issue of global state manifest in how Google developed architecture component API’s. Let’s take a look at ViewModel.

If you use ViewModel’s in your Android app, you probably know that ViewModel instances can be shared between Fragments, but not between Activity’s. Simply put, ViewModel instances can be used to share data between Fragments but not between Activity’s. But, why?

If you don’t know how ViewModel functions underneath the hood, I recommend setting some breakpoints in lifecycle methods and exploring. To loosely summarize, ViewModel instances are stored in a data structure. When an Activity or Fragment is destroyed, perhaps by rotating the device, the system checks this data structure for an existing instance of the ViewModel. The ViewModel instance is retained and any state owned by the ViewModel is restored.

In the case of an Activity, the owner of the data structure where the instance of ViewModel is preserved is the Activity itself. For Fragments, the owner of the data structure is the FragmentManager. When the Activity is destroyed, so is the data structure that contains the ViewModel instance, and therefore the ViewModel is also destroyed. When the Fragment is destroyed, the FragmentManager lives on, and therefore the ViewModel also lives on.

If you think about the way ViewModel is implemented, it makes sense! Having ViewModel instances saved at the Application scope level would be too global and expose ViewModel instances to the entire dependency graph of your application. The implementation of ViewModel clearly lends itself to a single Activity, multi-fragment architecture and is symptomatic of the Application scope simply being too large.

There are other Android API issues with multi-Activity architecture, such as transitions. Because Activity’s are part of the Android framework, new features aren’t always back-ported seamlessly. We can see this with glitchy transition animations. The most obvious issue reported is a glitchy status bar when navigating between two Activity’s, shown below.

We can also see problems with multi-Activity architecture manifest in third party libraries. Let’s talk about Glide.

Glide is a powerful image loading library widely used in Android development. Glide smartly caches images in memory to improve rendering speed and user experience. A common OOM exception occurs when you use Glide in a multi-Activity apps. If you load an image in memory in an Activity, Glide will not release that image from memory until that Activity is destroyed. If you have several Activity instances on the backstack, you end up consuming a lot of memory for all of the stale bitmaps that Glide has loaded in memory, which can result in an OOM exception.

This example is not necessarily a symptom of the global state issue, but would be avoided if your app adopted the single Activity philosophy adopted by others in the Android community. And its true, you could clean up bitmaps in memory through lifecycle hooks like onStop(). However, adopting a single Activity architecture would allow you to do any necessary clean up at the Activity scope and not the global Application scope!

Whether you are still using multiple Activity’s or you’re already a single Activity architecture convert, I hope the exercise of exploring the “why” behind single Activity apps was worth your while. Happy coding!

--

--

Lisa Watkins
Code With Lisa

Engineer, Activist, Cat Lady. Mobile engineering @ Lyft.