Well this is a bit of an advance topic that falls under the "I just wanted to see if I could do it category." If you are tired of all the string index code to save and retrieve Activity state on kill, you can wrap the activity state into an immutable Serializable object likes this:
package jalcomputing.confusetext;
import java.io.Serializable;
public final class ConfuseTextState implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
// data to persist
public final boolean isShowCharCount;
public final boolean isProhibitEditPassword;
public final long timeStart;
public final long timeExpire;
public final int timeoutType;
public final boolean isTimeout;
public final boolean isValidKey;
public final String password;
public ConfuseTextState(boolean isShowCharCount,
boolean isProhibitEditPassword,
long timeStart,
long timeExpire,
int timeoutType,
boolean isTimeout,
boolean isValidKey,
String password) {
this.isShowCharCount= isShowCharCount;
this.isProhibitEditPassword= isProhibitEditPassword;
this.timeStart= timeStart;
this.timeExpire= timeExpire;
this.timeoutType= timeoutType;
this.isTimeout= isTimeout;
this.isValidKey= isValidKey;
this.password= password;
}
}
Note that this object implements Serializable, which is just a marker that tells the compiler there is no special handling for this object when reading and writing. Now we can simplify the code in onSaveInstanceState, passing a single immutabel serializable object using putSerializable as:
protected void onSaveInstanceState(Bundle outState){
password= editTextPassword.getText().toString();
state= new ConfuseTextState(
isShowCharCount,
isProhibitEditPassword,
timeStart,
timeExpire,
timeoutType,
isTimeout,
isValidKey,
password);
outState.putSerializable("jalcomputing.confusetext.ConfuseTextState", state);
super.onSaveInstanceState(outState); // save view state
}
Note the naming convention ("jalcomputing.confusetext.ConfuseTextState", state);
Given:
private ConfuseTextState state;
You can then retrieve the state in onCreate as in:
if( inState!= null){ // get saved state
try {
state= (jalcomputing.confusetext.ConfuseTextState) inState.getSerializable("jalcomputing.confusetext.ConfuseTextState");
this.isShowCharCount= state.isShowCharCount;
this.isProhibitEditPassword= state.isProhibitEditPassword;
this.timeStart= state.timeStart;
this.timeExpire= state.timeExpire;
this.timeoutType= state.timeoutType;
this.isTimeout= state.isTimeout;
this.isValidKey= state.isValidKey;
this.password= state.password;
}
catch(Exception e){
Log.d(TAG,"FailedToRestore",e);
}
}
Note the cast from Serializable to ConfuseTextState. This can result in a ClassCastException, but is unlikely with the fully qualified naming convention. The concept of sending thread safe immutable objects over the wire as a form of communication or messaging is a powerful concept, whether it is simply passing an object between activities or embedding state into messages sent between threads. It may be preferable to save state as a single object in onRetainNonConfigurationInstance instead of using onSaveInstanceState. However, it is possible to serialize more than one object if you use onSaveInstanceState.
Using Mutable Serializable Objects
If we serialize objects and send them between activities, we are actually sending a copy of the object. So.... it could be argued that using immutable serializable objects is overkill. The use of mutable serializable objects would simplify the implementation.
Adding a Mutable Helper Class, ConfuseTextStateBuilder
The difficulty in using the ConfuseTextState class is the possibility of passing the wrong parameter to the constructor. The strength of the ConfuseTextState class is that it forces you to pass all of the parameters. We can write a helper class that simplifies the setting of the state values at the cost of failing to insure that a state value is initialized. Here is our mutable ConfuseTextStateBuilder class:
package jalcomputing.confusetext;
public class ConfuseTextStateBuilder {
// data to persist
public boolean isShowCharCount;
public boolean isProhibitEditPassword;
public long timeStart;
public long timeExpire;
public int timeoutType;
public boolean isTimeout;
public boolean isValidKey;
public String password;
public ConfuseTextStateBuilder() {;}
public ConfuseTextState Build(){
return new ConfuseTextState(isShowCharCount,
isProhibitEditPassword,
timeStart,
timeExpire,
timeoutType,
isTimeout,
isValidKey,
password);
}
}
We can then use this class to encapsulate state as in:
ConfuseTextStateBuilder b= new ConfuseTextStateBuilder();
b.isShowCharCount= isShowCharCount;
b.isProhibitEditPassword= isProhibitEditPassword;
b.timeStart= timeStart;
b.timeExpire= timeExpire;
b.timeoutType= timeoutType;
b.isTimeout= isTimeout;
b.isValidKey= isValidKey;
b.password= password;
state= b.Build();
outState.putSerializable("state", state);
So we use a mutable builder class to return an immutable class with the same state. We retrieve the state in the same way as in the first sample in onCreate.
Strict ConfuseTextBuilder
Finally, it is possible to build a helper class that enforces initialization of every field using the appropriate setXXX method. Here is our strict ConfuseTextBuilder class:
package jalcomputing.confusetext;
import java.security.InvalidParameterException;
public class ConfuseTextStateBuilder {
// data to persist
private boolean isShowCharCount;
private boolean isProhibitEditPassword;
private long timeStart;
private long timeExpire;
private int timeoutType;
private boolean isTimeout;
private boolean isValidKey;
private String password;
private boolean isShowCharCountInit= false;
private boolean isProhibitEditPasswordInit= false;
private boolean timeStartInit= false;
private boolean timeExpireInit= false;
private boolean timeoutTypeInit= false;
private boolean isTimeoutInit= false;
private boolean isValidKeyInit= false;
private boolean passwordInit= false;
public void setIsShowCharCount(boolean isShowCharCount){
isShowCharCountInit= true;
this.isShowCharCount= isShowCharCount;
}
public void setIsProhibitEditPassword(boolean isProhibitEditPassword){
isProhibitEditPasswordInit= true;
this.isProhibitEditPassword=isProhibitEditPassword;
}
public void setTimeStart(long timeStart){
timeStartInit= true;
this.timeStart=timeStart;
}
public void setTimeExpire(long timeExpire){
timeExpireInit= true;
this.timeExpire=timeExpire;
}
public void setTimeoutType(int timeoutType){
timeoutTypeInit= true;
this.timeoutType=timeoutType;
}
public void setIsTimeout(boolean isTimeout){
isTimeoutInit= true;
this.isTimeout=isTimeout;
}
public void setIsValidKey(boolean isValidKey){
isValidKeyInit= true;
this.isValidKey=isValidKey;
}
public void setPassword(String password){
passwordInit= true;
this.password=password;
}
public boolean getIsInitialized() {
return(isShowCharCountInit &&
isProhibitEditPasswordInit &&
timeStartInit &&
timeExpireInit &&
timeoutTypeInit &&
isTimeoutInit &&
isValidKeyInit &&
passwordInit);
}
public ConfuseTextStateBuilder() {;}
// ASSERT GetIsInitialized is true
public ConfuseTextState build() throws InvalidParameterException {
boolean isInitialized= getIsInitialized();
if (isInitialized) {
return new ConfuseTextState(isShowCharCount,
isProhibitEditPassword,
timeStart,
timeExpire,
timeoutType,
isTimeout,
isValidKey,
password);
}
else {throw new InvalidParameterException();}
}
}
We can use it as in:
ConfuseTextStateBuilder b= new ConfuseTextStateBuilder();
b.setIsShowCharCount(isShowCharCount);
b.setIsProhibitEditPassword(isProhibitEditPassword);
b.setTimeStart(timeStart);
b.setTimeExpire(timeExpire);
b.setTimeoutType(timeoutType);
b.setIsTimeout(isTimeout);
b.setIsValidKey(isValidKey);
b.setPassword(password);
state= b.build(); // may throw if all fields not initialized
outState.putSerializable("jalcomputing.confusetext.ConfuseTextState", state);
super.onSaveInstanceState(outState); // save view state
If we fail to init every field, build() will throw an InvalidParameterException! We retrieve the data in the same way as the first example in onCreate.
Adding a Helper Factory Method
Since we are experimenting here. I modified the code so that ConfuseTextState returns a strict builder as in:
ConfuseTextStateBuilder b= ConfuseTextState.getBuilder();
So for the state class:
package jalcomputing.confusetext;
import java.io.Serializable;
public final class ConfuseTextState implements Serializable {
/**
* ConfuseTextState
* Encapsulate non view state of class ConfuseState
* Save this object on soft kill
* Copyright JALComputing 2011
*/
private static final long serialVersionUID = 1L;
public static ConfuseTextStateBuilder getBuilder() {
return new ConfuseTextStateBuilder(true); // strict builder
}
// data to persist
public final boolean isShowCharCount;
public final long timeExpire;
public final int timeoutType;
public final boolean isValidKey;
public final String password;
public ConfuseTextState(boolean isShowCharCount,
long timeExpire,
int timeoutType,
boolean isValidKey,
String password) {
this.isShowCharCount= isShowCharCount;
this.timeExpire= timeExpire;
this.timeoutType= timeoutType;
this.isValidKey= isValidKey;
this.password= password;
}
}
We can have the builder class:
package jalcomputing.confusetext;
import java.security.InvalidParameterException;
/*
* ConfuseTextStateBuilder
* Builder of Immutable ConfuseTextState object
* may THROW if all fields are not initialized on call to build
* Does NOT do any error checking on initialization values
* JALComputing Copyright 2011
*/
public class ConfuseTextStateBuilder {
// data to persist
private boolean isShowCharCount;
private long timeExpire;
private int timeoutType;
private boolean isValidKey;
private String password;
private boolean isStrict;
// flags false= clean true= dirty, was initialized
private boolean isShowCharCountInit= false;
private boolean timeExpireInit= false;
private boolean timeoutTypeInit= false;
private boolean isValidKeyInit= false;
private boolean passwordInit= false;
public void setIsShowCharCount(boolean isShowCharCount){
isShowCharCountInit= true;
this.isShowCharCount= isShowCharCount;
}
public void setTimeExpire(long timeExpire){
timeExpireInit= true;
this.timeExpire=timeExpire;
}
public void setTimeoutType(int timeoutType){
timeoutTypeInit= true;
this.timeoutType=timeoutType;
}
public void setIsValidKey(boolean isValidKey){
isValidKeyInit= true;
this.isValidKey=isValidKey;
}
public void setPassword(String password){
passwordInit= true;
this.password=password;
}
// return true if all fields are dirty (initialized)
public boolean getIsInitialized() {
return(isShowCharCountInit &&
timeExpireInit &&
timeoutTypeInit &&
isValidKeyInit &&
passwordInit);
}
public ConfuseTextStateBuilder(boolean isStrict) {
this.isStrict= isStrict;
}
// ASSERT if isStrict, isInitialized must be true
public ConfuseTextState build() throws InvalidParameterException {
boolean isInitialized= getIsInitialized();
if (!isStrict || isInitialized) {
return new ConfuseTextState(isShowCharCount,
timeExpire,
timeoutType,
isValidKey,
password);
}
else {throw new InvalidParameterException();}
}
}
I am getting a headache now.
Persisting Large Objects
It is not efficient to persist large objects through serialization. If you need to save a large object such as a bitmap see onRetainNonConfigurationInstance() and getLastNonConfigurationInstance. In fact, it is not that hard to save our data object using this scheme. It is also not recommended to save any context related data in static variables such as a bitmap, else you can leak memory.
Using onRetainNonConfigurationInstance()
If we want to avoid the overhead of serializing and de-serializing our state object, we can save the state object in onRetainNonConfiguationInstance() rather than in onSaveInstanceState as in:
@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,"onRetainNonConfigurationInstance");
}
catch(InvalidParameterException e){
isSavedInstanceState= false;
state= null; // throw somewhere
Log.d(TAG,"FailedToSaveState",e); // will be stripped out of runtime
}
return state;
}
We can easily modify our onCreate code to restore state as in:
state= (ConfuseTextState)getLastNonConfigurationInstance();
if (state != null) { // get state from onRetainNonConfigurationInstance
try {
this.isShowCharCount= state.isShowCharCount;
this.timeExpire= state.timeExpire;
this.timeoutType= state.timeoutType;
this.isValidKey= state.isValidKey;
this.password= state.password;
}
catch(Exception e){
Log.d(TAG,"FailedToRestoreState",e);
}
}
else { // get saved state from preferences on first pass if they exist
// Restore preferences (7) on hard kill when USER hit back and killed us WITHOUT calling onRetainNonConfigurationInstance
SharedPreferences prefs = getPreferences(MODE_PRIVATE); // singleton
if (prefs != null){
this.isShowCharCount= prefs.getBoolean("isShowCharCount",false);
this.timeExpire= prefs.getLong("timeExpire",Calendar.getInstance().getTimeInMillis()+LONG_YEAR_MILLIS);
this.timeoutType= prefs.getInt("timeoutType",TIMEOUT_NEVER);
this.isValidKey= prefs.getBoolean("isValidKey",false);
this.password= prefs.getString("password","");
this.editTextPlainText.setText(prefs.getString("plainText", ""));
this.editTextConfusedText.setText(prefs.getString("confusedText", ""));
}
}
This may also be useful for saving objects that are not serializable throughout their object tree.
Note that onRetainNonConfigurationInstance() is called AFTER onStop() and before onDestory(), any code to save state to a prefs file on a hard kill, might need to be moved from onStop() to onDestroy(). See generic template for saving state.
Using Interfaces
It is possible to use interfaces to abstract behavior between intents.
Given an interface:
public interface ISayHello {
public void SayHello();
}
A concrete class SayHello
public class SayHello implements ISayHello, Serializable {
public void SayHello() {
Log.d(Main.TAG,"hello");
}
}
One can pass the fully qualified name of the interface in an intent as:
Bundle b = new Bundle();
SayHello say= new SayHello();
b.putSerializable("qprinstitute.crisis.ISayHello", say);
intent.putExtras(b);
startActivity(intent);
And cast the instance of the concrete class to the interface in the receiving activity as:
Intent i= getIntent();
if (i != null) {inBundle= i.getExtras();}
if (inBundle != null) {
ISayHello say= (ISayHello)inBundle.getSerializable("qprinstitute.crisis.ISayHello");
say.SayHello();
}
Which logs "hello". Cool!
JAL