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.