Search code examples
androidandroid-layoutandroid-widgetandroid-custom-view

Save state on screen rotate when a compound component is used multiple times


I'm facing a problem where I know the root cause but don't see a way to fix it. If a custom compound component is used multiple times in an activity, the values saved from views will overwrite each other. To explain it easier I made the following example.

The xml for the new component, only an EditText to make it shorter.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >

    <EditText
        android:id="@+id/custom_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="number" >
    </EditText>

</merge>

The class implementing the new behavior, only inflating the layout.

public class CustomView extends LinearLayout {
    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.custom_view, this, true);
    }
}

And a layout using 2 of them.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <test.customview.CustomView
        android:id="@+id/customView1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >
    </test.customview.CustomView>

    <test.customview.CustomView
        android:id="@+id/customView2"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >
    </test.customview.CustomView>

</LinearLayout>

When the screen is rotated, the value from second View is also restored in the first one.

Digging into the framework's code I found out that Parcelable objects returned from onSaveInstanceState defined in View class are put in a SparseArray with the key object's id. Because I'm including CustomView multiple times the EditText with id "custom_text" is also getting added multiple times. Having the same id, values saved will overwrite each other.

I'm looking for any suggestion on how this should be actually implemented. Right now, I don't see any way to change those identifiers.


Solution

  • Seems like I have some solution with this problem. I try to find it for some time.

    1.First you must create inner class which extends BaseSavedState, inside your CustomView.

    CustomView{
         String value;  //some text value from edittext 
    
         EditText edittext;
          ...
    
         private static class Save extends BaseSavedState{
    
        String savedValue;
    
        public Save(Parcel incoming) {
            super(incoming);
            savedValue = incoming.readString();
            Log.i("Save", "Parcel");
        }
    
        public Save(Parcelable parcelable) {
            super(parcelable);
            Log.i("Save", "Parcelable");
        }
    
        @Override
        public void writeToParcel(Parcel outcoming, int flags) {
            super.writeToParcel(outcoming, flags);
            outcoming.writeString(savedValue );
            Log.i("Save", "writeToParcel");
        }
    
        public static final Parcelable.Creator<Save> CREATOR =
                     new Creator<CustomView.Save>() {
    
            @Override
            public Save[] newArray(int size) {
                Log.i("Parcelable.Creator<Save>", "newArray");
                return new Save[size];
            }
    
            @Override
            public Save createFromParcel(Parcel incoming) {
                Log.i("Parcelable.Creator<Save>", "createFromParcel");
                return new Save(incoming);
            }
        };
    }
    
    }
    

    2.then override this two methods in CustomView

    CustomView{
         String value;  //some text value from edittext 
    
         EditText edittext;
    
         ...
    
        @Override
    protected Parcelable onSaveInstanceState() {
        Log.i("CustomView", "onSaveInstanceState");
    
        Parcelable p = super.onSaveInstanceState();
    
        Save save = new Save(p);
        save.savedValue = value; // value is from CustomView class
    
        return save;
    }
    
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        Log.i("CustomView", "onRestoreInstanceState");
        if(!(state instanceof Save)){
            super.onRestoreInstanceState(state);
            return;
        }
    
        Save save = (Save) state;
        value = save.savedValue;
        //setting in this place value to edittext will not do anything.
                //instead, you have to do this in step 3
        super.onRestoreInstanceState(save.getSuperState());
    }
    
      ...
    }
    

    3.override onAttachedToWindow() and set to edittext "value".

    CustomView{
         String value;  //some text value from edittext 
    
         EditText edittext;
         ...
         @Override
         protected void onAttachedToWindow() {
               edittext.setText(value);
               super.onAttachedToWindow();
         }
         ...
    }
    

    and now you can have multiple instances of your Custom View 's that are resistant to change orientation - they will have the correct values.I have not tested this solution in 100% but it seems to be good.