2.2: Activity Lifecycle and Instance State

Contents:

In this practical you'll learn more about the activity lifecycle. The activity lifecycle is the set of states an activity can be in during its entire lifetime, from the time it is initially created to when it is destroyed and the system reclaims that activity's resources. As a user navigates between activities in your app (as well as into and out of your app), those activities each transition between different states in the activity lifecycle.

Diagram of the App Lifecycle

Each stage in the lifecycle of an activity has a corresponding callback method (onCreate(), onStart(), onPause(), and so on). When an activity changes state, the associated callback method is invoked. You've already seen one of these methods: onCreate(). By overriding any of the lifecycle callback methods in your activity classes, you can change the default behavior of how your activity behaves in response to different user or system actions.

Changes to the activity state can also occur in response to device configuration changes such as rotating the device from portrait to landscape. These configuration changes result in the activity being destroyed and entirely recreated in its default state, which may cause the loss of information the user has entered in that activity. It's important to develop your app to prevent this to avoid user confusion. Later in this practical we'll experiment with configuration changes and learn how to preserve the state of your activities in response to device configuration changes or other Activity lifecycle events.

In this practical you'll add logging statements to the TwoActivities app and observe the lifecycle changes as you use the app in various ways. You will then begin working with these changes and exploring how to handle user input under these conditions..

What you should already KNOW

From the previous practicals, you should be able to:

  • Create and running an app project in Android Studio.
  • Add log statements to your app and viewing those logs in the Android Monitor (logcat).
  • Understand and work with activities and intents, and be comfortable interacting with them.

What you will LEARN

You will learn to:

  • Understand the activity lifecycle, and when activities are created, pause, stop, and are destroyed.
  • Understand the lifecycle callback methods associated with activity changes.
  • Understand the effect of actions such as configuration changes that can result in activity lifecycle events.
  • Retain activity state across lifecycle events.

What you will DO

In this practical, you will:

  • Extend the TwoActivities app from the previous practical to implement the various activity lifecycle callbacks to include logging statements.
  • Observe the state changes as your app runs and as you interact with the activities in your app.
  • Modify your app to retain the instance state of an activity that is unexpectedly recreated in response to user behavior or configuration change on the device.

App Overview

For this practical you'll add onto the TwoActivities app. The app looks and behaves roughly the same as it did in the last section: with two activities and two messages you can send between them. The changes you make to the app in this practical will not affect its visible user behavior.

Task 1. Add Lifecycle Callbacks to TwoActivities

In this task you will implement all of the activity lifecycle callback methods to print messages to logcat when those methods are invoked. These log messages will allow you to see when the activity lifecycle changes state, and how those lifecycle state changes affect your app as it runs.

1.1 (Optional) Copy the TwoActivities Project

For the tasks in this practical, you will modify the existing TwoActivities project that you built in the last practical. If you'd prefer to keep the previous TwoActivities project intact, follow the steps in the Appendix to make a copy of the project.

1.2 Implement callbacks in to MainActivity

  1. Open java/com.example.android.twoactivities/MainActivity.
  2. In the onCreate() method, add the following log statements:
    Log.d(LOG_TAG, "-------");
    Log.d(LOG_TAG, "onCreate");
    
  3. Add a new method for the onStart() callback, with a statement to the log for that event:

    @Override
    public void onStart(){
        super.onStart();
        Log.d(LOG_TAG, "onStart");
    }
    

    TIP: Select Code > Override Methods in Android Studio. A dialog appears with all of the possible methods you can override in your class. Choosing one or more callback methods from the list inserts a complete template for those methods, including the required call to the superclass.

  4. Use the onStart() method as a template to implement the other lifecycle callbacks:

    • onPause()
    • onRestart()
    • onResume()
    • onStop()
    • onDestroy()

    All the callback methods have the same signatures (except for the name). If you copy and paste onStart() to create these other callback methods, don't forget to update the contents to call the right method in the superclass, and to log the correct method.

  5. Build and run your app.

Solution Code (not the entire class):

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   Log.d(LOG_TAG, "-------");
   Log.d(LOG_TAG, "onCreate");

   mMessageEditText = (EditText) findViewById(R.id.editText_main);
   mReplyHeadTextView = (TextView) findViewById(R.id.text_header_reply);
   mReplyTextView = (TextView) findViewById(R.id.text_message_reply);
}

@Override
public void onStart(){
   super.onStart();
   Log.d(LOG_TAG, "onStart");
}

@Override
public void onRestart() {
   super.onRestart();
   Log.d(LOG_TAG, "onRestart");
}

@Override
public void onResume() {
   super.onResume();
   Log.d(LOG_TAG, "onResume");
}

@Override
public void onPause() {
   super.onPause();
   Log.d(LOG_TAG, "onPause");
}

@Override
public void onStop() {
   super.onStop();
   Log.d(LOG_TAG, "onStop");
}

@Override
public void onDestroy() {
   super.onDestroy();
   Log.d(LOG_TAG, "onDestroy");
}

1.3 Implement lifecycle callbacks in SecondActivity

Now that you've implemented the lifecycle callback methods for MainActivity, do the same for SecondActivity.

  1. Open java/com.example.android.twoactivities/SecondActivity.
  2. At the top of the class, add a constant for the LOG_TAG variable:
    private static final String LOG_TAG =
         SecondActivity.class.getSimpleName();
    
  3. Add the lifecycle callbacks and log statements to the second activity. (You can also just copy and paste the callback methods from MainActivity)
  4. Add a log statement to the returnReply() method, just before the finish() method:
    Log.d(LOG_TAG, "End SecondActivity");
    

Solution Code (not the entire class):

private static final String LOG_TAG = SecondActivity.class.getSimpleName();

public void returnReply(View view) {
   String reply = mReply.getText().toString();

   Intent replyIntent = new Intent();
   replyIntent.putExtra(EXTRA_REPLY, reply);
   setResult(RESULT_OK, replyIntent);

   Log.d(LOG_TAG, "End SecondActivity");
   finish();
}

@Override
protected void onStart() {
   super.onStart();
   Log.d(LOG_TAG, "onStart");
}

@Override
public void onRestart() {
   super.onRestart();
   Log.d(LOG_TAG, "onRestart");
}

@Override
public void onResume() {
   super.onResume();
   Log.d(LOG_TAG, "onResume");
}

@Override
public void onPause() {
   super.onPause();
   Log.d(LOG_TAG, "onPause");
}

@Override
public void onStop() {
   super.onStop();
   Log.d(LOG_TAG, "onStop");
}

@Override
public void onDestroy() {
   super.onDestroy();
   Log.d(LOG_TAG, "onDestroy");
}

1.4 Observe the log as the app runs

  1. Run your app.
  2. Click Android Monitor at the bottom of Android Studio to open the Android Monitor.
  3. Select the logcat tab.
  4. Type "Activity" in the Android Monitor search box.

    The Android logcat can be very long and cluttered. Because the LOG_TAG variable in each class contains either the words MainActivity or SecondActivity, this keyword lets you filter the log for only the things you're interested in. Log for Lifecycle State

  5. Experiment using your app and note that the lifecycle events occur in response to different actions. In particular, try these things:
    • Use the app normally (send a message, reply with another message.)
    • Use the back button to go back from the second activity to the main activity.
    • Use the left arrow in the action bar to go back from the second activity to the main activity.
    • Rotate the device on both the main and second activity at different times in your app and observe what happens in the log and on the screen. TIP: If you're running your app in an emulator, you can simulate rotation with Ctrl-F11 or Ctrl-Fn-F11.
    • Press the overview button (the square button to the right of Home) and close the app (tap the X).
    • Return to the home screen and restart your app.

Coding challenge

Note: All coding challenges are optional and are not prerequisites for later lessons.

Challenge: Watch for onDestroy() in particular. Why is onDestroy() called sometimes (after clicking the back button, or on device rotation) and not others (manually stopping and restarting the app)?

Task 2. Save and restore the activity instance state

Depending on system resources and user behavior, the activities in your app may be destroyed and reconstructed far more frequently than you might think. You may have noticed this set of activities in the last section when you rotated the device or emulator. Rotating the device is one example of a device configuration change. Although rotation is the most common one, all configuration changes result in the current activity being destroyed and recreated as if it were new. If you don't account for this behavior in your code, when a configuration change occurs, your activity's layout may revert to its default appearance and initial values, and your user may lose their place, their data, or the state of their progress in your app.

The state of each activity is stored as a set of key/value pairs in a Bundle object called the activity instance state. The system saves default state information to instance state bundle just before the activity is stopped, and passes that bundle to the new activity instance to restore.

To keep from losing data in your activities when they are unexpectedly destroyed and recreated, you need to implement the onSaveInstanceState() method. The system calls this method on your activity (between onPause() and onStop()) when there is a possibility the activity may be destroyed and recreated.

The data you save in the instance state is specific to only this instance of this specific activity during the current app session. When you stop and restart a new app session, the activity instance state is lost and your activities will revert to their default appearance. If you need to save user data between app sessions, use shared preferences or a database. You'll learn about both of these in a later practical.

2.1 Save the activity instance state with onSaveInstanceState()

You may have noticed that rotating the device does not affect the state of the second activity at all. This is because the second activity's layout and state are generated from the layout and the intent that activated it. Even if the activity is recreated, the intent is still there and the data in that intent is still used each time the second activity's onCreate() is called.

In addition, you may notice that in both activities, any text you typed into message or reply EditTexts is retained even when the device is rotated. This is because the state information of some of the views in your layout are automatically saved across configuration changes, and the current value of an EditText is one of those cases.

The only activity states you're interested in are the TextViews for the reply header and the reply text in the main activity. Both TextViews are invisible by default; they only appear once you send a message back to the main activity from the second activity.

In this task you'll add code to preserve the instance state of these two TextViews using onSaveInstanceState().

  1. Open java/com.example.android.twoactivities/MainActivity.
  2. Add this skeleton implementation of onSaveInstanceState() to the activity, or use Code > Override Methods to insert a skeleton override.
    @Override
    public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
    }
    
  3. Check to see if the header is currently visible, and if so put that visibility state into the state bundle with the putBoolean() method and the key "reply_visible".

    if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
        outState.putBoolean("reply_visible", true);
    }
    

    Remember that the reply header and text are marked invisible until there is a reply from the second activity. If the header is visible, then there is reply data that needs to be saved. We're only interested in that visibility state -- the actual text of the header doesn't need to be saved, because that text never changes.

  4. Inside that same check, add the reply text into the bundle.

    outState.putString("reply_text", mReplyTextView.getText().toString());
    

    If the header is visible you can assume that the reply message itself is also visible. You don't need to test for or save the current visibility state of the reply message. Only the actual text of the message goes into the state bundle with the key "reply_text".

    We only save the state of those views that might change after the activity is created.

    The other views in your app (the EditText, the Button) can be recreated from the default layout at any time.

    Note: The system will save the state of some views, such as the contents of the EditText.

Solution Code (not the entire class):

@Override
public void onSaveInstanceState(Bundle outState) {
   super.onSaveInstanceState(outState);

   // If the heading is visible, we have a message that needs to be saved.
   // Otherwise we're still using default layout.
   if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
       outState.putBoolean("reply_visible", true);
       outState.putString("reply_text", mReplyTextView.getText().toString());
   }
}

2.2 Restore the activity instance state in onCreate()

Once you've saved the activity instance state, you also need to restore it when the activity is recreated. You can do this either in onCreate(), or by implementing the onRestoreInstanceState() callback, which is called after onStart() after the activity is created.

Most of the time the better place to restore the activity state is in onCreate(), to ensure that your user interface including the state is available as soon as possible. It is sometimes convenient to do it in onRestoreInstanceState() after all of the initialization has been done, or to allow subclasses to decide whether to use your default implementation.

  1. In the onCreate() method, add a test to make sure the bundle is not null.

    if (savedInstanceState != null) {
    }
    

    When your activity is created, the system passes the state bundle to onCreate() as its only argument. The first time onCreate() is called and your app starts, the bundle is null - there's no existing state the first time your app starts. Subsequent calls to onCreate() have a bundle populated with any the data you stored in onSaveInstanceState().

  2. Inside that check, get the current visibility (true or false) out of the bundle with the key "reply_visible"
    if (savedInstanceState != null) {
        boolean isVisible =
        savedInstanceState.getBoolean("reply_visible");
    }
    
  3. Add a test below that previous line for the isVisible variable.

    if (isVisible) {
    }
    

    If there's a reply_visible key in the state bundle (and isVisible is thus true), we will need to restore the state.

  4. Inside the isVisible test, make the header visible.
    mReplyHeadTextView.setVisibility(View.VISIBLE);
    
  5. Get the text reply message from the bundle with the key "reply_text", and set the reply TextView to show that string.
    mReplyTextView.setText(savedInstanceState.getString("reply_text"));
    
  6. Make the reply TextView visible as well:
    mReplyTextView.setVisibility(View.VISIBLE);
    
  7. Run the app. Try rotating the device or the emulator to ensure that the reply message (if there is one) remains on the screen after the activity is recreated.

Solution Code (not the entire class):

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   Log.d(LOG_TAG, "-------");
   Log.d(LOG_TAG, "onCreate");

   // Initialize all the view variables.
   mMessageEditText = (EditText) findViewById(R.id.editText_main);
   mReplyHeadTextView = (TextView) findViewById(R.id.text_header_reply);
   mReplyTextView = (TextView) findViewById(R.id.text_message_reply);

   // Restore the saved state. See onSaveInstanceState() for what gets saved.
   if (savedInstanceState != null) {
       boolean isVisible = savedInstanceState.getBoolean("reply_visible");
       // Show both the header and the message views. If isVisible is
       // false or missing from the bundle, use the default layout.
       if (isVisible) {
           mReplyHeadTextView.setVisibility(View.VISIBLE);

           mReplyTextView.setText(savedInstanceState.getString("reply_text"));
           mReplyTextView.setVisibility(View.VISIBLE);
       }
   }
}

Solution code

Android Studio Project: TwoActivitiesLifecycle

Coding challenge

Note: All coding challenges are optional and are not prerequisites for later lessons.

Challenge: Create a simple shopping list builder app with two activities. The main activity contains the list itself, which is made up of ten (empty) text views. A button on the main activity labelled "Add Item" launches a second activity that contains a list of common shopping items (Cheese, Rice, Apples, and so on). Use Buttons to display the items. Choosing an item returns you to the main activity, and updates an empty TextView to include the chosen item.

Use intents to pass information between the two activities. Make sure that the current state of the shopping list is saved when you rotate the device.

Summary

  • The Activity lifecycle is a set of states an activity migrates through, beginning when it is first created and ending when the Android system reclaims that activity's resources.
  • As the user navigates between activities and inside and outside of your app, each activity moves between states in the activity lifecycle.
  • Each state in the activity lifecycle has a corresponding callback method you can override in your Activity class. Those lifecycle methods are:
    • onCreate()
    • onStart()
    • onPause()
    • onRestart()
    • onResume()
    • onStop()
    • onDestroy()
  • Overriding a lifecycle callback method allows you to add behavior that occurs when your activity transitions into that state.
  • You can add skeleton override methods to your classes in Android Studio with Code > Override.
  • Device configuration changes such as rotation results in the activity being destroyed and recreated as if it were new.
  • A portion of the activity state is preserved on a configuration change, including the current values of of EditTexts. For all other data, you must explicitly save that data yourself.
  • Save activity instance state in the onSaveInstanceState() method.
  • Instance state data is stored as simple key/value pairs in a Bundle. Use the Bundle methods to put data into and get data back out of the bundle.
  • Restore the instance state in onCreate(), which is the preferred way, or onRestoreInstanceState().

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

results matching ""

    No results matching ""