The Activity Lifecycle
In the previous chapter, we used the onCreate(saveInstanceState: Bundle?)
method to display a layout in the UI of our screen. Now, we'll explore in more detail how the Android system interacts with your application to make this happen. As soon as an Activity is launched, it goes through a series of steps to take it through initialization and preparing it to be displayed to being partially displayed, and then fully displayed. There are also steps that correspond with your application being hidden, backgrounded, and then destroyed. This process is called the Activity lifecycle. For every one of these steps, there is a callback that your Activity can use to perform actions such as creating and changing the display and saving data when your app has been put into the background and then restoring that data after your app comes back into the foreground. You can consider these callbacks as hooks into how the system interacts with your activity/screen.
Every Activity has a parent Activity class that it extends. These callbacks are made on your Activity's parent, and it's up to you to decide whether you need to implement them in your own Activity to take any corresponding action. Every one of these callback functions has the override
keyword. The override
keyword in Kotlin means that either this function is providing an implementation of an interface or an abstract method, or, in the case of your Activity here, which is a subclass, it is providing the implementation that will override its parent.
Now that you know how the Activity lifecycle works in general, let's go into more detail about the principal callbacks you will work with in order, from creating an Activity to the Activity being destroyed:
override fun onCreate(savedInstanceState: Bundle?)
: This is the callback that you will use the most for activities that draw a full-sized screen. It's here where you prepare your Activity layout to be displayed. At this stage, after the method completes, it is still not displayed to the user, although it will appear that way if you don't implement any other callbacks. You usually set up the UI of your Activity here by calling thesetContentView
methodsetContentView(R.layout.activity_main
) and carry out any initialization that is required. This method is only called once in its lifecycle unless the Activity is created again. This happens by default for some actions (such as rotating the phone from portrait to landscape orientation, for example). ThesavedInstanceState
parameter of theBundle?
type (?
means the type can be null) in its simplest form is a map of key-value pairs optimized to save and restore data. It will be null if this is the first time that the Activity has been run after the app has started or if the Activity is being created for the first time or recreated without any states being saved. It may contain a saved state if it has been saved in theonSaveInstanceState(outState: Bundle?)
callback prior to the Activity being recreated.override fun onRestart()
: When the Activity restarts, this is called immediately beforeonStart()
. It is important to be clear about the difference between restarting an Activity and recreating an activity. When the Activity is backgrounded by pressing the home button—for instance, when it comes back into the foreground again—onRestart()
will be called. Recreating an Activity is what happens when a configuration change happens, such as the device being rotated. The Activity is finished and then created again.override fun onStart()
: This is the callback made when the Activity first comes into view. Also, after the app is backgrounded by pressing either the back, home, or therecents/overview
hardware buttons, on selecting the app again from therecents/overview
menu or the launcher, this function will be run. It is the first of the visible lifecycle methods.override fun onRestoreInstanceState(savedInstanceState: Bundle?)
: If the state has been saved usingonSaveInstanceState(outState: Bundle?)
this is the method which the system calls afteronStart()
where you can retrieve theBundle
state instead of restoring the state duringonCreate(savedInstanceState: Bundle?)
override fun onResume()
: This callback is run as the final stage of creating an Activity for the first time, and also when the app has been backgrounded and then is brought into the foreground. Upon the completion of this callback, the screen/activity is ready to be used, receive user events, and be responsive.override fun onSaveInstanceState(outState: Bundle?)
: If you want to save the state of the activity, this function can do so. You add key-value pairs using one of the convenience functions depending on the data type. The data will then be available if your Activity is recreated inonCreate(saveInstanceState: Bundle?)
andonRestoreInstanceState(savedInstanceState: Bundle?)
.override fun onPause()
: This function is called when the Activity starts to be backgrounded or another dialog or Activity comes into the foreground.override fun onStop()
: This function is called when the Activity is hidden, either because it is being backgrounded or another Activity is being launched on top of it.override fun onDestroy()
: This is called by the system to kill the Activity when system resources are low, whenfinish()
is called explicitly on the Activity, or, more commonly, when the Activity is killed by the user closing the app from the recents/overview button.
Now that you understand what these common lifecycle callbacks do, let's implement them to see when they are called.
Exercise 2.01: Logging the Activity Callbacks
Let's create an application called Activity Callbacks with an empty Activity, as you did previously in Chapter 1, Creating Your First App. The aim of this exercise is to log the Activity callbacks and the order that they occur for common operations:
- After the application has been created,
MainActivity
will appear as follows:package com.example.activitycallbacks import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
In order to verify the order of the callbacks, let's add a log statement at the end of each callback. To prepare the Activity for logging, import the Android log package by adding
import android.util.Log
to theimport
statements. Then, add a constant to the class to identify your Activity. Constants in Kotlin are identified by theconst
keyword and can be declared at the top level (outside the class) or in an object within the class. Top level constants are generally used if they are required to be public. For private constants, Kotlin provides a convenient way to add static functionality to classes by declaring a companion object. Add the following at the bottom of the class belowonCreate(savedInstanceState: Bundle?)
:companion object { private const val TAG = "MainActivity" }
Then add a log statement at the end of
onCreate(savedInstanceState: Bundle?)
:Log.d(TAG, "onCreate")
Our Activity should now have the following code:
package com.example.activitycallbacks import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d(TAG, "onCreate") } companion object { private const val TAG = "MainActivity" } }
d
in the preceding log statement refers to debug. There are six different log levels that can be used to output message information from the least to most important -v
for verbose,d
for debug,i
for info,w
for warn,e
for error, andwtf
for what a terrible failure. (This last log level highlights an exception that should never occur.)Log.v(TAG, "verbose message") Log.d(TAG, "debug message") Log.i(TAG, "info message") Log.w(TAG, "warning message") Log.e(TAG, "error message") Log.wtf(TAG, "what a terrible failure message")
- Now, let's see how the logs are displayed in Android Studio. Open the
Logcat
window. It can be accessed by clicking on theLogcat
tab at the bottom of the screen and also from the toolbar by going toView
|Tool Windows
|Logcat
. - Run the app on the virtual device and examine the
Logcat
window output. You should see the log statement you have added formatted like the following line in Figure 2.1: - Log statements can be quite difficult to interpret at first glance, so let's break down the following statement into its separate parts:
2020-03-03 20:36:12.308 21415-21415/com.example.activitycallbacks D/MainActivity: onCreate
Let's examine the elements of the log statement in detail:
You can examine the output of the different log levels by changing the log filter from
Debug
to other options in the drop-down menu. If you selectVerbose
, as the name implies, you will see a lot of output. - What's great about the
TAG
option of the log statement is that it enables you to filter the log statements that are reported in theLogcat
window of Android Studio by typing in the text of the tag, as shown in Figure 2.3:So, if you are debugging an issue in your Activity, you can type in the
TAG
name and add logs to your Activity to see the sequence of log statements. This is what you are going to do next by implementing the principal Activity callbacks and adding a log statement to each one to see when they are run. - Place your cursor on a new line after the closing brace of the
onCreate(savedInstanceState: Bundle?)
function and then add theonRestart()
callback with a log statement. Make sure you call through tosuper.onRestart()
so that the existing functionality of the Activity callback works as expected:override fun onRestart() { super.onRestart() Log.d(TAG, "onRestart") }
- You will find that once you start typing the name of the function, Android Studio's autocomplete feature will suggest options for the name of the function you want to override.
Note
In Android Studio you can start typing the name of a function, and autocomplete options will pop up with suggestions for functions to override. Alternatively, if you go to the top menu and then
Code
|Generate
|Override methods
, you can select the methods to override.Do this for all of the following callback functions:
onCreate(savedInstanceState: Bundle?) onRestart() onStart() onRestoreInstanceState(savedInstanceState: Bundle?) onResume() onPause() onStop() onSaveInstanceStateoutState: Bundle?) onDestroy()
- Your Activity should now have the following code (truncated here). You can see the full code on GitHub at http://packt.live/38W7jU5
The completed activity will now override the callbacks with your implementation, which adds a log message:
package com.example.activitycallbacks import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d(TAG, "onCreate") } override fun onRestart() { super.onRestart() Log.d(TAG, "onRestart") } //Remaining callbacks follow: see github link above companion object { private const val TAG = "MainActivity" } }
- Run the app, and then once it has loaded, as in Figure 2.4, look at the
Logcat
output; you should see the following log statements (this is a shortened version):D/MainActivity: onCreate D/MainActivity: onStart D/MainActivity: onResume
The Activity has been created, started, and then prepared for the user to interact with:
- Press the round home button in the center of the bottom navigation controls and background the app. You should now see the following Logcat output:
D/MainActivity: onPause D/MainActivity: onStop D/MainActivity: onSaveInstanceState
For apps which target below Android Pie (API 28) then
onSaveInstanceState(outState: Bundle?)
may also be called beforeonPause()
oronStop()
. - Now, bring the app back into the foreground by pressing the recents/overview button (usually a square or three vertical lines) on the right and selecting the app, or by going to the launcher and opening the app. You should now see the following:
D/MainActivity: onRestart D/MainActivity: onStart D/MainActivity: onResume
The Activity has been restarted. You might have noticed that the
onRestoreInstanceState(savedInstanceState: Bundle)
function was not called. This is because the Activity was not destroyed and recreated. - Press the triangle back button on the left of the bottom navigation controls (it may also be on the right) and you will see the Activity being destroyed. You can also do this by pressing the recents/overview button and then swiping the app upward to kill the activity. This is the output:
D/MainActivity: onPause D/MainActivity: onStop D/MainActivity: onDestroy
- Launch your app again and then rotate the phone. You might find that the phone does not rotate and the display is sideways. If this happens drag down the status bar at the very top of the virtual device and select the auto-rotate button 2nd from the right in the settings.
You should see the following callbacks:
D/MainActivity: onCreate D/MainActivity: onStart D/MainActivity: onResume D/MainActivity: onPause D/MainActivity: onStop D/MainActivity: onSaveInstanceState D/MainActivity: onDestroy D/MainActivity: onCreate D/MainActivity: onStart D/MainActivity: onRestoreInstanceState D/MainActivity: onResume
Please note that as stated in step 11, the order of the
onSaveInstanceState(outState: Bundle?)
callback may vary. - Configuration changes, such as rotating the phone, by default recreate the activity. You can choose not to handle certain configuration changes in the app, which will then not recreate the activity. To do this for rotation, add
android:configChanges="orientation|screenSize|screenLayout"
toMainActivity
in theAndroidManifest.xml
file. Launch the app and then rotate the phone, and these are the only callbacks that you have added toMainActivity
that you will see:D/MainActivity: onCreate D/MainActivity: onStart D/MainActivity: onResume
The
orientation
andscreenSize
values have the same function for different Android API levels for detecting screen orientation changes. ThescreenLayout
value detects other layout changes which might occur on foldable phones. These are some of the config changes you can choose to handle yourself (another common one iskeyboardHidden
to react to changes in accessing the keyboard). The app will still be notified by the system of these changes through the following callback:override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) Log.d(TAG, "onConfigurationChanged") }
If you add this callback function to
MainActivity
, and you have addedandroid:configChanges="orientation|screenSize|screenLayout"
toMainActivity
in the manifest, you will see it called on rotation.
In this exercise, you have learned about the principal Activity callbacks and how they run when a user carries out common operations with your app through the system's interaction with MainActivity
. In the next section, you will cover saving the state and restoring it, as well as see more examples of how the Activity lifecycle works.