5

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);
        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("super",super.onSaveInstanceState());
        bundle.putString("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("super");
            super.onRestoreInstanceState(state);
            editText.setText(bundle.getString("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;
            }
        }
    }
}
Community
  • 1
  • 1
john-salib
  • 575
  • 11
  • 26
  • I don't know exactly the other things you tried, but the code you posted here will definitely return these results. You keep on saving all values to the same editText key, so it keeps on getting overwritten, so you stay with the last one, which is 5. You say you tried something else, post that code. – lionscribe Aug 28 '16 at 12:41
  • I will, but just to consider doesn't every view store its state in a different bundle. – john-salib Aug 28 '16 at 12:46
  • Why are you at all dealing with saving state. The OS should be saving the basic state of the editText. You save state only for custom variables. – lionscribe Aug 28 '16 at 13:00
  • the same problem happens otherwise I wouldn't have thought of saving the state at all – john-salib Aug 28 '16 at 14:22

1 Answers1

8

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.

lionscribe
  • 2,917
  • 1
  • 12
  • 17
  • after thorough investigation, it returns the same editText object in every call of restoreState of this custom view. I managed to use part of your code to help me restore the state correctly – john-salib Aug 29 '16 at 07:35
  • Please post your final code, so that future users can use it. – lionscribe Aug 29 '16 at 10:28
  • I would really appreciate it if you have any comments on the code above – john-salib Aug 29 '16 at 15:48
  • 1
    2 comments. You would be better off calling your myGenerateViewId, for the way it's now, it's not clear which you are using, and can create compiler issues. 2. You can remove the getId() from the key name, as that was not the issue. 3. I am not sure what happens when it is restored, probably the constructor gets called. If so, you will be having new ids for the editText, so how does it know what to restore. If this sounds confusing, it is because I am a little confused how restoring works on compound views. – lionscribe Aug 29 '16 at 16:12
  • 1
    If you do this, what is the guarantee that the id that gets created during restore is same as the id generated during create? – Ashok Koyi Jul 27 '17 at 16:02
  • 1
    @kalinga I will make this short. There is a big difference between Activities and Fragments. In Fragments, the OS restore the actual views, so other than making sure each view has a unique id, there is no need for anything further. In Activities, the app has to recreate the views, which the OS restore afterwards to state, with a call to restoreState. For this to work, it is the apps responsibility to make sure that the ids are the same as the first time, otherwise restoreState cannot restore it. That's why I suggested using staticIDs, so that you can use same logic and use same ids. – lionscribe Jul 28 '17 at 00:28
  • @lionscribe In effect the code snippet (accepted answer) which says he should use generated ids is not that useful for the problem at hand. I request you to correct it so that people dont get an impression that they should use `generatedId` (tho you mentioned the benefit of static ids at the end, its almost overshadowed by the `generatedId` completely) – Ashok Koyi Jul 28 '17 at 12:06
  • @kalinga As I noted in previous comment, in Fragments using generatedID is fine, as the OS recreates all the views. The sample code was using Fragments, therefore it was fine. In Activities it gets much more complicated. Using staticIDs is very often not an option in dynamic ListViews, especially if items can be added or removed. The solution would be, having the ListView itself save to Bundle an array with the ids, so when it is recreated by app, the app can set the same ids. Using this method, generatedIDs can be used. – lionscribe Jul 28 '17 at 21:40
  • 1
    <> Did u validate this statement in a sample?. I have done the testing myself & it does not work. Android does not have a clue about the view because the newly generated id is different from the old one. Even when the views are not dynamic. Please create a simple fragment with a simple custom view whose id you set programatically using `generateId` & see if the state of the view is restored or not. – Ashok Koyi Jul 29 '17 at 07:11
  • @kalinga You are probably recreating your Fragment in the Activity onCreate function, therefore it has no knowledge of it's previous details. You should be using the FragmentManager to retrieve the original Activity. Your code should be something like this 'private Fragment1 mFragment; protected void onCreate(Bundle savedState) { super.onCreate(savedState); // ... if (savedState == null) { mFragment = Fragment1.newInstance(); ///... } else { mFragment = getFragmentMananager().findFragmentByTag(TAG); } ///... }' – lionscribe Jul 30 '17 at 03:18
  • Sorry. Don't remember. I'm now using stable id generator by following some conventions – Ashok Koyi Jul 31 '17 at 07:34