From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Up until now, it has been common practice to kick off and cancel UI updates during the foreground lifecycle of an activity. However, with the advent of multi-window in Android Nougat, the visible lifecycle is no longer as roughly equivalent to the foreground lifecycle as it was before. In turn you should carefully consider what your UI should be doing when your activity is visible vs. in the foreground, rather than treating them as one and the same.
This post will highlight considerations to make in updating your UI on Nougat (and beyond) devices, while still providing a smooth experience for pre-Nougat users.
If you are not already familiar with the activity states and lifecycle callback methods, or even if you are, check out my in-depth talk on the activity lifecycle from 360AnDev 2016. But not to worry, we’ll do a quick overview here.
An activity’s state depends on three things:
There are four states an activity can be in:
State | In memory? | Visible to user? | In foreground? |
---|---|---|---|
Non-Existent | No | No | No |
Stopped | Yes | No | No |
Paused | Yes | Yes/Partially | No |
Running (aka Resumed, Active) | Yes | Yes | Yes |
Nonexistent represents an activity that has not been launched yet, or that was just destroyed (e.g. by the user pressing back). There is no instance in memory, and in turn there is no view associated with it for the user to see nor to interact with.
Stopped represents an activity that has an instance in memory, but the whose view is not visible on the screen. This state occurs in passing when the activity is first spinning up, and any time the view is fully occluded (e.g. when the user launches another full screen activity to the foreground, presses the home button, or uses the overview screen to switch tasks).
Paused represents an activity whose view is visible, or partially visible, but is not active in the foreground. An activity would be partially-visible, for example, if the user launches a new dialog-themed or transparent activity on top of it. An activity could also be fully-visible but not in the foreground if the user is viewing two separate activities in multi-window mode.
Running represents an activity that is in memory, fully visible, and is in the foreground. It is the activity the user is interacting with currently. Only one activity across the entire system can be running at any given time. That means if one activity is moving to the running state, another is likely moving out of the running state.
The operating system calls methods, known as lifecycle callbacks, on the Activity
object to notify us developers the state of the activity is changing. onStop()
is one such lifecycle callback.
The operating system calls onStop()
on an activity as the activity’s view moves out of the user’s sight. Once onStop()
completes, the activity is in the stopped state.
onStop()
represents the end of the visible lifecycle of an activity (the time in which the user can see the activity’s view). Its counterpart, onStart()
, represents the beginning of the visible lifecycle of an activity. You can assume the activity’s view is partially- or wholly-visible in any callbacks called between onStart()
and onStop()
.
Knowing when your activity’s view will no longer be visible to the user is important. As Android developers we want to provide the best user experience while minimizing resource usage. Consider, for example, an app that displays location data. Polling for location drains power. The more accurate and more frequent, the more power. If the user is not able to see your application, do you still need to be polling for such information? It depends on your requirements, but it behooves you as a developer to consider what work you might be able to minimize or stop altogether when your activity in the stopped state.
Prior to Nougat, an activity spent very little time in the scenario where it was both paused and fully-visible. The main scenario where paused and fully-visible occurred was on activity launch. And in that case, the scenario lasted very briefly as the activity was cycled immediately to running (aka resumed) state. For the most part, pre-Nougat activities find themselves in the paused state when their view is partially occluded (for example, when a new activity that is transparent or smaller than the entire screen is launched on top).
Because of this, it has been common practice to use onResume()
and onPause
to set up or tear down any updates related to the user interface. The assumption was you do not need to update your UI when the activity is paused, only when it is in the foreground. In other words, the foreground lifecycle (see diagram above) was treated as roughly equivalent to the visible lifecycle.
However, with the advent of multi-windowing in Nougat, activities will find themselves in the paused and fully-visible state for longer periods of time. This means your user will be looking at your paused activity alongside a window containing a different running activity. Remember, only one activity can be running at a time, so when the user interacts with the activity in the window on the left, it comes to the foreground and, in turn the activity in the window on the right cannot be running and thus is paused.
Since your activity’s view is fully visible, users will still expect your app to be running (e.g. updating data, playing video, etc.) even though it is not in the foreground. In a Nougat world, then, your activity should update the UI during the entire visible lifecycle of your activity, between onStart()
and onStop()
.
Consider video, for example. You have a legacy app that does simple video playback. You start or resume video playback in onResume()
and pause playback in onPause
. This worked well up until now.
However, in multi-window mode, your users are complaining because they want to watch their video while they send a text message in a separate window, but your app stops video whenever they interact with another app in that second window. You can fix the problem by moving your playback resuming and pausing to onStart()
and onStop()
. Same would go for any live updating date, like a photo gallery app that refreshes to show new images as they are pushed to a Flickr stream.
A word of caution, though, before you blindly move all of your UI setup and teardown code. Prior to Nougat, onStop()
was a lazy operation. It was not guaranteed that onStop()
would be called as soon as the activity was no longer visible. Instead, it could occur a short time after.
On Nougat and beyond, though, you are guaranteed onStop()
will be called called as soon as the activity is no longer visible.
With respect to our video example, if you move your code to pause or stop playback to onStop()
, users on pre-Nougat may hear the audio continuing to play momentarily after they press the back button, even after the video is no longer visible.
So what are you to do? To ensure a positive user experience on both pre-Nougat and Nougat devices, add conditionals to address scenarios like this one. Place the clean up code (in this case, stopping video playback) in both onPause()
and onStop()
, guarded with conditional checks for API level, as shown in this snippet from the ExoPlayer demo code:
...
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
onHidden();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
onHidden();
}
}
...
Not all scenarios are as extreme as video when it comes to pre-Nougat lazy stop manifesting itself in a noticeable way. In many cases it will be just fine to always stop your UI updates in onStop()
, rather than conditionally running the updates in both onPause
and onStop()
.
Making decisions about what to do in each of the activity lifecycle methods requires consideration of:
The states and their meaning have not changed, but the addition of multi-windowing renders a previously rare state (paused and fully-visible for an extended period of time) more common. Luckily, having a strong understanding of the activity lifecycle will help you reason about how to update your code (if at all) when new additions like multi-windowing are added as the platform evolves!
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...