Reflecting on Android application development
In this article I want to go back to the Android system as of Android 1.0, and look at the reasons that make Android app development what it is today and why certain decisions were made. Back in 2008, mobile devices were not the almighty powerful devices with 4GB of RAM and octo-core processors we have in our pockets today. Instead, storage was limited, memory was scarce and the network was slow. Despite of all this, users were able to switch between multiple applications on their phones, ‘talk’ to other applications to request data, and you might have guessed it, even take phone calls!
I will be reflecting on the fundamentals of developing applications for Android, looking at application states, lifecycles, monolith activities and modern approaches.
TL;DR
- Dealing with process deaths is important;
- Lifecycles are useful, not difficult;
- Dealing with Fragments is hard;
- Single-Activity approaches are rising;
- The back stack is hard.
In-app navigation
A typical Android application has more than one screen to present to the user.
A messaging app for example could start with a screen that shows a list of message threads. By clicking a specific thread, the application navigates to a new screen that shows the thread and allows the user to send messages.
From here, the user might even be able to go to a screen that shows the details of the contact he is messaging with. To return to the first screen, the user can simply press the back button a couple of times.
In the beginning of Android app development, this was typically done by defining an Activity for each screen in the application.
The OS starts the ‘main’ Activity when the app is launched, and by navigating through the application Activities are pushed and popped off a stack, which is where the name ‘back stack’ comes from.
Additionally, developers can use flags when starting Activities to manipulate the stack.
Multitasking and process deaths
Multitasking in Android meant that you could switch between apps. You could be browsing a webpage and decide to switch to the messaging app to send a message, or even get an incoming call and take the call. Often this switching between apps resulted in the OS running low on memory, and it needs to reclaim some by ending apps that were put in the background. When the user navigates back to that app however, he expects the application to not have lost its state. He could be in the middle of typing a message, or he could have navigated all the way down to a screen that displayed details of a product he wanted to buy.
Even within a single application, a large Activity stack could lead to bottom Activities being destroyed to reclaim memory. Losing this state over and over again would result in some very bad user experience.
To facilitate this, Android allows Activities to save their state as soon as they are put in the background. This way, an Activity can store that message the user was typing, and even store the exact position of a ListView it was displaying. When the OS kills an application to reclaim memory, information about the Activity stack and their states are persisted, and when the user returns to the application the stack is restored and Activities receive the state they saved.
By default, Android did and does a pretty good job at saving state. Activities automatically save the window hierarchy state on onSaveInstanceState
. You only need to make sure the views that need state saving are reachable from the root with all parent views having their ids set.
Configuration changes
Usually when an application is put in the background, its Activities are not destroyed directly. If the user returns to the application when it has not been killed yet, the same Activity instances are used and there is no need to restore anything. And because of the fact that process death doesn’t happen the majority of times, state restoration inevitably takes a back seat.
Android does not only destroy Activities when it needs to reclaim memory however. Certain system events can require an Activity to be recreated as some configuration has changed, like the system language, font size or screen orientation. These configuration changes often require a new set of resources, which is only applied to new Activities.
The system handles state restoration for configuration changes the same way as it does with process death: the new Activity receives the state that was saved before the configuration changed happened. Obviously, screen rotation does happen fairly often, and it would be detrimental to the user experience if all state was lost here.
Likewise, when navigating from Activity A to Activity B in an application, Activity A could be destroyed rather than being kept in memory. When navigating back from B, A’s state should be properly restored.
Activities and lifecycles
The Activity class has an extensive set of lifecycle callbacks to let it know what is happening in the system. Apps and their Activities may have interest in knowing when their focus is lost to stop updating the UI, and when it is put in the background to stop doing work at all.
Now instead of mapping callbacks to functionality, I’d like look at them the other way around from an app’s perspective.
During the lifetime of the Activity you generally want to show something to the user. The Activity provides a window to show a user interface, and you want to inflate this user interface as soon as the Activity is initialized. Since the Activity class is instantiated by the system and needs a bit of initialization, it provides the onCreate
method as a first point where you can set up your UI.
Since we’re dealing with process deaths, this is also the place where you receive a possible previous saved instance state.
When the Activity has finished onCreate
, it isn’t visible yet, and you might want to do work only when the Activity is visible. Android calls this the ‘started’ state, and notifies the Activity by calling onStart
.
In Android, apps can also be visible but not focused; for example when a dialog pops up or, in recent versions of Android, you’re dealing with multiple windows. In general, the onResume
method is the one being called when the Activity receives focus and interacts with the user¹.
When starting things, you generally need to stop things as well, and each of these ‘entry’ callbacks have their counterparts. onCreate
has onDestroy
, onStart
has onStop
, and onResume
has onPause
.
It is clear that in the sense of one screen per Activity, the lifecycle is primarily used to let the Activity know when to initialize and start work. And it’s actually not that difficult at all. This simple set of callbacks plus onSaveInstanceState
should really be all you need.
Application ‘architectures’
Back in the early years of Android app development, it was not uncommon to just put all logic for a single screen in its Activity class. The Activity was regarded as a ‘Controller’, and the framework ‘already forced’ developers to separate the view by using XML views. Applications for mobile devices were small, and managing code like this was somewhat doable.
Somewhere along the line however, devices got faster and applications became larger. The demand for more functionality grew, and with that the Activities themselves grew. (I don’t think I need to link to a certain 13K-line chat activity, but I’ll do it anyway ;-) ). Activities needed to handle more states, inflate and setup more views, and handle more and more features. I at least can imagine why process death became an after thought.
And it’s really no wonder at all this happened. As mentioned before, the Activity has a lot of callbacks. Just counting the number of void on
and boolean on
occurrences in Activity.java
in the Android 1.1 sources adds up to ~40 methods. These vary from lifecycle callbacks to motion events, back presses, options menu handling, and many more. For AppCompatActivity.java
in appcompat-v7:27.1.1
the tally is a staggering 80.
Furthermore, official Android documentation and examples used to demonstrate their code directly tied to the Activity, leading to developers just copying and pasting it into their projects.
All this makes for Activity implementations to be easily susceptible to become God classes. View related features like options menus and ActionBars were tied to it, but also navigational features, permission callbacks, result callbacks, etcetera.
MV*, Fragments and navigational libraries
In 2010, there were already a few queries into using MVC or MVP to structure applications, but it wasn’t really widely adopted. However, testing was hard and slow due to the tight coupling to the system and immensely slow emulator. Decoupling application and presenter logic from views meant that test could be run on the JVM without having to spin up an emulator, and this decoupling was primarily done by using some form of MV*. And it wasn’t long after that that libraries began to emerge to facilitate these patterns.
Meanwhile Fragments were introduced, “primarily to support more dynamic and flexible UI designs on large screens, such as tablets”. Fragments were supposed to become view controllers to decouple chunks of business logic in a single screen. A tablet for example could show multiple Fragments, whereas a phone would just show a single one.
With Fragments came the FragmentManager which allowed Fragments to be put on a back stack in a single Activity, much like the Activity stack itself. Navigating through screens didn’t have to mean you would start another Activity. Instead, you just push a new Fragment to the FragmentManager’s back stack and go from there.
There is just one slight downside: the Lolcycle. The combined lifecycle of the Activity and a Fragment is almost incomprehensible. Where previously an asynchronous request would ‘just’ temporarily leak an Activity after it was destroyed, now the Fragment would crash if the request was finished and you would try to access any resources in it. In fact, I owe one fifth of my StackOverflow internet points to this very problem.
On top of that, the FragmentManager implementation is incredibly difficult to follow. Fixing bugs in your application just became an all-day job.
Single Activity approaches
Fragments did however somewhat encourage developers to build single Activity applications. It became easier to do transition animations between screens, since there is no Activity window in between. Not having to deal with Activity flags would also make life a bit easier. New libraries emerged to manage a single Activity back stack without using Fragments. Resources could be passed to new screens directly without having to serialize and re-fetch them first.
Using a single Activity approach to host its own back stack of screens makes it a bit more difficult though to deal with life cycles and process deaths. Instead of just having to handle a single screen per Activity, one must now persist the back stack as well, plus all instance states for each of the screens in the stack. Again, since process death doesn’t happen that often, it inevitably takes a back seat. More often than not, these libraries didn’t support state restoration as a first class citizen.
Single Activity approaches are however gaining popularity and at I/O 2018, Google announced the Navigation Architecture Component which officially supports navigating through screens in a single Activity.
Back stacks
The one thing that is persistent throughout the years is the notion of a back stack. You push screens on it, and you can pop them back off it. Sound simple enough. And when you do just that, it really is.
It becomes difficult though when you need something else. You might want to pop 2 screens, or go back to screen A when you’re at screen F, or even determine the next screen based on the current state of the back stack. All of a sudden, screen F needs to know about the back stack, its state, and it must be able to manipulate it.
Closing remarks
Over the years, a lot of techniques have grown when it comes to developing Android applications. Some people use the original one Activity per screen approach while others use one Activity for their entire application, and there is clearly a division between ‘Fragments all the way’ at one side and ‘no Fragments at all’ at the other. Some people use MVP, MVC or whatever MV* pattern you may have, some even use it as the apps’ architecture.
It is pretty clear though that the Activity class is capable of doing way too much, and since Fragments are designed to be able to do everything an Activity can do, they aren’t any better.
The lifecycle of the Activity works pretty well though. Its various states provide a reasonably fair way of knowing when your application is active, visible or paused. The fact that it is a God class probably resulted in the drawback that it has to be destroyed on configuration changes, which makes it difficult to tie asynchronous calls to it.
Dealing with navigation certainly isn’t optimal, and there is definitely room for improvement.
In whatever way you build your applications, understanding how they work with the system is important. You need to react to system events, handle process deaths properly and generally your app needs to be a good citizen in its environment.
Fun facts
- The task state is meant to be cleared after ~30 minutes. This means that any Activities on the stack above the root Activity are cleared after a while, and the user starts at the root Activity again. On emulators running Android 2.3.3 this is indeed the case. Somewhere along newer versions of Android this ‘feature’ seems to have been lost, and you will indeed return to the sixth-level nested Activity (or Fragment) after not having opened the app in two days.
“The idea is that, after a time, users will likely have abandoned what they were doing before and are returning to the task to begin something new.” - In its first commit, Telegram’s
ChatActivity
was ‘only’ 3244 lines and had about 60 fields. - Here are some StackOverflow questions from 2010 which touch upon MVP and/or MVC.
- Actually,
onWindowFocusChanged
is recommended to know for certain that the Activity is visible to the user. The parameter supplied turnsfalse
for example when an AlertDialog is shown, or when the user opens the notification drawer.