Android CheckBox -- Restoring State After Screen Rotation
Asked Answered
M

8

30

I have come across some very unexpected (and incredibly frustrating) functionality while trying to restore the state of a list of CheckBoxes after a screen rotation. I figured I first would try to give a textual explanation without the code, in case someone is able to determine a solution without all the gory details. If anyone needs more details I can post the code.

I have a scrolling list of complex Views that contain CheckBoxes. I have been unsuccessful in restoring the state of these check boxes after a screen rotation. I have implemented onSaveInstanceState and have successfully transferred the list of selected check boxes to the onCreate method. This is handled by passing a long[] of database ids to the Bundle.

In onCreate() I check the Bundle for the array of ids. If the array is there I use it to determine which check boxes to check when the list is being built. I have created a number of test methods and have confirmed that the check boxes are being set correctly, based on the id array. As a last check I am checking the states of all check boxes at the very end of onCreate(). Everything looks good... unless I rotate the screen.

When I rotate the screen, one of two things happens: 1) If any number of the check boxes are selected, except for the last one, all check boxes are off after a rotation. 2) If the last check box is checked before rotation, then all check boxes are checked after rotation.

Like I said, I check the state of the boxes at the very end of my onCreate(). The thing is, the state of the boxes at the end of onCreate is correct based on what I selected before the rotation. However, the state of the boxes on the screen does not reflect this.

In addition, I have implemented each check box's setOnCheckChangedListener() and I have confirmed that my check boxes' state's are being altered after my onCreate method returns.

Anyone have an idea of what is going on? Why would the state of my check boxes change after my onCreate method returns?

Thanks in advance for your help. I have been trying to degub this for a couple days now. After I found that my check boxes were apparently changing somewhere outside my own code I figured it was time to ask around.

Morn answered 24/3, 2010 at 22:52 Comment(1)
I believe onResume is called after onCreate when you do a orientation change. Is anything going on in onResume?Fulford
A
56

I had similar problem. App had several checkboxes on screen.
After rotating phone app was 'manually' setting value for all checkboxes.
This code was executed in onStart().
But on screen all checkboxes were set with value of 'last checkbox' on screen.
Android was calling onRestoreInstanceState(..) and somehow was treating all checkboxes as 'one' [last from screen].

Solution were to disable 'restoring instance state':

<RadioButton
    ...
    android:saveEnabled="false" />
Alienate answered 24/1, 2013 at 11:15 Comment(5)
This is exactly the problem. From all these other comments it's obvious that this feature is not well understood by most android developers.Elegist
This along with storing the status of the button within onSavedInstanceState() helped solve my issueUnguentum
Only way that solved my problem. Since the state is saved, after a screen rotation the switch delegate gets called after the system automatically sets it to true. I am working with Xamarin, in C# you have to remove the delegate before making changes to switches, checkboxes so they are not executed over manual setting; after the manual setting we have to restore back the delegate. As the system doesn't do that, the delegates were being triggered. Well spotted Grzegorz Dev. Thanks!Urina
Wow, what a gotcha! I was inflating multiple instances of a view with a checkbox with an ID set in XML in a DialogFragment and they all kept getting checked on orientation change. I believe the identical ID's were the culprit - all checkboxes got the value of the last added checkbox. Using this answer solved the problem.Search
I love these types of problems. Ones where you spend hours banging your head, only to come across something like this. Thank you, sir.Nutriment
M
8

everyone. Looks like I figured this out. The state of the check boxes is being altered in onRestoreInstanceState(Bundle). This method is called after onCreate() (more precisely, after onStart()), and is another place Android recommends restoring state.

Now, I have no idea why my check boxes are being altered within onRestoreInstanceState, but at least I know that is where the problem is occurring. Surprisingly, when I override onRestoreInstanceState and do absolutely nothing (no call to super.onRestoreInstanceState) the entire Activity works perfectly.

If anyone can tell me why the check boxes' selected state is being changed in this method I would very much like to know. In my opinion this looks like a bug within the Android code itself.

Morn answered 3/4, 2010 at 20:22 Comment(2)
The state of the checkboxes is being altered (retained) because they have the property saveEnabled set to true, the default. See view docs. Even three years after this question was initially asked, this Android feature continues to surprise developers.Elegist
Thanks for the tip. I had a similar issue recently while writing a custom MVVM pattern... in any regards, since MVVM was managing the view state, I had to set myView.setSaveEnabled(false).Regorge
C
2

If you ever find anything else more about this problem, please let me know. I faced essentially this exact same problem, and only overriding onRestoreInstanceState() worked. Very odd.

Cirrostratus answered 10/6, 2010 at 16:38 Comment(0)
M
1

Rpond, I have not overriden onResume, so I don't think that is the issue. Here is the Activity and associated layouts for everyone to see. In the code you'll see many Log.d statements (there were even more at one point.) With these Log statements I was able to verify that the code works exactly as I expect it to. Also, notice the onCheckChangedListener I add to each CheckBox. All it does is print a Log statement telling me the state of one of my CheckBoxes changed. It was through this that I was able to determine the state of the CheckBoxes were being altered after my onCreate returns. You'll see how I call examineCheckboxes() at the end of my onCreate. The Log statements produced from this are not what is shown on my screen after a rotation, and I can see the state of my boxes being altered afterwards (because of the onCheckChangedListener.)

SelectItems.java:

public class SelectItems extends Activity {

public static final int SUCCESS = 95485839;

public static final String ITEM_LIST = "item list";
public static final String ITEM_NAME = "item name";

// Save state constants
private final String SAVE_SELECTED = "save selected";

private DbHelper db;

private ArrayList<Long> selectedIDs;

ArrayList<CheckBox> cboxes = new ArrayList<CheckBox>();

@Override
public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.select_items);

    // Create database helper
    db = new DbHelper(this);
    db.open();

    initViews(savedInstanceState);

    examineCheckboxes();

}

private void initViews(Bundle savedState) {
    initButtons();

    initHeading();

    initList(savedState);
}

private void initButtons() {

    // Setup event for done button
    Button doneButton = (Button) findViewById(R.id.done_button);
    doneButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            done();
        }
    });

    // Setup event for cancel button
    Button cancelButton = (Button) findViewById(R.id.cancel_button);
    cancelButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            cancel();
        }
    });
}

private void initHeading() {

    String itemName = getIntent().getExtras().getString(ITEM_NAME);

    TextView headingField = (TextView) findViewById(R.id.heading_field);

    if (itemName.equals("")) {
        headingField.setText("No item name!");
    } else {
        headingField.setText("Item name: " + itemName);
    }
}

private void initList(Bundle savedState) {

    // Init selected id list
    if (savedState != null && savedState.containsKey(SAVE_SELECTED)) {
        long[] array = savedState.getLongArray(SAVE_SELECTED);
        selectedIDs = new ArrayList<Long>();

        for (long id : array) {
            selectedIDs.add(id);
        }

        Log.d("initList", "restoring from saved state");
        logIDList();
    } else {
        selectedIDs = (ArrayList<Long>) getIntent().getExtras().get(
                ITEM_LIST);

        Log.d("initList", "using database values");
        logIDList();
    }

    // Begin building item list
    LayoutInflater li = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    LinearLayout scrollContent = (LinearLayout) findViewById(R.id.scroll_content);

    Cursor cursor = db.getAllItems();
    startManagingCursor(cursor);
    cursor.moveToFirst();

    // For each Item entry, create a list element
    while (!cursor.isAfterLast()) {

        View view = li.inflate(R.layout.item_element, null);
        TextView name = (TextView) view.findViewById(R.id.item_name);
        TextView id = (TextView) view.findViewById(R.id.item_id);
        CheckBox cbox = (CheckBox) view.findViewById(R.id.checkbox);

        name.setText(cursor.getString(cursor
                .getColumnIndexOrThrow(DbHelper.COL_ITEM_NAME)));

        final long itemID = cursor.getLong(cursor
                .getColumnIndexOrThrow(DbHelper.COL_ID));
        id.setText(String.valueOf(itemID));

        // Set check box states based on selectedIDs array
        if (selectedIDs.contains(itemID)) {
            Log.d("set check state", "setting check to true for " + itemID);
            cbox.setChecked(true);
        } else {
            Log.d("set check state", "setting check to false for " + itemID);
            cbox.setChecked(false);
        }

        cbox.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                Log.d("onClick", "id: " + itemID + ". button ref: "
                        + ((CheckBox) v));
                checkChanged(itemID);
            }

        });

        //I implemented this listener just so I could see when my
        //CheckBoxes were changing.  Through this I was able to determine
        //that my CheckBoxes were being altered outside my own code.
        cbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton arg0, boolean arg1) {

                Log.d("check changed", "button: " + arg0 + " changed to: "
                        + arg1);
            }

        });


        cboxes.add(cbox);

        scrollContent.addView(view);

        cursor.moveToNext();
    }

    cursor.close();

    examineCheckboxes();
}

private void done() {
    Intent i = new Intent();
    i.putExtra(ITEM_LIST, selectedIDs);
    setResult(SUCCESS, i);
    this.finish();
}

private void cancel() {
    db.close();
    finish();
}

private void checkChanged(long itemID) {
    Log.d("checkChaged", "checkChanged for: "+itemID);

    if (selectedIDs.contains(itemID)) {
        selectedIDs.remove(itemID);
    } else {
        selectedIDs.add(itemID);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    long[] array = new long[selectedIDs.size()];
    for (int i = 0; i < array.length; i++) {
        array[i] = selectedIDs.get(i);
    }

    outState.putLongArray(SAVE_SELECTED, array);
}

//Debugging method used to see what is in selectedIDs at any point in time.
private void logIDList() {
    String list = "";

    for (long id : selectedIDs) {
        list += id + " ";
    }

    Log.d("ID List", list);
}

//Debugging method used to check the state of all CheckBoxes.
private void examineCheckboxes(){
    for(CheckBox cbox : cboxes){
        Log.d("Check Cbox", "obj: "+cbox+" checked: "+cbox.isChecked());
    }
}

}

select_items.xml:

<?xml version="1.0" encoding="utf-8"?>

<TextView android:id="@+id/heading_field"
    android:layout_width="fill_parent" android:layout_height="wrap_content"
    android:layout_marginBottom="10dip" android:textSize="18sp"
    android:textStyle="bold" />

<LinearLayout android:id="@+id/button_layout"
    android:orientation="horizontal" android:layout_width="fill_parent"
    android:layout_height="wrap_content" android:layout_alignParentBottom="true">

    <Button android:id="@+id/done_button" android:layout_weight="1"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Done" />

    <Button android:id="@+id/cancel_button" android:layout_weight="1"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Cancel" />

</LinearLayout>

<ScrollView android:orientation="vertical"
    android:layout_below="@id/heading_field" android:layout_above="@id/button_layout"
    android:layout_width="fill_parent" android:layout_height="wrap_content">
    <LinearLayout android:id="@+id/scroll_content"
        android:orientation="vertical" android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    </LinearLayout>
</ScrollView>

item_element.xml:

<?xml version="1.0" encoding="utf-8"?>

<CheckBox android:id="@+id/checkbox" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_alignParentRight="true"
    android:layout_marginRight="5dip" />

<TextView android:id="@+id/item_name" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:textSize="18sp"
    android:layout_centerVertical="true" android:layout_toLeftOf="@id/checkbox"
    android:layout_alignParentLeft="true" />

<TextView android:id="@+id/item_id" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:visibility="invisible" />

Morn answered 26/3, 2010 at 18:35 Comment(0)
H
1

Probably the issue is that whenever onRestoreInstanceState(Bundle) is called it resets some or all of the "settings" of your app to the start-up "defaults". The best way to solve this is through app lifecycle method calls and management. Additionally, any time something is changed in your app that you don't want to loose, save the change to the saveInstanceState(Bundle) or create your own Bundle() to hold the changes temporarily until you can persist the changes in a file or something, Its easy enough to pass a bundle through an Intent between Activities. How you save whatever you need to save depending on how long you need to retain the "settings".

Horsley answered 25/1, 2011 at 18:21 Comment(0)
L
1

I've run into this too. You should restore checkbox state in onRestoreInstanceState() instead of onCreate().

The activity is destroyed when screen orientation changes and onRestoreInstanceState() is called after onCreate(). Since the parent/default implementation of onRestoreInstanceState() automatically restores state in Views with IDs, it's restoring your checkboxes after onCreate() and clobbering them -- apparently due to them having the same ID (framework bug?).

http://developer.android.com/reference/android/app/Activity.html

http://developer.android.com/reference/android/app/Activity.html#onRestoreInstanceState(android.os.Bundle)

Lactoscope answered 22/4, 2011 at 23:45 Comment(1)
This is happening due the the saveEnabled field. See the View docs. There definitely seems to be a bug where views with the same id have their state restored to only the last instance of that view.Elegist
F
1

You can also use onSaveInstanceState(Bundle savedInstanceState) and onRestoreInstanceState(Bundle savedInstanceState)

example

@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
  // EditTexts text
  savedInstanceState.putString("first_et",((EditText)findViewById(R.id.et_first)).getText().toString());
  // EditTexts text
  savedInstanceState.putInt("first_et_v", ((EditText)findViewById(R.id.et_first)).getVisibility());
  super.onSaveInstanceState(savedInstanceState);
}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState)
{
  super.onRestoreInstanceState(savedInstanceState);
  // EditTexts text
  ((EditText)findViewById(R.id.et_first)).setText(savedInstanceState.getString("first_et"));
  // EditText visibility
  ((EditText)findViewById(R.id.et_first)).setVisibility(savedInstanceState.getInt("first_et_v"));
}
Frae answered 9/12, 2011 at 2:23 Comment(0)
S
0

There is a solution that's easier to explain.

Android CHECKBOXES and RADIOBUTTON and RADIOGROUPS behave weirdly if the "id" attribute is not set on them.

I had the exact same problem in my code, and after I placed ids on checkboxes, it started working, without having to disable any of superclass methods.

Slavey answered 9/10, 2016 at 4:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.