Writing for Android can be challenging due to our pre-conceived notions about program state. First, refresh your reading of the life cycle of the Activity class and revisit the Android Activities page. Then review the Common Tasks in Android page, especially "Storing and Retrieving State." Still confused! The bottom line is that we cannot rely on the instance variables to preserve the object state. First, here are four observations.
1) When the user changes the orientation of the phone this will do a soft kill our Activity, but not until it first calls onSaveInstanceState. Non view state will be lost. View state with an ID (mostly) will be saved automagically by the OS. We have a chance to save non view state in onSaveInstanceState. So on a soft kill the following methods are called (not inclusive):
onSaveInstanceState(b)
onPause()
onStop()
onDestroy()
onCreate() // app resurrected after soft kill
2) If the OS runs short on memory while our activity is paused this will also do a soft kill our activity, but first the activity will call onSaveInstanceState. Non view state will be lost. View state will be saved. According to the Activity diagram and the docs, we can be sure that the there was a call to:
onPause() // prior to being paused
onSaveInstanceState(b) // before the soft kill
onCreate() // app resurrected after soft kill
3) If the USER does a hard kill by hitting the back button with our activity in focus, ALL state is lost since the OS will NOT call onSaveInstanceState and the OS will NOT save view state. So on a hard kill and relaunch the following methods are called (not inclusive):
onPause()
onStop()
onDestroy()
onCreate() // app relaunches after hard kill
4) Given: A parent activity launches a child activity using the call startActivityForResult. The child activity calls setResult. If the user changes the orientation of the phone, the result will be RESULT_CANCEL, NOT RESULT_OK and the state in setResult may be lost! So consider calling activity.finish() IMMEDIATELY after the call to setResult.
So we can save non-view instance state on a soft kill in onSaveInstanceState(b) using bundles. We can save non-view instance state on a hard kill, by convention, in onStop() or in onDestroy() using preferences. We can save "shared document-like data" using database writes, by convention, in onPause().
Then we can restore non-view instance state, by convention, in onCreate(). We can restore "shared document-like data", using database reads, in onResume();
So if we are going to use the "edit in place" or auto-commit model, "shared document-like data" is persisted in onPause() (typically to a database) while "internal state" is persisted in onStop() or onDestroy() (typically to a preferences file). It IS possible, for small chunks of view state, to save the view state in onStop() and restore the view from the data in onCreate.
Note: It is possible to set a flag isSavedInstanceState and use this flag to intelligently save state on a soft kill (onSaveInstanceState) vs a hard kill (onStop). See "Generic Template" below.)
Well, Not All Views Save State
Many, but not all, Android widgets, "those with an ID whose state can be changed by the user", appear to save their state on a soft kill. So EditText and CheckBox save their state, but TextView does not save its state on a soft kill. Although the text in EditText may be saved automagically on a soft kill, other state in EditText may NOT be saved. An examples of EditText state that is not saved is: programmatic changes to Hints and Transformation Methods.
Note: for reasons I cannot fathom, you may need to set TransformationMethods in onResume, not onCreate().
Note on configChanges
Although, in select cases, you may be need to reduce the calls to onSaveInstanceState and onCreate by defining android:configChanges= "orientation|keyboardHidden" in the file AndroidManifest.xml, you will still need to implement a plan to save internal state, as a soft kill can still occur if other applications need memory OR there is an incoming call. According to the docs:
"Always keep in mind that Android is a mobile platform. It may seem obvious to say it, but it's important to remember that another Activity (such as the "Incoming Phone Call" app) can pop up over your own Activity at any moment. This will fire the onSaveInstanceState() and onPause() methods, and will likely result in your application being killed."
Furthermore see Handling Runtime Changes:
"However, your application should always be able to shutdown and restart with its previous state intact. Not only because there are other configuration changes that you cannot prevent from restarting your application but also in order to handle events such as when the user receives an incoming phone call and then returns to your application."
So, save your internal instance state, even if you use android:configChanges.
Lets Try It
To understand what is happening add the following variables, calls and methods to your Activity class:
Add an instance variable password.
private String password="";
In the onCreate(Bundle savedInstanceState) method, add the following code:
// check for saved state
if( savedInstanceState!= null){ // get saved state
try {
password= savedInstanceState.getString("password");
Log.d(TAG,"RestoredState!");
}
catch(Exception e){
Log.d(TAG,"FailedToRestore",e);
}
}
Log.d(TAG,"onCreate");
The onCreate method will be called "the first time" and then every time the user changes the orientation of the phone! The bundle savedInstanceState should be null on the first call to onCreate.
Note: You can leverage the fact that the savedInstanceState bundle will be null on the first call to onCreate, to launch a splash screen only on a "new launch".
Now add the onSaveInstanceState method. This method is called on a soft kill when your Activity is killed such as when the user changes the orientation of the phone. The Activity can also be killed when your Activity has been paused and the OS needs to free up memory!
// ON_SAVE_INSTANCE_STATE
// save instance data on kill
protected void onSaveInstanceState(Bundle outState){
password= editTextPassword.getText().toString();
outState.putString("password", password);
super.onSaveInstanceState(outState); // save view state
}
We will also add the onPause, onStart, onResume and onStop methods. Note the calls to super.
// TEST ON SAVE
protected void onPause() {
Log.d(TAG,"onPause");
super.onPause();
}
protected void onStart() {
Log.d(TAG,"onStart");
super.onStart();
}
protected void onResume() {
Log.d(TAG,"onResume");
super.onResume();
}
protected void onStop() {
Log.d(TAG,"onStop");
super.onStop();
}
Run the app and watch the LogCat output after filtering for our TAG, Confuse Text. Change the orientation of the phone to horizontal in the emulator using ctrl-F11! Note: You may need to go to prefs-->keyboard-->Select use all F1 ... keys as standard function keys.
OK. STOP turning the phone now. You are having too much fun.
Now. Hit the back button, killing the app. onSaveInstance is NOT called.
It should be obvious that onSaveInstanceState, onPause and onCreate is being called each time the phone changes orientation. If you wish to persist non-view instance values, you will need to save these values in the method onSavedInstanceState() and you will need to extract these values in the method onCreate(). If you wish to save other data besides instance state, you may wish to write that data in the method onPause(). If you wish to save non-view instance state on a hard kill, you may wish to write to preferences in onStop() or onDestroy().
LogCat
If you are having trouble understanding how to use LogCat or view the log, go here.
Seeing Double
There is a bug in the EMULATOR that may cycle through a lifecycle twice on orientation change.
Saving State When User Hits Back Button
Again, this may be counter-intuitive, but when your app has the focus and the user hits back, your app is killed WITHOUT calling onSaveInstanceState! Yup. Your state will be lost, both view state and non view state. To save non view user state when your app is killed by the USER, you can save the state in the app wide preferences object.
Save your state in onStop() as in:
// need to save state to prefs when USER hits back and KILLS us without calling onSaveInstanceState
@Override
protected void onStop(){
super.onStop();
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("isShowCharCount",isShowCharCount);
editor.putLong("timeStart",timeStart);
editor.putLong("timeExpire",Calendar.getInstance().getTimeInMillis()+LONG_YEAR_MILLIS);
editor.putInt("timeoutType",timeoutType);
editor.putBoolean("isTimeout",isTimeout);
editor.putBoolean("isValidKey",isValidKey);
editor.putString("password",password);
// Commit the edits!
editor.commit();
}
Restore your state in onCreate as in:
// Restore preferences USER hit back and killed us WITHOUT calling onSaveInstanceState
SharedPreferences prefs = getPreferences(MODE_PRIVATE); // singleton
if (prefs != null){
this.isShowCharCount= prefs.getBoolean("isShowCharCount",true);
this.timeStart= prefs.getLong("timeStart",Calendar.getInstance().getTimeInMillis());
this.timeExpire= prefs.getLong("timeExpire",Calendar.getInstance().getTimeInMillis()+LONG_YEAR_MILLIS);
this.timeoutType= prefs.getInt("timeoutType",TIMEOUT_NEVER);
this.isTimeout= prefs.getBoolean("isTimeout",false);
this.isValidKey= prefs.getBoolean("isValidKey",false);
this.password= prefs.getString("password","");
}
This will not restore the view state, so you will need to code appropriately.
If we only save save state to prefs on a hard kill (see "generic template" below), we can avoid reading in the prefs every time in onCreate(inState) by leveraging the fact that the bundle returned in onCreate is null when the Activity is launched for the "first time":
// RESTORE STATE HERE
// Save state in onStop (prefs) and onSaveInstanceState (bundle)
if( bundle!= null){ // get saved state from soft kill after first pass
try {
Log.d(TAG,"RestoredState!");
}
catch(Exception e){
Log.d(TAG,"FailedToRestoreState",e);
}
}
else { // get saved state from preferences on first pass
SharedPreferences prefs = getPreferences(MODE_PRIVATE); // singleton
if (prefs != null){
Log.d(TAG,"gettingPrefs");
}
}
Log.d(TAG,"onCreate");
Generic Template for Saving State
What follows is a template for saving internal state on both a soft kill and a hard kill that ONLY saves to preferences on a hard kill and ONLY reads preferences on "the first pass." Due to the "non-deterministic" flow of code the user should test this template using a combination of user and OS actions. The general idea is to leverage the fact that IF the OS calls onSaveInstanceState it will call onSaveInstanceState BEFORE calling onStop. So, we as twisted coders should be able to set flags such that we only write to prefs on a hard kill. So given a flag:
private Boolean isSavedInstanceState= false;
We clear the flag in onResume() NOT in onCreate:
protected void onResume() {
super.onResume();
Log.d(TAG,"onResume");
isSavedInstanceState= false;
}
We set the isSavedInstanceState flag in, gee, onSavedInstanceState as:
protected void onSaveInstanceState(Bundle outState){
password= editTextPassword.getText().toString();
outState.putString("password",password);
isSavedInstanceState= true;
super.onSaveInstanceState(outState); // save view state
Log.d(TAG,"onSaveInstance");
}
We only write to prefs in onStop if isSavedInstanceState is false.
@Override
protected void onStop(){
super.onStop();
if (!isSavedInstanceState){ // this is a HARD KILL, write to prefs
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("password",password);
editor.commit();
Log.d(TAG,"savedPrefs");
}
else {
Log.d(TAG,"DidNotSavePrefs");
}
Log.d(TAG,"onStop");
}
Again, we selectively restore state in onCreate depending if we are recovering from a soft or hard kill:
if( savedInstanceState!= null){ // get saved state from bundle after a soft kill
try {
password= savedInstanceState.getString("password");
Log.d(TAG,"RestoredState!");
}
catch(Exception e){
Log.d(TAG,"FailedToRestoreState",e);
}
}
else { // get saved state from preferences on first pass after a hard kill
SharedPreferences prefs = getPreferences(MODE_PRIVATE); // singleton
if (prefs != null){
this.password= prefs.getString("password","");
Log.d(TAG,"gettingPrefs");
}
}
Log.d(TAG,"onCreate");
That's it! If we did this right, prefs will ONLY be written on a hard kill and ONLY be read "the first time." You should remove the calls to Log.d in production code. So, in review, we persist internal state to prefs in onStop and we should persist shared document-like data in onPause (code not shown).
Generic Template Using onRetainNonConfigurationState
Although it may be more efficient to save a state object in onRetainNonConfigurationState, the onRetainNonConfigurationState method is called after onStop() and before onDestroy. So we must move our code that write to prefs from onStop to onDestroy. Here is the call to save state:
@Override
public Object onRetainNonConfigurationInstance() {
password= editTextPassword.getText().toString();
try {
ConfuseTextStateBuilder b= ConfuseTextState.getBuilder();
b.setIsShowCharCount(isShowCharCount);
b.setTimeExpire(timeExpire);
b.setTimeoutType(timeoutType);
b.setIsValidKey(isValidKey);
b.setPassword(password);
state= b.build(); // may throw
isSavedInstanceState= true;
//Log.d(TAG,"onRetainNonConfigurationState");
}
catch(InvalidParameterException e){
isSavedInstanceState= false;
state= null; // throw somewhere
Log.d(TAG,"FailedToSaveState",e); // will be stripped out of runtime
}
return state;
}
So if we use onRetainNonConfigurationState to save state instead of onSaveInstanceState, we would need to write to prefs in onDestroy() instead of in onStop() as in:
@Override
protected void onDestroy() {
super.onDestroy();
if (!isSavedInstanceState){ // this is a HARD KILL, write to prefs
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("isShowCharCount",isShowCharCount);
editor.putLong("timeExpire",timeExpire);
editor.putInt("timeoutType",timeoutType);
editor.putBoolean("isValidKey",isValidKey);
editor.putString("password",password);
editor.putString("plainText", editTextPlainText.getText().toString());
editor.putString("confusedText", editTextConfusedText.getText().toString());
editor.commit();
Log.d(TAG,"wroteToPrefs");
}
}
Multiple Sets of State
To be clear. If you are launching a separate activity using an intent, there are FOUR sets of state that you are concerned with. The first set is used to save the state of the first activity on a soft kill. The second set is used to save the state of the second activity on a soft kill. The third set has to do with values being sent between the two activities. The fourth set is the data persisted in preferences on a hard kill of the main activity. I would just recommend that you set a count of values for each activity and make sure you are processing a matching count of values on the other end. For instance:
First Activity:
onSaveInstanceState put*** (8) // save state on kill
onCreate get*** (8) // get state on kill
onOptionsSelected Intent.putExtra (6) ---> data sent via intent extra to second activity
onActivityResult getExtras (3) <--- data received from second activity from setResult
SecondActivity:
onSaveInstanceState put*** (6) // save state on kill
onCreate get*** (6) // get state on kill
onCreate getIntent.get*** (6) <--- data received via intent extra by second activity
setResult getIntent.putExtra(3) ----> data sent to first activity in setResult
In a more advanced lesson, we investigate saving instance state in an immutable serializable object and passing this object using putSerializable. In the end, I created a single immutable object that encapsulates non-view state and implements serializable. I used instances of this single class to push state from the parent activity to the child activity, back from the child activity to the parent activity, and to save the child activity's state on a soft kill in onRetainNonConfigurationState. I created a second class to save state of the Parent activity on a soft kill. I used a set of name:value pairs only to save state on a hard kill by writing to preferences. Note: Android provides a class, PreferenceActivity, to save preferences automatically.
KNOWN ISSUES
If the we call setResult in a child activity and we do NOT call a.finish() immediately, we cannot be guaranteed that our result will be received in the parent activity with RESULT_OK. For instance, if the user changes the phone orientation the update will not take place. Also calling finish in onPause may not save the data when the user hits the home button. Consider calling activity.finish() immediately after the call to setResult.
Static State
Surprise. Surprise. Static variables DO retain their state on a hard kill for a while, but depending on static state may result in "unpredictable behavior wherein the variable appears to be saved sometimes, but not always. If Android decides to garbage collect my lingering process, the static goes with it."
So, in review, although it may work most of the time, "persisting" internal state in static variables or avoiding saving internal state by setting android:configChanges= "orientation|keyboardHidden" may not represent best practice.
Available State in onCreate
It gets confusing, but the following may contain state in onCreate. Some of these may be null.
When to Write to Prefs
Consider writing to prefs on a hard kill and whenever the user changes a preferences setting. Remember, the user or OS may launch a new instance or your app at any time. Writing to prefs immediately means that changes to prefs in one instance will be immediately respected by any newly launched instance of the app. You should still be able to limit reading from prefs to the initial launch.
Hope that helps,
JAL