Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Android change the value of EditTexts with same id?

I have a Fragment containing a LinearLayout where different items are inflated depending on some business logic. On of these items contains an EditText. When I have multiple of these items with different content and I detach/attach the fragment, all EditTexts somehow get all the same text. This only happens as long as the the EditText has an id in the layout file.

Why does that happen? Is there any other way to prevent this except removing the id? I would like to use findViewById on my inflated items to access the views instead of error prone getChildAt.

I've created a minimalistic example to demonstrate the problem at https://github.com/rodja/EditTextValueProblem

like image 316
Rodja Avatar asked Dec 05 '13 16:12

Rodja


3 Answers

It can simply be fixed by setting android:saveEnabled="false" in the EditTexts Layout definition. Of course you need to make sure that the content is saved/restored yourself. So this is an non-intuitive work around -- but it works for my case. None the less the entire thing looks like an Android bug:

A nice feature of the Android layout system is that

An ID need not be unique throughout the entire tree [...]

as stated in the Android documentation. This makes code and layout reuse much simpler and is heavily used by developers. I think the save/restore instance state implementation for views uses the view's ID as the key to store it's state, hence it relies on uniqueness in the entire tree. WTF?

Update

I have added a ListView to the example at GitHub which demonstrates that the ListView almost certainly uses a similar workaround to prevent EditTexts to run into this problem. As can be seen, text which is entered into an EditText inside a ListView is not automatically restored.

like image 138
Rodja Avatar answered Nov 20 '22 05:11

Rodja


There is a different possibility, just change the id of the edit text, for example,

mEditText.setId((parentView.getId()+editTextPosition+someFinalNumber));

Or if it is a EditText inside some custom Layout:

mEditText.setId((this.getId()+someFinalNumber));

In this way all EditTexts will have a different id and the text will be restored correctly.

like image 27
Mikelis Kaneps Avatar answered Nov 20 '22 07:11

Mikelis Kaneps


Greetings from the future. Sadly Android framework developers still like to laugh at our expense. Thanks to our bright scientists, we have come up with a bit better solution. See below (yes we still use java in the future). You can find the original "research paper" here.

private static final String KEY_SUPER = "superState";
private static final String KEY_CHILD = "childState";

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    // Don't call super, to disable saving child view states
    dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    // Similar as above, but other way around
    dispatchThawSelfOnly(container);
}

@Override
protected @Nullable Parcelable onSaveInstanceState() {
    val bundle = new Bundle();

    val superState = super.onSaveInstanceState();
    bundle.putParcelable(KEY_SUPER, superState);

    val childStates = saveChildViewStates();
    bundle.putSparseParcelableArray(KEY_CHILD, childStates);

    return bundle;
}

private SparseArray<Parcelable> saveChildViewStates() {
    val childViewStates = new SparseArray<Parcelable>();
    for(val view : getChildViews())
        view.saveHierarchyState(childViewStates);
    return childViewStates;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    val bundle = (Bundle) state;
    val superState = bundle.getParcelable(KEY_SUPER);
    super.onRestoreInstanceState(superState);

    val childState = bundle.getSparseParcelableArray(KEY_CHILD);
    restoreChildViewStates(childState);
}


private void restoreChildViewStates(SparseArray<Parcelable> states) {
    for(val view : getChildViews())
        view.restoreHierarchyState(states);
}

private Iterable<View> getChildViews() {
    int childCount = getChildCount();

    return () -> new Iterator<View>() {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < childCount;
        }

        @Override
        public View next() {
            val view = getChildAt(index);
            if(view == null) {
                throw new RuntimeException("View was null. Index: " + index +
                        " count: " + childCount + ".");
            }
            Objects.requireNonNull(view);
            index++;
            return view;
        }
    };
}

PS: Note that this solution can misbehave, if you order of child Views changes. Bob's uncle says it's okay for what we need right now. I leave it for future scientists to build upon this.

PPS: If you're working at google, feel free to copy paste it into framework and build on it.

PPPS: You might wanna use better app architecture, like MVVM.

like image 1
Peter Avatar answered Nov 20 '22 06:11

Peter