12

I am inflating a few EditTexts and adding them to a LinearLayout:

private void setupCommentViews(){
    int i = 0;
        Iterator<CommentInformation> iterator = commentInformations.iterator();
    while(iterator.hasNext()) {
            i++;
            View v = LayoutInflater.from(context).inflate(R.layout.comment_row_item, commentsContainer, false);

            EditText commentField = (EditText)v.findViewById(R.id.comment);         

            CommentInformation commentInformation = iterator.next();
            final String uniqueId = commentInformation.getUniqueId();

            String currentCommentText = comments.get(uniqueId);         
            if(currentCommentText != null) {
                Log.v("into","Setting old text: "+currentCommentText);
                commentField.setText(currentCommentText);
            } else {
                Log.v("into","Setting empty text: "+i);
                commentField.setText("Setting empty text: "+i);
            }


            commentField.addTextChangedListener(new TextWatcher() {

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }

                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {

                }

                @Override
                public void afterTextChanged(Editable s) {
                    comments.put(uniqueId, s.toString().trim());
                }
            });

            commentsContainer.addView(v);
        }
}

The logs after the first run look like this:

04-01 17:40:41.244: V/into(28770): Setting empty text: 1
04-01 17:40:41.254: V/into(28770): Setting empty text: 2
04-01 17:40:41.254: V/into(28770): Setting empty text: 3

And all of the EditTexts have the correct text in them ("Setting empty text: #")

-

I am adding a TextChangedListener to my EditText that I inflate and when the text is changed I update a Map<String, String> and put the uniqueId as the key and the comment text as the value.

The problem occurs when I rotate the phone after changing one of the comments. I save the comments Map inside of onSaveInstanceState:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable(COMMENTS, (Serializable)comments);
}

and I then retrieve them inside of onActivityCreated

if(savedInstanceState != null) {
        Log.v("into","Retrieving...");
        comments = (Map<String, String>)savedInstanceState.getSerializable(COMMENTS);
}

I also iterate through that Map and verify that there is one entry with the correct uniqueId and correct changed comment text.

-

Next I call setupCommentViews() again. This time the logs look right, but the EditTexts ALL have the same text as the final one (the last inflated one).

04-01 17:42:49.664: V/into(28770): Setting empty text: 1
04-01 17:42:49.664: V/into(28770): Setting old text: Changed the second textview
04-01 17:42:49.674: V/into(28770): Setting empty text: 3

But all the EditTexts now say: "Setting empty text: 3"

Using a ListView is NOT the solution I'm looking for as there are lots of bugs with EditTexts inside of ListView with AdjustResize softInputMode

-

Can anyone shed any light on what is going on here?

pat
  • 1,005
  • 4
  • 12
  • 29
  • I gather each of your EditText components have their own unique IDs. If so, and since you save the IDs to the Map, can't you just traverse the Map, get the unique ID, match it to each of your EditText, then write the result (i.e. `setText`) to each EditText? – ChuongPham Apr 02 '14 at 00:57
  • Each `EditText` has the same ID (R.id.comment). Since we are inflating them separately I think this is okay since we are getting a reference to them using the newly inflated `View` v `(v.findViewById(R.id.comment))` – pat Apr 02 '14 at 01:03
  • 2
    Even if you inflate them separately, when you do a `setText`, Android will store the value set for that ID, so whichever is the last value set, it will be displayed for all your EditText with same IDs. – ChuongPham Apr 02 '14 at 01:11
  • Hmm I'm not sure about that because wouldn't the first pass cause all the `EditTexts` to have "Setting empty text: 3" as the value? Instead the first time it properly has "Setting empty text: 1" "Setting empty text: 2" and "Setting empty text: 3" – pat Apr 02 '14 at 01:14
  • Put in debug breakpoint where you set your EditText and find out. – ChuongPham Apr 02 '14 at 01:26
  • Well I know that is the case because the EditTexts have the proper values (1,2,3) on screen after the first pass :). Only after rotation do all three get the values of the third one. – pat Apr 02 '14 at 01:28
  • Post your `onSaveInstanceState` so I can see how you saved the text value (Note: You will need to save to the `bundle` and not the Map, as the Map will be reset after each screen rotation.) Also, you may want to set debug breakpoint in this method to see what's been saved - because what is saved here will be written back in your Activity's `onCreate` method after the screen rotates. – ChuongPham Apr 02 '14 at 01:50
  • Yea it is exactly what I have above `outState.putSerializable(COMMENTS, (Serializable)comments);` – pat Apr 02 '14 at 01:52
  • What value is saved to your `outState.putSerializable(...)` method before the screen rotates? And after the screen rotated, and before you write the value with `setText`, what is the value returned. This is value that will be shown for all your EditText with the same IDs. It's good practice to define Android components with unique IDs, otherwise, you get into all sorts of problem, like this one. – ChuongPham Apr 02 '14 at 02:01
  • I've tried to reproduce your code (mostly, as there are implementations I can't know) and I couldn't, so I need 2 clarifications: Where do you start/restart `iterator`? What does `getUniqueId()` return? – nKn Apr 09 '14 at 09:39
  • Thanks! I edited the code to show how I start the `iterator`, it just is an `iterator` on an `ArrayList` of `CommentInformation` objects. `getUniqueId()` simply returns a unique `String` ID tied to that specific `CommentInformation` object so that I can populate my `Map` with the uniqueID as the key and the entered text as the value when the `afterTextChanged` method gets called. – pat Apr 09 '14 at 18:33
  • I've tried that android:saveEnabled="false" as in the accepted answer and worked perfectly – MohamedHarmoush Sep 19 '21 at 15:23

4 Answers4

21

By default, when onSaveInstanceState method of an activity is called, it also goes through the view hierarchy and try to save the state of any view if possible and restore those views state if onRestoreInstanceState method of activity is called later. When each View is called to save the state, we can see this method in the View class:

protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            Parcelable state = onSaveInstanceState();
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onSaveInstanceState()");
            }
            if (state != null) {
                // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                // + ": " + state);
                container.put(mID, state);
            }
        }
    }

EditText inherits from TextView, when check the code of TextView we see the onSaveInstanceState method return current text and onRestoreInstanceState method get back the text and display.

Now, back to your code. You save the text content yourself and restore it when onCreate is called, but EditText itself also save the text and restore when onRestoreInstanceState method of Activity is call, which is called after onCreate for sure, one more thing, all your EditText have same id, so when the state of view hierarchy is saved in to a SparseArray, the last EditText state (in this case is text content) will overwrite all the previous. And that's why your problem happen.

Solutions:

  1. You can override method onRestoreInstanceState of activity and not call super method so that you can prevent the views to auto-restore its state, but I don't think this is good idea since there might be other things need to be restored by default, not only your EditText
  2. You can set the attribute saveEnabled of EditText to false so that it will not restore the state itself, I recommend this.
Binh Tran
  • 2,478
  • 1
  • 21
  • 18
2

I had the same problem. In my custom LinearLayout I Override these methods.

   @Override
protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    return new SavedState(superState, txtData.getText().toString());
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState savedState = (SavedState) state;
    super.onRestoreInstanceState(savedState.getSuperState());
    txtData.setText(savedState.data);
}

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    super.dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    super.dispatchThawSelfOnly(container);
}

private class SavedState extends BaseSavedState {
    public final Creator<SavedState> CREATOR = new Creator<SavedState>() {
        @Override
        public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
        }

        @Override
        public SavedState[] newArray(int size) {
            return new SavedState[size];
        }
    };
    private String data;

    public SavedState(Parcel source) {
        super(source);
        data = source.readString();
    }

    public SavedState(Parcelable superState, String data) {
        super(superState);
        this.data = data;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString(data);
    }
}

Hope this helps.

Yogitha Bellare
  • 109
  • 1
  • 5
1

EditText restores text based on resId. If your edittexts don't have resIds or all have the same resIds you'll have restore problems.

MinceMan
  • 7,483
  • 3
  • 38
  • 40
1

I met this problem too, but I have to restore the sate before. So i create a View named FixedEditText and override both dispatchSaveInstanceState() and dispatchRestoreInstanceState() two method. just like this:

 override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
    try {
        val key = initKey()
        if (key != null && isSaveEnabled) {
            val state = onSaveInstanceState()
            if (state != null) {
                //don't use the id as the key
                container.put(key.hashCode(), state)
            }
        }
    } catch (e: Exception) {
        super.dispatchSaveInstanceState(container)
        e.printStackTrace()
    }
}

override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
    try {
        val key = initKey()
        if (key != null) {
            val state = container.get(key.hashCode())
            if (state != null) {
                onRestoreInstanceState(state)
            }
        }
    } catch (e: Exception) {
        super.dispatchRestoreInstanceState(container)
        e.printStackTrace()
    }
}
 private fun initKey(): Any? {
    (parent as? View)?.run {
        //My ViewGroup has diff id.
        return "${this.javaClass.simpleName}$${this.id}"
    }
    return null
}
joe
  • 619
  • 6
  • 10