17
Visit: www.Intertech.com/Blog Saving (and Retrieving) Android Instance State This whitepaper is dedicated to explaining and exemplifying how Android (activity/fragment) state can be saved and restored. It provides information about the use of onSaveInstanceState( ) and onRestoreInstanceState( ) methods of the activity for saving and restoring state. Also, it gives a thorough explanation of a fragment’s setRetainInstance( ) and getRetainInstance( ) methods for managing state. The setRetainInstance( ) and getRetainInstance( ) methods are replacements for the now deprecated onRetainNonConfigurationInstance( ) and getLastNonConfigurationInstance( ) activity methods. These later two methods were deprecated as of Android 3.2. Why do we need to save instance state? Android activities have a lifecycle (for a diagram of this lifecycle see here). As an activity becomes partially hidden (paused) or fully hidden (stopped), possibly even destroyed, your applications need a way to keep valuable state (i.e. data) the activity has obtained (probably from user input) or created even after the activity has gone away. As an example, an orientation change can cause your activity to be destroyed and removed from memory and then recreated again. Unless you avail yourself to certain state saving mechanisms, any data your activity has at the time of orientation change can be completely lost in this seemingly simple act. How do we save activity instance state? The Activity class provides the first, and possibly easiest, means of saving and restoring activity state. The onSaveInstanceState( ) method is a callback method that is not shown on most Activity lifecycle diagrams. Nonetheless, this method is called by Android between the onPause( ) and onStop( ) methods. Important side note: the API documentation explicitly states the onSaveInstanceState( ) method will be called before onStop( ) but makes no guarantees it will be called before or after onPause( ). In the testing I did on the Android Virtual Device and two actually devices, it did get called between onPause( ) and onStop( ), but the quote below suggests one should not take this for granted.

Saving (and Retrieving) Android Instance State · Saving (and Retrieving) Android Instance State This whitepaper is dedicated to explaining and exemplifying how Android (activity/fragment)

  • Upload
    others

  • View
    9

  • Download
    0

Embed Size (px)

Citation preview

Visit: www.Intertech.com/Blog

Saving (and Retrieving) Android Instance State

This whitepaper is dedicated to explaining and exemplifying how Android (activity/fragment) state can be

saved and restored. It provides information about the use of onSaveInstanceState( ) and

onRestoreInstanceState( ) methods of the activity for saving and restoring state. Also, it gives a thorough

explanation of a fragment’s setRetainInstance( ) and getRetainInstance( ) methods for managing state. The

setRetainInstance( ) and getRetainInstance( ) methods are replacements for the now deprecated

onRetainNonConfigurationInstance( ) and getLastNonConfigurationInstance( ) activity methods. These later

two methods were deprecated as of Android 3.2.

Why do we need to save instance state?

Android activities have a lifecycle (for a diagram of this lifecycle see here). As an activity becomes partially

hidden (paused) or fully hidden (stopped), possibly even destroyed, your applications need a way to keep

valuable state (i.e. data) the activity has obtained (probably from user input) or created even after the activity

has gone away.

As an example, an orientation change can cause your activity to be destroyed and removed from memory and

then recreated again. Unless you avail yourself to certain state saving mechanisms, any data your activity has

at the time of orientation change can be completely lost in this seemingly simple act.

How do we save activity instance state?

The Activity class provides the first, and possibly easiest, means of saving and restoring activity state. The

onSaveInstanceState( ) method is a callback method that is not shown on most Activity lifecycle

diagrams. Nonetheless, this method is called by Android between the onPause( ) and onStop( ) methods.

Important side note: the API documentation explicitly states the onSaveInstanceState( ) method will be called

before onStop( ) but makes no guarantees it will be called before or after onPause( ). In the testing I did on

the Android Virtual Device and two actually devices, it did get called between onPause( ) and onStop( ), but

the quote below suggests one should not take this for granted.

Consultants Who Teach

2

“If called, this method will occur before onStop(). There are no guarantees about whether it will occur

before or after onPause().”

The onSaveInstanceState( ) method is passed a Bundle object as a parameter. Data you want saved for the

activity should be placed in the Bundle. In loose terms, a Bundle is a custom Android hash map. Data you

want saved for the activity (for its redisplay or recreation) are stored in the Bundle with a String key. The key

allows the data to be retrieved back from the Bundle.

Saving State using onSaveInstanceState( ) Example

As an example, say your activity has a Calendar object property. This property might be used to track and

display the “start time” when a user opened the activity and started entering data (not a far-fetched idea given

that some transactions must be completed within a certain amount of time or are discarded).

public class DataEntryActivity extends Activity {

private Calendar startTime = Calendar.getInstance();

// ... the rest of the activity

}

Consultants Who Teach

3

To underscore the importance of saving an activity’s state, what would happen if the orientation of the device

were to change while the user is looking at this activity? Indeed, because the activity would be destroyed and

recreated on an orientation change, a brand new instance of the activity is created and therefore a new “start

time” would be created and displayed to the user on this orientation change. Yikes! You would want the start

time to be maintained, even if the actual activity instance is not the same one that kicked off the data entry.

To keep the start time state across activity lifecycle events (to include destroy and recreation), implement the

onSaveInstanceState( ) method as follows:

@Override

protected void onSaveInstanceState(Bundle state) {

super.onSaveInstanceState(state);

state.putSerializable("starttime", startTime);

}

The start time data is now saved under the key of “starttime” in the bundle. Again, this method will be called

automatically by Android before onStop( ) and the serialized Calendar object will be saved in the bundle which

gets maintained and used across any instance of this activity.

Important side note: the bundle is for data managed by instances of the activity class and not across activity

classes. In other words, you can not put state in the bundle of Activity A and have Activity B opened and

expect to see the state of Activity A in its bundle.

How do we retrieve saved state?

Consultants Who Teach

4

If you notice, the onCreate( ) lifecycle method of the Activity class is passed a Bundle object as a

parameter. This is the same Bundle object that was used to save state data in the onSaveInstanceState( )

method as shown above. Android passes the Bundle to each instance of an activity it creates. So, you could

programmatically reach into that Bundle object to retrieve state, like the Calendar object above, in the

onCreate( ) method. The code example below (specifically lines 8-12) does exactly that.

1: @Override

2: protected void onCreate(Bundle bundle) {

3: super.onCreate(bundle);

4: Log.v(TAG, "Inside of onCreate");

5: setContentView(R.layout.activity_data_entry);

6: timeTV = (TextView) findViewById(R.id.time_tv);

7:

8: if ((bundle != null)

9: && (bundle.getSerializable("starttime") != null)) {

10: startTime = (Calendar) bundle

11: .getSerializable("starttime");

12: }

13: }

Import side note: notice the check for the bundle being null? Remember, the onCreate( ) method gets called

each time the activity is created, to include the very first time the activity is created and before there was ever

any state. So make sure you check for the bundle being null before trying to use it and extract state data. The

bundle will be null on the first call to onCreate( ).

Alternately, use the onRestoreInstanceState( ) method to extract saved state from the Bundle and restore it into

new instances of the activity. The onRestoreInstanceState( ) method is another callback lifecycle method that

is also rarely seen in the activity lifecycle diagrams. The onRestoreInstanceState( ) method is automatically

invoked by Android between the onStart( ) and the onResume( ) lifecycle methods.

Consultants Who Teach

5

So, in order to restore the state of your activity, simply implement the onRestoreInstanceState( ) method to

have it retrieve and restore activity state. This allows you to separate creation code from state restoring

code. The example code below restores the Calendar “start time.”

@Override

protected void onRestoreInstanceState(Bundle savedInstanceState) {

super.onRestoreInstanceState(savedInstanceState);

Log.v(TAG, "Inside of onRestoreInstanceState");

startTime = (Calendar) savedInstanceState.getSerializable("starttime");

}

You’ll notice that the check for null isn’t necessary as Android will have created an empty Bundle object by

the time the method is called even on the initial creation of the activity.

What kind of state can be saved in the Bundle?

As I indicated above, the Bundle object is a kind of simple Android hash map. So data is stored in key value

pairs. The keys are always Strings. The values must be of android.os.Parcelable type. Parcelables are close to

Java’s Serializable, but not the same. In fact, the Android documentation is quick to point out that the

Parcelable API “is not a general-purpose serialization mechanism.” Without getting into all the gory details of

the Parcelable type, suffice it to say that types of data that can be placed into the value side of a Bundle are

listed in the table below.

Allowed Bundle Value Types

boolean

boolean[ ]

Consultants Who Teach

6

Bundle (yes – store a Bundle in a Bundle)

byte

byte[ ]

char

char[ ]

CharSequence

CharSequence[ ]

ArrayList<CharSequence>

double

double[ ]

float

float[ ]

int

int [ ]

ArrayList<Integer>

long

long[ ]

Serializable

short

short[ ]

SparseArray – a map of integer to Object and is more

efficient than a HashMap. Used more internally by

Android (see below)

Consultants Who Teach

7

String

String[ ]

ArrayList<String>

Automatic View State Saving/Restoring

The Bundle, the onSaveInstanceState( ) and onRestoreInstanceState( ) methods are already used by the

Activity super class to automatically save and restore the state of any View object in the Activity so long as the

View component has an id. To demonstrate, say in the data entry form used in my examples, you start to enter

some data in the “Your Data” EditText entry field. If the orientation were to change when the user has

provided data in the EditText field, that data would be lost since the Activity instance and its associated view

components need to be destroyed and recreated. But as you can see below, the data is not lost! How does this

happen?

Android stores all View component data (to which EditText is a subclass) in the Bundle automatically so long

as you have provided the View an id property. The View’s id serves as the key in the Bundle to the data that

needs to be restored. In fact, adding a little bit of code to onRestoreInstanceState( ) as shown below

(specifically the code of lines 4-16) allows you to see the automatically saved View state.

1: @Override

2: protected void onRestoreInstanceState(Bundle savedInstanceState) {

Consultants Who Teach

8

3: startTime = (Calendar) savedInstanceState.getSerializable("starttime");

4: Bundle viewHierarchy = savedInstanceState

5: .getBundle("android:viewHierarchyState");

6: if (viewHierarchy != null) {

7: SparseArray views = viewHierarchy.getSparseParcelableArray("android:views");

8: if (views != null) {

9: for (int i = 0; i < views.size(); i++) {

10: Log.v(TAG, "key -->" + views.get(i));

11: Log.v(TAG, "value --> " + views.valueAt(i));

12: }

13: }

14: } else {

15: Log.v(TAG, "no view data");

16: }

17: super.onRestoreInstanceState(savedInstanceState);

18: Log.v(TAG, "Inside of onRestoreInstanceState");

19: }

Inside of the Bundle, under a key of “android:viewHierarchyState” Android puts another Bundle object. This

bundle holds, as its name implies, the View state. Inside the view hierarchy state bundle Android stores a

SparseArray under the key “android:views.” A sparse array is an Android specific data structure that is an

integer/index to Object map and is more efficient than a HashMap. Inside of the SparseArray is the View state

data stored per View component id. Using the Eclipse debug inspector I have a snapshot of the SparseArray

holding the state of my simple Activity (specifically the text data in the Your data EditText view).

Consultants Who Teach

9

Again, you don’t have to do anything special to allow Android to save and restore View state so long as you

provide the View components and id property. Also, and very importantly, if you do provide your own

implementation of onSaveInstanceState( ) and onRestoreInstanceState( ) methods, remember to call to the

super (Activity class) implementation of these methods. It is in the super class implementations that the work

Consultants Who Teach

10

of saving and restoring View state is actually accomplished. So,if you were to leave out the call to

super.onSaveInstanceState( ) in your onSaveInstanceState( ) method, the View data would not be saved

automatically!

Fragments

Fragments were introduced to the Android UI with API 11 (Android 3.0, Honeycomb). A fragment, while

similar to an activity in many ways, represents only a “piece of an application’s user interface or behavior.” A

fragment must be embedded in an activity (its “host”) and its life is tied to the life of the hosting

activity. Fragments were added primarily as a means to support larger screens such as tablets. Fragments

allow the UI layout of an activity to be more easily divided. Multiple divisions – the fragments – can be

displayed together when a screen is larger. A single fragment can be displayed when the screen is smaller. All

without having to actually change the activity itself. See this reference document for more information on the

design philosophy around fragments. To learn more about fragments, you can check out this set of online

tutorials here.

Fragment Lifecycle and onSaveInstanceState( )

Not unlike activities, fragments have a lifecycle too. In fact, the fragment lifecycle is closely tied to the

activity lifecycle and so too are the lifecycle callback methods similar to the lifecycle methods of an

activity. Therefore, it should come as no surprise that the state of a fragment instance can be saved via the

fragment’s onSaveInstanceState( ) method.

Just like the activity’s onSaveInstanceState( ) method described in my last post, this method usually gets called

between a fragment’s onPause( ) and onStop( ) methods. To be complete, the documentation states that the

method “may be called anytime before onDestroy().”

Further, just like the activity’s onSaveInstanceState( ) method described in my last post, this method gets

passed a Bundle object. You can use store any data you need to retain between fragment (and/or the associated

activity) instances in that Bundle.

Consultants Who Teach

11

One difference between a fragment and an activity is that there is no onRestoreInstanceState( ) method to

retrieve and restore the state in a fragment. Instead, the fragment’s on onCreate( ), onCreateView( ) and

onActivityCreated( ) lifecycle methods all get passed the Bundle object. You can use these methods and the

passed in Bundle containing the fragment’s saved state to restore any state you need. Keep in mind, the

Bundle passed to these methods will be null unless the system is recreating the fragment (i.e. this is not the

first time the methods are called on the fragment). So when trying to restore state, put a null check in place to

before accessing the Bundle.

Just like an activity, a fragment will automatically save the data of any fragment View component in the

Bundle by the View component’s id. And just like in the activity, if you do implement the

onSaveInstanceState( ) method make sure you add calls to the super.onSaveInstanceState( ) methods so as to

retain this automatic save of View data feature.

Fragment onSaveInstanceState( ) example

As with my last post, assume your display needed to show the user when they arrived on an activity (and

associated fragment) screen and started to enter data. Even when the orientation or other configuration change

happens, you want to make sure the display of that start time does not change and is retained. Here is how to

maintain the start time state using the fragment’s onSaveInstanceState( ) method.

First, assume a Calendar instance variable is provided on the fragment to hold the current “start time” and this

information is displayed in a TextView component of the fragment.

1: public class DataEntryFragment extends Fragment {

2:

3: private Calendar startTime = Calendar.getInstance();

4:

5: }

Next, implement the onSaveInstanceState( ) method to store the original start time in the state Bundle so it can

be retrieved and reused when the fragment is recreated.

1: @Override

2: public void onSaveInstanceState(Bundle outState) {

Consultants Who Teach

12

3: super.onSaveInstanceState(outState);

4: Log.v(TAG, "In frag's on save instance state ");

5: outState.putSerializable("starttime", startTime);

6: }

Finally, in one of the Bundle methods (onCreate, onCreateView, or onActivityCreated) – I have choose the

onCreateView( ) here – pull the data out of the Bundle parameter passed in and restore the start time instance

variable. Again, note the null check to handle the case of when the fragment is created for the very first time.

1: @Override

2: public View onCreateView(LayoutInflater inflater, ViewGroup container,

3: Bundle savedInstanceState) {

4: Log.v(TAG, "In frag's on create view");

5: View view = inflater.inflate(R.layout.fragment_data_entry, container,

6: false);

7: timeTV = (TextView) view.findViewById(R.id.time_tv);

8: if ((savedInstanceState != null)

9: && (savedInstanceState.getSerializable("starttime") != null)) {

10: startTime = (Calendar) savedInstanceState

11: .getSerializable("starttime");

12: }

13: return view;

14: }

Understanding Configuration Changes

One of the major reasons for retaining state in fragments and activities is because of runtime configuration

changes. Whenever a device’s runtime configuration changes, the activity and associated fragments go

through a complete lifecycle. That is, they are destroyed and recreated. When the instances are destroyed,

Consultants Who Teach

13

their state (i.e. data) is lost unless captured and provided to the next instance – as was accomplished in the

onSaveInstanceState( ) method above.

What constitutes a runtime configuration change you might ask? There are several things that are considered

runtime configuration change triggers in Android. Orientation is the most well known configuration

change. When the device is rotated (portrait to landscape or vice versa), the device changes its runtime

configuration – causing a destroy and recreation of the activities and fragments. Other configuration changes

include the change of a language, addition or removal of an input device (such as a keyboard), or when a

device changes dock status (when a device is plugged into/unplugged from a car or desk dock).

Understanding what constitutes a configuration change and implementing the right state saving means, as

discussed below, can also allow your activity and fragment state to be saved automatically.

How to Retain the Fragment Instance with setRetainInstance( )

On the Fragment class is a pair of get/set methods called setRetainInstance( ) and getRetainInstance( ). The

setter takes a boolean and the getter, obviously, returns a boolean. The boolean is an indication of whether a

fragment instance should be retained across any associated activity’s re-creation during such actions as a

configuration change. In other words, a fragment instance does not have to be completely destroyed and

recreated when an activity is destroyed and recreated. If a fragment is told to retain its instance with a call to

setRetainInstance(true), then the fragment lifecycle is altered such that the fragment’s onDestroy( ) and

onCreate( ) methods will not be called and the existing instance is associated to the new activity instance.

On the orientation change of a device, for example, the existing instance of a fragment is provided to the new

instance of the associated activity when it gets recreated.

Consultants Who Teach

14

This has profound impacts on the development of applications. It means, for example, that much of the state

of the application does not need to be stored in some bundle during configuration changes. Instead, the

fragment itself serves as a state holder for data of the associated activity during configuration changes such as

the orientation change of the device.

Take, for example, the “start time” Calendar instance from the example above. By simply calling on

setRetainInstance(true) on a fragment (probably calling on this method shortly after the creation of the

fragment as shown below), the fragment does not get destroyed and recreated on orientation change so the

Calendar instance does not need to be otherwise persisted.

1: @Override

2: public void onCreate(Bundle savedInstanceState) {

3: super.onCreate(savedInstanceState);

4: Log.v(TAG, "In frag's on create");

Consultants Who Teach

15

5: this.setRetainInstance(true);

6: }

What’s more, the state stored with the fragment does not have to be Parcelable or of Serializable form. More

complex objects and object graphs can remain with the fragment and never be removed from memory until the

fragment is removed from memory. You’ll even find app designers advocating the use of fragments to host a

SQLiteDatabase objects, Threads or AsyncTasks (see here for one such recommendation) because the

fragment instance can be kept around so long – making it easier to manage and access the thread or task.

Activity onRetainNonConfigurationInstance( )/getLastNonConfigurationInstance( )

Activities also have a onRetainNonConfigurationInstance( ) method that is called by the Android OS as part of

the lifecycle. This method gets called between the activity’s onStop( ) and onDestroy( ) method on any

configuration change and can also be used to save activity state. You must implement the

onRetainNonConfigurationInstance( ) method and have it return any object you’d like. This object, your state

holder if you will, is “saved” and can be retrieved by calling getLastNonConfigurationInstance( ) in the newly

created activity instance after a configuration change. The code below demonstrates how to implement

onRetainNonConfigurationInstance( ) to save the “start time.”

1: @SuppressWarnings("deprecation")

2: @Override

3: public Object onRetainNonConfigurationInstance() {

4: Log.v(TAG, "Inside of onRetainNonConfigurationInstance");

5: return startTime;

6: }

Presumably, you would call getLastNonConfigurationInstance( ) in something like onCreate( ) or onStart( ) of

the activity to restore the state stored in this object. Below, the getLastNonConfigurationInstance( ) method is

used from onStart( ) to restore the example “start time.” Note the null check in the code. When the activity is

created or during times when the onStart( ) method is called not after a configuration change, the

getLastNonConfigurationInstance( ) method will return null.

1: @SuppressWarnings("deprecation")

2: @Override

Consultants Who Teach

16

3: protected void onStart() {

4: super.onStart();

5: Log.v(TAG, "Inside of onStart");

6: Calendar oldTime = (Calendar) getLastNonConfigurationInstance();

7: if (oldTime != null) {

8: startTime = oldTime;

9: Log.v(TAG, "start time restored");

10: }

11: }

Like using the fragment to save state, this method is a little more versatile in that any object can be saved and

retrieved with the onRetainNonConfigurationInstance( ) and getLastNonConfigurationInstance( ) methods. In

fact, the Android documentation even suggests that even “the activity instance itself” could be stored and later

retrieved with this mechanism.

Importantly, the onRetainNonConfigurationInstance( ) and getLastNonConfigurationInstance( ) methods

have been deprecated with Android 3.0 (API level 13). You’ll note the @SuppressWarnings annotations in

the code above. There replacement is considered to be the fragment’s setRetainInstance(boolean) mechanism

described above. If you are building applications for older platforms, the fragment capability, complete with

setRetainInstance( ) is available through the Android compatibility package (see here for details).

Comparing/Contrasting State Saving Mechanisms

So, over the past two blog posts, I’ve shown you a couple of means to retain state in activities and/or fragments

to allow data in the activity/fragment to survive activity destruction and configuration changes such as the

orientation change of the device. Which option is best? The answer will depend on your application needs.

Data Type of the Saved State

First, the onSaveInstanceState( ) mechanism of an activity or fragment is based on saving information into a

Bundle object. The bundle object requires the information to be Parcelable. Whereas, the state held by a

retained fragment instance (use setRetainInstance set to true) has no such limitation. Any information can be

stored in the fragment and retained – even something like a thread or task object. Similarly, although now

Consultants Who Teach

17

deprecated, the activity’s onRetainNonConfigurationInstance( ) method would allow the storing/retrieving of

any type of java.lang.Object to be saved across activity instances.

Limitations of Fragments and NonConfig

Using a fragment or the now deprecated onRetainNonConfigurationInstance( ) have some limitations. The

fragment setRetainInstance( ) can only be used with fragments “not in the back stack.” In other words, the

fragment cannot be on an activity that has been pushed back into the activity display stack only to be restored

once a user has pushed the back button (see here for more details about the back stack).

Further, with regard to onRetainNonConfigurationInstance( ), not all state is triggered because of configuration

changes. For example, sometimes Android needs to destroy an activity because it is out of some resource like

memory. The onSaveInstanceState( ) method would handle the case of saving state when an activity must be

destroyed to preserve memory and then brought back to display. The configuration triggering mechanisms

would not always work for this state capture need.

Speed

When a fragment is used to retain state, there really isn’t anything physically being saved or restored in your

code. The fragment just doesn’t get destroyed and recreated by Android and so any data it has does not have

to get stored and recreated. Therefore, using the fragment to maintain state is painless and fast. Using

onSaveInstanceState( ), on the other hand, the data must be entered into a bundle, managed by Android, and

then retrieved by your application from the bundle and restored into is proper place. All this work of

managing the data with onSaveInstanceState( ) and onRestoreInstanceState( ) can be a bit of a time consuming

process depending on how much data has to be dealt with.

So, as always in software engineering, there is no perfect solution – no free lunch. Use the state saving

mechanism that is right for your application needs. And in some cases, you might find your application needs

to use different state saving mechanisms in different situations.

Finally, when state needs to be saved across shutdown/startup of the application (or even the device) you may

have to look at more industrial persistence mechanisms such as the SQLite database or file storage.

Wrap Up

If you need to learn more about Android, consider Android classes at Intertech. If you need some help on

your Android project, please consider hiring our consultants.