Based on the information outlined in the chapter entitled Android App and Activity Lifecycles it is now evident that the activities and fragments that make up an application pass through various different states during the application’s lifespan. The Android runtime system imposes the change from one state to the other and is, therefore, largely beyond the control of the activity itself. That does not, however, mean that the app cannot react to those changes and take appropriate actions.
The primary objective of this chapter is to provide a high-level overview of how an activity may be notified of a state change and outline the areas where it is advisable to save or restore state information. Having covered this information, the chapter will touch briefly on activity lifetimes.
New vs. Old Lifecycle Techniques
Until recently, there was a standard way to build lifecycle awareness into an app. This approach is covered in this chapter and involves implementing a set of methods (one for each lifecycle state) within an activity or fragment instance that the operating system calls when the lifecycle status of that object changes. This approach has remained unchanged since the early years of the Android operating system, and while still a viable option today, it does have some limitations, which will be explained later in this chapter.
With the introduction of the lifecycle classes with the Jetpack Android Architecture Components, a better approach to lifecycle handling is now available. This modern approach to lifecycle management (together with the Jetpack components and architecture guidelines) will be covered in detail in later chapters. It is still essential, however, to understand the traditional lifecycle methods for a couple of reasons. First, as an Android developer, you will not be completely insulated from the traditional lifecycle methods and will still use some of them. More importantly, understanding the older way of handling lifecycles will provide a sound foundation for learning the new approach later in the book.
The Activity and Fragment Classes
With few exceptions, an application’s activities and fragments are created as subclasses of the Android AppCompatActivity class and Fragment classes, respectively.
Consider, for example, the AndroidSample project created in An Android Studio Tutorial and subsequently converted to use view binding. Load this project into the Android Studio environment and locate the MainActivity.kt file (located in app -> kotlin+java -> <your domain> -> androidsample). Having located the file, double-click on it to load it into the editor, where it should read as follows:
package com.example.androidsample
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import com.example.androidsample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun convertCurrency(view: View) {
.
.
}
}
Code language: Kotlin (kotlin)
When the project was created, we instructed Android Studio also to create an initial activity named MainActivity. kt As is evident from the above code, the MainActivity class is a subclass of the AppCompatActivity class.
A review of the reference documentation for the AppCompatActivity class would reveal that it is itself a subclass of the Activity class. This can be verified within the Android Studio editor using the Hierarchy tool window. With the MainActivity.kt file loaded into the editor, click on AppCompatActivity in the class declaration line and press the Ctrl-H keyboard shortcut. The hierarchy tool window will subsequently appear, displaying the class hierarchy for the selected class. As illustrated in Figure 20-1, AppCompatActivity is subclassed from the FragmentActivity class, which is itself ultimately a subclass of the Activity class:
The Activity and Fragment classes contain a range of methods intended to be called by the Android runtime to notify the object when its state is changing. For this chapter, we will refer to these as the lifecycle methods. An activity or fragment class needs to override these methods and implement the necessary functionality to react accordingly to state changes.
One such method is named onCreate(), and, turning once again to the above code fragment, we can see that this method has already been overridden and implemented for us in the MainActivity class. In a later section, we will explore onCreate() and the other relevant lifecycle methods of the Activity and Fragment classes.
Dynamic State vs. Persistent State
A key objective of lifecycle management is ensuring that the state of the activity is saved and restored at appropriate times. When talking about state in this context, we mean the data currently being held within the activity and the appearance of the user interface. The activity might, for example, maintain a data model in memory that needs to be saved to a database, content provider, or file. Because it persists from one invocation of the application to another, such state information is referred to as the persistent state.
The appearance of the user interface (such as text entered into a text field but not yet committed to the application’s internal data model) is referred to as the dynamic state since it is typically only retained during a single invocation of the application (and also referred to as user interface state or instance state).
Understanding the differences between these two states is important because the ways they are saved and the reasons for doing so differ.
The purpose of saving the persistent state is to avoid data loss that may result from an activity being killed by the runtime system while in the background. On the other hand, the dynamic state is saved and restored for slightly more complex reasons.
Consider, for example, that an application contains an activity (which we will refer to as Activity A) containing a text field and some radio buttons. During the course of using the application, the user enters some text into the text field and makes a selection from the radio buttons. However, before performing an action to save these changes, the user switches to another activity, causing Activity A to be pushed down the Activity Stack and placed into the background. After some time, the runtime system ascertains that memory is low and kills Activity A to free up resources. However, as far as the user is concerned, Activity A was placed in the background and is ready to be moved to the foreground at any time. On returning Activity A to the foreground, the user would reasonably expect the entered text and radio button selections to have been retained. In this scenario, however, a new instance of Activity A will have been created, and if the dynamic state is not saved and restored, the previous user input is lost.
Therefore, the primary purpose of saving dynamic state is to give the perception of seamless switching between foreground and background activities, regardless of the fact that activities may have been killed and restarted without the user’s knowledge.
The mechanisms for saving persistent and dynamic states will become more apparent in the following sections of this chapter.
The Android Lifecycle Methods
As previously explained, the Activity and Fragment classes contain several lifecycle methods which act as event handlers when the state of an instance changes. The primary methods supported by the Android Activity and Fragment class are as follows:
- onCreate(savedInstanceState: Bundle?) – The method called when the activity is first created and the ideal location for most initialization tasks to be performed. The method is passed an argument in the form of a Bundle object that may contain dynamic state information (typically relating to the state of the user interface) from a prior invocation of the activity.
- onRestart() – Called when the activity is about to restart after having previously been stopped by the runtime system.
- onStart() – Always called immediately after the call to the onCreate() or onRestart() methods. This method indicates to the activity that it is about to become visible to the user. This call will be followed by a call to onResume() if the activity moves to the top of the activity stack, or onStop() if it is pushed down the stack by another activity.
- onResume() – Indicates that the activity is now at the top of the activity stack and is the activity with which the user is currently interacting.
- onPause() – Indicates that a previous activity is about to become the foreground activity. This call will be followed by a call to either the onResume() or onStop() method, depending on whether the activity moves back to the foreground or becomes invisible to the user. Steps may be taken within this method to store persistent state information not yet saved by the app. To avoid delays in switching between activities, time-consuming operations such as storing data to a database or performing network operations should be avoided within this method. This method should also ensure that any CPU-intensive tasks, such as animation, are stopped.
- onStop() – The activity is no longer visible to the user. The two possible scenarios following this call are a call to onRestart() if the activity moves to the foreground again or onDestroy() if the activity is terminated.
- onDestroy() – The activity is about to be destroyed, either voluntarily because the activity has completed its tasks and has called the finish() method or because the runtime is terminating it either to release memory or due to a configuration change (such as the orientation of the device changing). It is important to note that a call will not always be made to onDestroy() when an activity is terminated.
- onConfigurationChanged() – Called when a configuration change occurs for which the activity has indicated it is not to be restarted. The method is passed a Configuration object outlining the new device configuration, and it is then the responsibility of the activity to react to the change.
The following lifecycle methods only apply to the Fragment class:
- onAttach() – Called when the fragment is assigned to an activity.
- onCreateView() – Called to create and return the fragment’s user interface layout view hierarchy.
- onViewCreated() – Called after onCreateView() returns.
- onViewStatusRestored() – The fragment’s saved view hierarchy has been restored.
In addition to the lifecycle methods outlined above, there are two methods intended specifically for saving and restoring the dynamic state of an activity:
- onRestoreInstanceState(savedInstanceState: Bundle?) – This method is called immediately after a call to the onStart() method if the activity restarts from a previous invocation in which the state was saved. As with onCreate(), this method is passed a Bundle object containing the previous state data. This method is typically used when it makes more sense to restore a previous state after the initialization of the activity has been performed in onCreate() and onStart().
- onSaveInstanceState(outState: Bundle?) – Called before an activity is destroyed so that the current dynamic state (usually relating to the user interface) can be saved. The method is passed the Bundle object into which the state should be saved and which is subsequently passed through to the onCreate() and onRestoreInstanceState() methods when the activity is restarted. Note that this method is only called when the runtime ascertains that dynamic state needs to be saved.
When overriding the above methods, it is important to remember that, except for onRestoreInstanceState() and onSaveInstanceState(), the method implementation must include a call to the corresponding method in the superclass. For example, the following method overrides the onRestart() method but also includes a call to the superclass instance of the method:
override fun onRestart() {
super.onRestart()
Log.i(TAG, "onRestart")
}
Code language: Kotlin (kotlin)
Failure to make this super class call in method overrides will result in the runtime throwing an exception during execution. While calls to the superclass in the onRestoreInstanceState() and onSaveInstanceState() methods are optional (they can, for example, be omitted when implementing custom save and restoration behavior), there are considerable benefits to using them, a subject that will be covered in the chapter entitled Saving and Restoring the State of an Android Activity.
Lifetimes
The final topic to be covered involves an outline of the entire, visible, and foreground lifetimes through which an activity or fragment will transition during execution:
- Entire Lifetime –The term “entire lifetime” is used to describe everything that takes place between the initial call to the onCreate() method and the call to onDestroy() before the object terminates.
- Visible Lifetime – Covers the periods of execution between the call to onStart() and onStop(). During this period, the activity or fragment is visible to the user though it may not be the object with which the user is currently interacting.
- Foreground Lifetime – Refers to the periods of execution between calls to the onResume() and onPause() methods.
It is important to note that an activity or fragment may pass through the foreground and visible lifetimes multiple times during the course of the entire lifetime.
The concepts of lifetimes and lifecycle methods are illustrated in Figure 20-2:
Foldable Devices and Multi-Resume
As discussed previously, an activity is considered to be in the resumed state when it has moved to the foreground and is the activity with which the user is currently interacting. On standard devices, an app can have one activity in the resumed state at any one time, and all other activities are likely to be in the paused or stopped state.
For some time now, Android has included multi-window support, allowing multiple activities to appear simultaneously in either split-screen or freeform configurations. Although initially used primarily on large-screen tablet devices, this feature is likely to become more popular with the introduction of foldable devices.
On devices running Android 10 and on which multi-window support is enabled (as will be the case for most foldable devices), it will be possible for multiple app activities to be in the resumed state at the same time (a concept referred to as multi-resume) allowing those visible activities to continue functioning (for example streaming content or updating visual data) even when another activity currently has focus. Although multiple activities can be in the resumed state, only one of these activities will be considered the topmost resumed activity (in other words, the activity with which the user most recently interacted).
An activity can be notified that it has gained or lost the topmost resumed status by implementing the onTopResumedActivityChanged() callback method.
Disabling Configuration Change Restarts
As previously outlined, an activity may indicate that it is not to be restarted in the event of certain configuration changes. This is achieved by adding an android:configChanges directive to the activity element within the project manifest file. The following manifest file excerpt, for example, indicates that the activity should not be restarted in the event of configuration changes relating to orientation or device-wide font size:
<activity android:name=".MainActivity"
android:configChanges="orientation|fontScale"
android:label="@string/app_name">
Code language: HTML, XML (xml)
Lifecycle Method Limitations
As discussed at the start of this chapter, lifecycle methods have been in use for many years and, until recently, were the only mechanism available for handling lifecycle state changes for activities and fragments. There are, however, areas for improvement in this approach.
One issue with the lifecycle methods is that they do not provide an easy way for an activity or fragment to discover its current lifecycle state at any given point during app execution. Instead, the object must track the state internally or wait for the next lifecycle method call.
Also, the methods do not provide a simple way for one object to observe the lifecycle state changes of other objects within an app. This is a serious consideration since a lifecycle state change in a given activity or fragment can impact many other objects within an app.
The lifecycle methods are also only available on subclasses of the Fragment and Activity classes. Therefore, it is impossible to build custom classes that are genuinely lifecycle aware.
Finally, the lifecycle methods result in most lifecycle handling code being written within the activity or fragment, which can lead to complex and error-prone code. Ideally, much of this code should reside in the other classes impacted by the state change. For example, an app that streams video might include a class designed specifically to manage the incoming stream. If the app needs to pause the stream when the main activity is stopped, the code to do so should reside in the streaming class, not the main activity.
All these problems and more are resolved using lifecycle-aware components, a topic that will be covered starting with the chapter entitled Modern Android App Architecture with Jetpack.
Summary
All activities are derived from the Android Activity class, which, in turn, contains several lifecycle methods that are designed to be called by the runtime system when the state of an activity changes. Similarly, the Fragment class contains several comparable methods. By overriding these methods, activities and fragments can respond to state changes and, where necessary, take steps to save and restore the current state of the activity and the application. Lifecycle state can be thought of as taking two forms. The persistent state refers to data that needs to be stored between application invocations (for example, to a file or database). Dynamic state, on the other hand, relates instead to the current appearance of the user interface.
Although lifecycle methods have some limitations that can be avoided using lifecycle-aware components, understanding these methods is essential to fully understand the new approaches to lifecycle management covered later in this book.
In this chapter, we have highlighted the lifecycle methods available to activities and covered the concept of activity lifetimes. In the next chapter, entitled “Android Activity State Changes by Example”, we will implement an example application that puts much of this theory into practice.