Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android CheckBox -- Restoring State After Screen Rotation

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.

like image 505
Jared M Avatar asked Mar 24 '10 22:03

Jared M


7 Answers

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" />
like image 120
Grzegorz Dev Avatar answered Sep 28 '22 02:09

Grzegorz Dev


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.

like image 25
Jared M Avatar answered Sep 28 '22 02:09

Jared M


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.

like image 42
ExistentialEnso Avatar answered Sep 29 '22 02:09

ExistentialEnso


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" />

like image 31
Jared M Avatar answered Sep 29 '22 02:09

Jared M


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".

like image 29
Kingsolmn Avatar answered Sep 28 '22 02:09

Kingsolmn


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)

like image 33
JustinC Avatar answered Sep 25 '22 02:09

JustinC


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"));
}
like image 28
bdely Avatar answered Sep 29 '22 02:09

bdely