john-aziz57 john-aziz57 - 3 months ago 14
Android Question

Android Compound Custom View restore save state

I created compound custom view which contains

TextView
and
EditText
called
LabledEditText
, since I will have a lot of EditText fields in a fragment.

I have created an XML file that holds the following

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linearLayoutLabeledEditText"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/label_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label"
android:textAppearance="?android:attr/textAppearanceMedium" />

<EditText
android:id="@+id/value_editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:freezesText="true"
android:saveEnabled="true"/>
</LinearLayout>


and in the View class is as following

public class LabeledEditText extends LinearLayout {
private EditText editText;
private TextView label;

public LabeledEditText(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context,R.layout.labeled_edit_text, this);
label = (TextView) this.findViewById(R.id.label_textView);
label.setText("some label");
editText = (EditText) this.findViewById(R.id.value_editText);
}

protected void onRestoreInstanceState(Parcelable state) {
String id= this.getId()+" ";
if (state instanceof Bundle) // implicit null check
{
Bundle bundle = (Bundle) state;
state = bundle.getParcelable(id+"super");
super.onRestoreInstanceState(state);
editText.setText(bundle.getString(id+"editText"));
}
}

protected Parcelable onSaveInstanceState() {
String id= this.getId()+" ";
Bundle bundle = new Bundle();
bundle.putParcelable(id+"super",super.onSaveInstanceState());
bundle.putString(id+"editText",editText.getText().toString());
return bundle;
}
}


then I use it in a 3 fragments that represent 3 steps. When I insert values in the first 1 step/fragment

1st time after visiting the first step

then switch to other fragments and return to the 1 step/fragment again I find the following

2nd time visiting the first step

what is causing this problem ?

I have been debugging it for at least 5 days, keeping in mind that each of those custom views has different id when used inside the fragment layout.

I also have tried to add the id of the custom view as part of the key during saving the state
this.getId()+"editText"
still the same problem.

EDIT
the genrateViewId for api < 17

the code after alteration

import java.util.concurrent.atomic.AtomicInteger;

public class LabeledEditText extends LinearLayout {
private EditText editText;
private TextView label;

public LabeledEditText(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context,R.layout.labeled_edit_text, this);
label = (TextView) this.findViewById(R.id.label_textView);
editText = (EditText) this.findViewById(R.id.value_editText);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
editText.setId(this.generateViewId());
else{
editText.setId(generateViewId());
}
applyAttr(context,attrs);
}

@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
//adding the id of the parent view as part of the key so that
//editText state won't get overwritten by other editText
//holding the same id
bundle.putParcelable(getId()+"super",super.onSaveInstanceState());
bundle.putString(getId()+"editText",editText.getText().toString());
return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) // implicit null check
{
Bundle bundle = (Bundle) state;
state = bundle.getParcelable(getId()+"super");
super.onRestoreInstanceState(state);
editText.setText(bundle.getString(getId()+"editText"));
}
}

private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

public static int generateViewId() {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
}
}

Answer

Your problem is that you have multiple ViewGroups (LinearLayout), with children having same ids. Therefore when saving state, all of them are being saves in same state, and the last one, overwrites all.
To solve this, you have to give each view a unique I'd when you inflate. In v17 and later you can use View.generateViewId();, in older versions you will have to create static ids manually in the ids file.
Your code should look like this;

public LabeledEditText(Context context, AttributeSet attrs) { 
    super(context, attrs); inflate(context,R.layout.labeled_edit_text, this); 
    label = (TextView) this.findViewById(R.id.label_textView); label.setText("some label"); 
    editText = (EditText) this.findViewById(R.id.value_editText); 
    label.setId(View.generateViewId());
    editText.setId(View.generateViewId());
}

In any case it may be better to use static ids, as it would be easier to reference them later. You may not even need anymore to overwrite the onSave and onRestore, especially if you use static ids.