Handling Android Activity State Changes

Revision as of 20:42, 9 January 2019 by Neil (Talk | contribs)

Revision as of 20:42, 9 January 2019 by Neil (Talk | contribs)


Based on the information outlined in the chapter entitled “Understanding Android Application and Activity Lifecycles” it is now evident that the activities and fragments that make up an application pass through a variety of different states during the course of the application’s lifespan. The change from one state to the other is imposed by the Android runtime system 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 the ways in which an activity may be notified of a state change and to outline the areas where it is advisable to save or restore state information. Having covered this information, the chapter will then touch briefly on the subject of activity lifetimes.

New vs. Old Lifecycle Techniques

Up until recently, there was a standard way to build lifecycle awareness into an app. This is the approach covered in this chapter and involves implementing a set of methods (one for each lifecycle state) within an activity or fragment instance that get called by the operating system 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 important, 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 make use of some of them. More importantly, understanding the older way of handling lifecycles will provide a good knowledge foundation on which to begin learning the new approach later in the book.

The Activity and Fragment Classes

With few exceptions, activities and fragments in an application are created as subclasses of the Android AppCompatActivity class and Fragment classes respectively.

Consider, for example, the simple AndroidSample project created in “Creating an Example Android App in Android Studio”. Load this project into the Android Studio environment and locate the AndroidSampleActvity.java file (located in app -> java -> com.<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.ebookfrenzy.androidsample;
 
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.TextView;
 
public class AndroidSampleActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_android_sample);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
 
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }
 
    public void convertCurrency(View view) {
 
        EditText dollarText = findViewById(R.id.dollarText);
        TextView textView = findViewById(R.id.textView);
 
        if (!dollarText.getText().toString().equals("")) {
 
            Float dollarValue = Float.valueOf(dollarText.getText().toString());
            Float euroValue = dollarValue * 0.85F;
            textView.setText(euroValue.toString());
        } else {
            textView.setText("No Value");
        }
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_android_sample, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
 
        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
}

When the project was created, we instructed Android Studio also to create an initial activity named AndroidSampleActivity. As is evident from the above code, the AndroidSampleActivity 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 AndroidSampleActivity.java 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 12-1, AppCompatActivity is clearly subclassed from the FragmentActivity class which is itself ultimately a subclass of the Activity class:

As 3.0 class hierarchy.png


The Activity and Fragment classes contain a range of methods that are intended to be called by the Android runtime to notify the object when its state is changing. For the purposes of this chapter, we will refer to these as the lifecycle methods. An activity or fragment class simply needs to override these methods and implement the necessary functionality within them in order 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 AndroidSampleActivity class. In a later section we will explore in detail both 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 that is 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. Such state information, because it persists from one invocation of the application to another, 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 both the ways they are saved, and the reasons for doing so, differ.

The purpose of saving the persistent state is to avoid the loss of data that may result from an activity being killed by the runtime system while in the background. The dynamic state, on the other hand, is saved and restored for reasons that are slightly more complex.

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. Before performing an action to save these changes, however, the user then 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 consequently kills Activity A to free up resources. As far as the user is concerned, however, Activity A was simply placed into the background and is ready to be moved to the foreground at any time. On returning Activity A to the foreground the user would, quite 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 was not saved and restored, the previous user input lost.

The main purpose of saving dynamic state, therefore, is to give the perception of seamless switching between foreground and background activities, regardless of the fact that activities may actually have been killed and restarted without the user’s knowledge.

The mechanisms for saving persistent and dynamic state will become clearer in the following sections of this chapter.

The Android Lifecycle Methods

As previously explained, the Activity and Fragment classes contain a number of 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(Bundle savedInstanceState) – The method that is 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() in the event that 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 now no longer visible to the user. The two possible scenarios that may follow this call are a call to onRestart() in the event that the activity moves to the foreground again, or onDestroy() if the activity is being 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.
  • onActivityCreated() - The onCreate() method of the activity with which the fragment is associated has completed execution.
  • 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(Bundle savedInstanceState) – This method is called immediately after a call to the onStart() method in the event that the activity is restarting from a previous invocation in which state was saved. As with onCreate(), this method is passed a Bundle object containing the previous state data. This method is typically used in situations where it makes more sense to restore a previous state after the initialization of the activity has been performed in onCreate() and onStart().
  • onSaveInstanceState(Bundle outState) – 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 in situations where the runtime ascertains that dynamic state needs to be saved.

When overriding the above methods, it is important to remember that, with the exception of onRestoreInstanceState() and onSaveInstanceState(), the method implementation must include a call to the corresponding method in the super class. For example, the following method overrides the onRestart() method but also includes a call to the super class instance of the method:

protected void onRestart() {
       super.onRestart();
       Log.i(TAG, "onRestart");
}

Failure to make this super class call in method overrides will result in the runtime throwing an exception during execution. While calls to the super class 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() prior to the object terminating.
  • 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 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 12-2:

File:.png


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=".DemoActivity"
          android:configChanges="orientation|fontScale"
          android:label="@string/app_name">

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, shortcomings to this approach.

One issue with the lifecycle methods is that they do not provide an easy way for an activity or fragment to find out its current lifecycle state at any given point during app execution. Instead the object would need to 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 many other objects within an app can potentially be impacted by a lifecycle state change in a given activity or fragment.

The lifecycle methods are also only available on subclasses of the Fragment and Activity classes. It is not possible, therefore, to build custom classes that are truly lifecycle aware.

Finally, the lifecycle methods result in most of the 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 that are impacted by the state change. An app that streams video, for example, 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 of these problems and more are resolved by using lifecycle-aware components, a topic which 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 a number of lifecycle methods that are designed to be called by the runtime system when the state of an activity changes. Similarly, the Fragment class contains a number of 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 both 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 a number of limitations that can be avoided by making use of lifecycle-aware components, an understanding of these methods is important in order 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.