270

I've successfully implemented onRetainNonConfigurationInstance() for my main Activity to save and restore certain critical components across screen orientation changes.

But it seems, my custom views are being re-created from scratch when the orientation changes. This makes sense, although in my case it's inconvenient because the custom view in question is an X/Y plot and the plotted points are stored in the custom view.

Is there a crafty way to implement something similar to onRetainNonConfigurationInstance() for a custom view, or do I need to just implement methods in the custom view which allow me to get and set its "state"?

JJD
52.6k63 gold badges217 silver badges350 bronze badges
asked Aug 22, 2010 at 16:45

10 Answers 10

484

I think this is a much simpler version. Bundle is a built-in type which implements Parcelable

public class CustomView extends View
{
 private int stuff; // stuff
 @Override
 public Parcelable onSaveInstanceState()
 {
 Bundle bundle = new Bundle();
 bundle.putParcelable("superState", super.onSaveInstanceState());
 bundle.putInt("stuff", this.stuff); // ... save stuff 
 return bundle;
 }
 @Override
 public void onRestoreInstanceState(Parcelable state)
 {
 if (state instanceof Bundle) // implicit null check
 {
 Bundle bundle = (Bundle) state;
 this.stuff = bundle.getInt("stuff"); // ... load stuff
 state = bundle.getParcelable("superState");
 }
 super.onRestoreInstanceState(state);
 }
}
answered Nov 14, 2011 at 20:41
Sign up to request clarification or add additional context in comments.

18 Comments

Why wouldn't onRestoreInstanceState be called with a Bundle if onSaveInstanceState returned a Bundle?
OnRestoreInstance is inherited. We can't change the header. Parcelable is just an interface, Bundle is an implementation for that.
Thanks this way is much better and avoids BadParcelableException when using the SavedState framework for custom views since the saved state seems to be unable to set the class loader correctly for your custom SavedState!
I have several instances of the same view in an activity. They all have unique id's in the xml. But still all of them gets the settings of the last view. Any ideas?
This solution might be ok, but it is definitely not safe. By implementing this you're assuming that base View state is not a Bundle. Of course, that is true at the moment, but you are relying on this current implementation fact that is not guaranteed to be true.
|
425

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

Daniel Lubarov
7,9341 gold badge39 silver badges57 bronze badges
answered Aug 22, 2010 at 19:02

16 Comments

For those arriving here because this isn't working when using Fragments with the v4 support library, I note that the support library doesn't seem to call the View's onSaveInstanceState/onRestoreInstanceState for you; you have to explicitly call it yourself from a convenient place in the FragmentActivity or Fragment.
Note that the CustomView you apply this to should have a unique id set, otherwise they will share state with each other. SavedState is stored against the CustomView's id, so if you have multiple CustomViews with the same id, or no id, then the parcel saved in the final CustomView.onSaveInstanceState() will be passed into all the calls to CustomView.onRestoreInstanceState() when the views are restored.
This method didn't work for me with two custom views (one extending the other). I kept getting a ClassNotFoundException when restoring my view. I had to use the Bundle approach in Kobor42's answer.
onSaveInstanceState() and onRestoreInstanceState() should be protected (like their superclass), not public. No reason to expose them...
This doesn't work well when saving a custom BaseSaveState for a class that extends RecyclerView, you get Parcel: Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedState so you need to do the bug fix that's written down here: github.com/ksoichiro/Android-ObservableScrollView/commit/… (using the ClassLoader of RecyclerView.class to load the super state)
|
29

Easy with kotlin

@Parcelize
class MyState(val superSavedState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSavedState), 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 ?: state)
 loading = myState?.loading ?: false
 //redraw
 }
}
answered Jun 29, 2020 at 9:39

3 Comments

What do you gain by extending View.BaseSavedState in this case? Does it make any difference?
You can check the source code of the View.BaseSavedState class where it stores some common view state properties including the activity that the view is in (for text processing in the text selection toolbar according to the Git blame), and some autofill states/options
also interested in this. i would suggest you try with and without
18

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;
 }
}
answered May 11, 2012 at 14:27

1 Comment

This doesn't work. I get a ClassCastException... And that's because it needs a public static CREATOR so that it instantiates your State from the parcel. Please take a look at: charlesharley.com/2012/programming/…
11

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);
 } 
}
mmBs
8,5796 gold badges41 silver badges47 bronze badges
answered Sep 27, 2015 at 5:41

3 Comments

Custom id is really an issue, but I think it should be handled at the initialization of the view, and not at state save.
Good point. Do you suggest setting mViewIds in the constructor then overwrite if state is restored?
This is the most complete answer that is valid for inflatable custom layouts. Unfortunately the answer generates ids only for the direct children but usually that's not the case. E.g. the children could be TextInputEditText inside TextInputLayout. In this case the solution would be much more complex.
5

I had the problem that onRestoreInstanceState restored all my custom views with the state of the last view. I solved it by adding these two methods to my custom view:

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
 dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
 dispatchThawSelfOnly(container);
}
answered Nov 19, 2018 at 22:07

1 Comment

The dispatchFreezeSelfOnly and dispatchThawSelfOnly methods belong to ViewGroup, not View. So in case, your custom View is extended from a build-in View. Your solution is not applicable.
2

To augment other answers - if you have multiple custom compound views with the same ID and they are all being restored with the state of the last view on a configuration change, all you need to do is tell the view to only dispatch save/restore events to itself by overriding a couple of methods.

class MyCompoundView : ViewGroup {
 ...
 override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
 dispatchFreezeSelfOnly(container)
 }
 override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
 dispatchThawSelfOnly(container)
 }
}

For an explanation of what is happening and why this works, see this blog post. Basically your compound view's children's view IDs are shared by each compound view and state restoration gets confused. By only dispatching state for the compound view itself, we prevent their children from getting mixed messages from other compound views.

answered Aug 29, 2018 at 1:02

Comments

2

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 LinearLayout {
 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];
 }
 };
 }
}
answered Dec 12, 2019 at 7:02

2 Comments

Did you try the code yourself? The method dispatchFreezeSelfOnly and dispatchFreezeSelfOnly are belong to ViewGroup, not View.
@zeleven You're right, I used code from another answer and didn't edit it correctly. You should just extend LinearLayout or ViewGroup instead of View and it will be fine. I do use a slightly altered version of this solution in my app and it works fine. I Will try to edit my answer.
1

Based on @Fletcher Johns answer I came up with:

  • custom layout
  • can inflate from XML
  • is able to save/restore direct and indirect children. I improved @Fletcher Johns' answer to save the ids in String->Id map instead of IntArray.
  • the only small drawback is that you must declare your saveable child views beforehand.

open class AddressView @JvmOverloads constructor(
 context: Context,
 attrs: AttributeSet? = null,
 defStyleAttr: Int = 0,
 defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
 protected lateinit var countryInputLayout: TextInputLayout
 protected lateinit var countryAutoCompleteTextView: CountryAutoCompleteTextView
 protected lateinit var cityInputLayout: TextInputLayout
 protected lateinit var cityEditText: CityEditText
 protected lateinit var postCodeInputLayout: TextInputLayout
 protected lateinit var postCodeEditText: PostCodeEditText
 protected lateinit var streetInputLayout: TextInputLayout
 protected lateinit var streetEditText: StreetEditText
 
 init {
 initView()
 }
 private fun initView() {
 val view = inflate(context, R.layout.view_address, this)
 orientation = VERTICAL
 countryInputLayout = view.findViewById(R.id.countryInputLayout)
 countryAutoCompleteTextView = view.findViewById(R.id.countryAutoCompleteTextView)
 streetInputLayout = view.findViewById(R.id.streetInputLayout)
 streetEditText = view.findViewById(R.id.streetEditText)
 cityInputLayout = view.findViewById(R.id.cityInputLayout)
 cityEditText = view.findViewById(R.id.cityEditText)
 postCodeInputLayout = view.findViewById(R.id.postCodeInputLayout)
 postCodeEditText = view.findViewById(R.id.postCodeEditText)
 }
 // Declare your direct and indirect child views that need to be saved
 private val childrenToSave get() = mapOf<String, View>(
 "coutryIL" to countryInputLayout,
 "countryACTV" to countryAutoCompleteTextView,
 "streetIL" to streetInputLayout,
 "streetET" to streetEditText,
 "cityIL" to cityInputLayout,
 "cityET" to cityEditText,
 "postCodeIL" to postCodeInputLayout,
 "postCodeET" to postCodeEditText,
 )
 private var viewIds: HashMap<String, Int>? = null
 override fun onSaveInstanceState(): Parcelable? {
 // Create a bundle to put super parcelable in
 val bundle = Bundle()
 bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState())
 // Store viewIds in the bundle - initialize if necessary.
 if (viewIds == null) {
 childrenToSave.values.forEach { view -> view.id = generateViewId() }
 viewIds = HashMap<String, Int>(childrenToSave.mapValues { (key, view) -> view.id })
 }
 bundle.putSerializable(STATE_VIEW_IDS, viewIds)
 return bundle
 }
 override fun onRestoreInstanceState(state: Parcelable?) {
 // We know state is a Bundle:
 val bundle = state as Bundle
 // Get mViewIds out of the bundle
 viewIds = bundle.getSerializable(STATE_VIEW_IDS) as HashMap<String, Int>
 // For each id, assign to the view of same index
 if (viewIds != null) {
 viewIds!!.forEach { (key, id) -> childrenToSave[key]!!.id = id }
 }
 super.onRestoreInstanceState(bundle.getParcelable(SUPER_INSTANCE_STATE))
 }
 companion object {
 private const val SUPER_INSTANCE_STATE = "saved_instance_state_parcelable"
 private const val STATE_VIEW_IDS = "state_view_ids"
 }
}
answered Jan 15, 2021 at 20:22

Comments

0

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:

answered Jan 2, 2019 at 14:06

2 Comments

@JJD I agree with the article you posted, one still has to handle save and restore properly. ViewModel is especially handy if you have large data sets to retain during a state change, such as a screen rotation. I prefer using the ViewModel instead of writing it into Application because it's clearly scoped, and I can have multiple Activities of the same Application behaving correctly.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.