I\'ve successfully implemented onRetainNonConfigurationInstance() for my main Activity
to save and restore certain critical components across screen orientation
Here is another variant that uses a mix of the two above methods.
Combining the speed and correctness of Parcelable
with the simplicity of a Bundle
:
@Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// The vars you want to save - in this instance a string and a boolean
String someString = "something";
boolean someBoolean = true;
State state = new State(super.onSaveInstanceState(), someString, someBoolean);
bundle.putParcelable(State.STATE, state);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
State customViewState = (State) bundle.getParcelable(State.STATE);
// The vars you saved - do whatever you want with them
String someString = customViewState.getText();
boolean someBoolean = customViewState.isSomethingShowing());
super.onRestoreInstanceState(customViewState.getSuperState());
return;
}
// Stops a bug with the wrong state being passed to the super
super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE);
}
protected static class State extends BaseSavedState {
protected static final String STATE = "YourCustomView.STATE";
private final String someText;
private final boolean somethingShowing;
public State(Parcelable superState, String someText, boolean somethingShowing) {
super(superState);
this.someText = someText;
this.somethingShowing = somethingShowing;
}
public String getText(){
return this.someText;
}
public boolean isSomethingShowing(){
return this.somethingShowing;
}
}
The answers here already are great, but don't necessarily work for custom ViewGroups. To get all custom Views to retain their state, you must override onSaveInstanceState()
and onRestoreInstanceState(Parcelable state)
in each class.
You also need to ensure they all have unique ids, whether they're inflated from xml or added programmatically.
What I came up with was remarkably like Kobor42's answer, but the error remained because I was adding the Views to a custom ViewGroup programmatically and not assigning unique ids.
The link shared by mato will work, but it means none of the individual Views manage their own state - the entire state is saved in the ViewGroup methods.
The problem is that when multiple of these ViewGroups are added to a layout, the ids of their elements from the xml are no longer unique (if its defined in xml). At runtime, you can call the static method View.generateViewId()
to get a unique id for a View. This is only available from API 17.
Here is my code from the ViewGroup (it is abstract, and mOriginalValue is a type variable):
public abstract class DetailRow<E> extends LinearLayout {
private static final String SUPER_INSTANCE_STATE = "saved_instance_state_parcelable";
private static final String STATE_VIEW_IDS = "state_view_ids";
private static final String STATE_ORIGINAL_VALUE = "state_original_value";
private E mOriginalValue;
private int[] mViewIds;
// ...
@Override
protected Parcelable onSaveInstanceState() {
// Create a bundle to put super parcelable in
Bundle bundle = new Bundle();
bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState());
// Use abstract method to put mOriginalValue in the bundle;
putValueInTheBundle(mOriginalValue, bundle, STATE_ORIGINAL_VALUE);
// Store mViewIds in the bundle - initialize if necessary.
if (mViewIds == null) {
// We need as many ids as child views
mViewIds = new int[getChildCount()];
for (int i = 0; i < mViewIds.length; i++) {
// generate a unique id for each view
mViewIds[i] = View.generateViewId();
// assign the id to the view at the same index
getChildAt(i).setId(mViewIds[i]);
}
}
bundle.putIntArray(STATE_VIEW_IDS, mViewIds);
// return the bundle
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
// We know state is a Bundle:
Bundle bundle = (Bundle) state;
// Get mViewIds out of the bundle
mViewIds = bundle.getIntArray(STATE_VIEW_IDS);
// For each id, assign to the view of same index
if (mViewIds != null) {
for (int i = 0; i < mViewIds.length; i++) {
getChildAt(i).setId(mViewIds[i]);
}
}
// Get mOriginalValue out of the bundle
mOriginalValue = getValueBackOutOfTheBundle(bundle, STATE_ORIGINAL_VALUE);
// get super parcelable back out of the bundle and pass it to
// super.onRestoreInstanceState(Parcelable)
state = bundle.getParcelable(SUPER_INSTANCE_STATE);
super.onRestoreInstanceState(state);
}
}
You do this by implementing View#onSaveInstanceState and View#onRestoreInstanceState and extending the View.BaseSavedState class.
public class CustomView extends View {
private int stateToSave;
...
@Override
public Parcelable onSaveInstanceState() {
//begin boilerplate code that allows parent classes to save state
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
//end
ss.stateToSave = this.stateToSave;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
//begin boilerplate code so parent classes can restore state
if(!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
//end
this.stateToSave = ss.stateToSave;
}
static class SavedState extends BaseSavedState {
int stateToSave;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}
//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
The work is split between the View and the View's SavedState class. You should do all the work of reading and writing to and from the Parcel
in the SavedState
class. Then your View class can do the work of extracting the state members and doing the work necessary to get the class back to a valid state.
Notes: View#onSavedInstanceState
and View#onRestoreInstanceState
are called automatically for you if View#getId
returns a value >= 0. This happens when you give it an id in xml or call setId
manually. Otherwise you have to call View#onSaveInstanceState
and write the Parcelable returned to the parcel you get in Activity#onSaveInstanceState
to save the state and subsequently read it and pass it to View#onRestoreInstanceState
from Activity#onRestoreInstanceState
.
Another simple example of this is the CompoundButton
Instead of using onSaveInstanceState
and onRestoreInstanceState
, you can also use a ViewModel. Make your data model extend ViewModel
, and then you can use ViewModelProviders
to get the same instance of your model every time the Activity is recreated:
class MyData extends ViewModel {
// have all your properties with getters and setters here
}
public class MyActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// the first time, ViewModelProvider will create a new MyData
// object. When the Activity is recreated (e.g. because the screen
// is rotated), ViewModelProvider will give you the initial MyData
// object back, without creating a new one, so all your property
// values are retained from the previous view.
myData = ViewModelProviders.of(this).get(MyData.class);
...
}
}
To use ViewModelProviders
, add the following to dependencies
in app/build.gradle
:
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
Note that your MyActivity
extends FragmentActivity
instead of just extending Activity
.
You can read more about ViewModels here:
I found that this answer was causing some crashes on Android versions 9 and 10. I think it's a good approach but when I was looking at some Android code I found out it was missing a constructor. The answer is quite old so at the time there probably was no need for it. When I added the missing constructor and called it from the creator the crash was fixed.
So here is the edited code:
public class CustomView extends View {
private int stateToSave;
...
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
// your custom state
ss.stateToSave = this.stateToSave;
return ss;
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
{
dispatchFreezeSelfOnly(container);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// your custom state
this.stateToSave = ss.stateToSave;
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container)
{
dispatchThawSelfOnly(container);
}
static class SavedState extends BaseSavedState {
int stateToSave;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}
// This was the missing constructor
@RequiresApi(Build.VERSION_CODES.N)
SavedState(Parcel in, ClassLoader loader)
{
super(in, loader);
this.stateToSave = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
// This was also missing
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader)
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new SavedState(in, loader) : new SavedState(in);
}
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
Easy with kotlin
@Parcelize
class MyState(val superSaveState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSaveState), Parcelable
class MyView : View {
var loading: Boolean = false
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return MyState(superState, loading)
}
override fun onRestoreInstanceState(state: Parcelable?) {
val myState = state as? MyState
super.onRestoreInstanceState(myState?.superSaveState)
loading = myState?.loading ?: false
//redraw
}
}