Save State for an Android Compound Component

I’ve been working on a simple application in Android. One of the things I’ve been working on is a custom component. This component allows the user to select a character by spinning a wheel-like component.Text Spinner
To achieve this, I build a compound component, with a simple structure: MyComponent (extends ScrollView) contains a LinearLayout, which in turn contains one TextView for each character. It’s features are very basic:
- snap, so the selected character always appears in the same spot;
- spin-through, so when you spin down past ‘Z’, ‘A’ reappears (and back);
- customised set, so you can choose to select from A-Z, or 0-9, or …

However, when the screen switches orientation, the component is destroyed and recreated, and normally the active scroll position (i.e. the selected character) would be lost. To fix this, Android fires two events, enabling a component to save its state: onSaveInstanceState and onRestoreInstanceState. The required implementation is not very well documented, but not hard either. For this component:

public class CharacterPicker extends ScrollView {
...
  public static class SavedState extends BaseSavedState {
    String allowedCharacters;
    String value;

    SavedState(Parcelable superState) {
      super(superState);
    }

    SavedState(Parcel in) {
      super(in);
      allowedCharacters = in.readString();
      value = in.readString();
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeString(allowedCharacters);
      out.writeString(value);
    }

    public static final Parcelable.Creator CREATOR
        = new Parcelable.Creator() {
      public SavedState createFromParcel(Parcel in) {
        return new SavedState(in);
      }

      public SavedState[] newArray(int size) {
        return new SavedState[size];
      }
    };
  }

  @Override
  protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.allowedCharacters = allowedChars;
    ss.value = getValue();
    return ss;
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    setAllowedChars(ss.allowedCharacters);
    setValue(ss.value);
  }
}

However, this didn’t work as well as expected. Though the code above is correct (afaik), an error occurred:

java.lang.ClassCastException: android.view.AbsSavedState$1
  at ...CharacterPicker.onRestoreInstanceState(...)
  ...

This exception was caused, as turned out after lots and lots of debugging, by rebuilding the TextView-children in my setAllowedChars() method. Combined with the TextView objects trying to save their own state it caused the error above.
The solution was to make sure this doesn’t happen immediately, but instead to post the request, as such:

	public synchronized void setAllowedChars(String allowedChars) {
		if (allowedChars.equals(this.allowedChars))
			return;
		this.allowedChars = allowedChars;
		handler.post(new Runnable() {
			@Override
			public void run() {
				updateView();
			}
		});
	}

I also added a call to setSaveEnabled(false) on each child component, but this seems to be neither necessary nor sufficient to solve the problem above.

As with many things, once you know it all makes sense, but it took me a while to find out.

Tags: , ,
Posted in development | 1 Comment »

One Comment

  1. Nick Dunn zegt:

    Excellent information – saved me a lot of time trying to figure out how to do this. Just to be explicit, you need to ensure that your View returns true in the isSaveEnabled() method (which you can change in xml via the android:saveEnabled=”true” attribute)

Leave a Reply