1

I have a ListView with a custom ArrayAdapter that uses a list of OrderedProductItems (my own Model-class). In this ArrayAdapter's getView I use the ViewHolder design pattern:

@Override
public View getView(int position, View convertView, ViewGroup parent){
    View view = convertView;
    MyHolder h;
    if(view == null){
        view = inflater.inflate(layoutResourceId, parent, false);
        RelativeLayout rl = (RelativeLayout) view.findViewById(R.id.item_layout);
        h = new MyHolder(rl);
        view.setTag(h);
    }
    else
        h = (MyHolder) view.getTag();

    if(h != null){
        h.orderedProductItem = getItem(position);

        ... // Do stuff like:
            //    - setting Texts of TextViews/EditText
            //    - updating data of AutoCompleteTextView and Spinner
            //    - etc.

        h.etResultPrice.setTag(h.orderedProductItem);
        h.etResultPrice.addTextChangedListener(new MyTextWatcher(h.etResultPrice));
    }
}

In my ListView's Items I have an EditText that uses a custom TextWatcher. Inside this TextWatcher I save the input of the User to my OrderedProductItem-class:

public class MyTextWatcher implements TextWatcher
{
    // Logcat tag
    private static final String TAG = "MyTextWatcher";

    // The values used for the test output of changed EditText texts
    private View view;
    private String from;

    public MyTextWatcher(View v){
        view = v;
    }

    // Default Android method used by TextWatcher
    // (to do something before an EditText's is going to change)
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Get the old text before we make a change to this EditText
        from = ((EditText)view).getText().toString();
    }

    // Default Android method used by TextWatcher
    // (to do something when an EditText's text is changing)
    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {}

    // Default Android method used by TextWatcher
    // (to do something after an EditText's text is changed)
    @Override
    public void afterTextChanged(Editable s) {
        if(s != null && s.toString() != null && view.getTag() != null){
            OrderedProductItem opi = (OrderedProductItem)view.getTag();
            if(opi != null){
                // Send the changed text of this EditText to the OrderedProductItem's Result-values
                if(view.getId() == R.id.et_result_amount){
                    int amount = 1;
                    try{
                        amount = Integer.parseInt(s.toString());
                    }
                    catch(NumberFormatException ex){
                        amount = 1;
                    }
                    if(opi.getResultAmount() != amount){
                        opi.setResultAmount(amount);
                        Log.i(TAG, "ResultAmount changed from " + from + " to " + s.toString() + " (-> " + String.valueOf(opi.getResultAmount()) + ")");
                    }
                }
                else if(view.getId() == R.id.actv_result_name){
                    if(!V.compare(opi.getResultName(), s.toString(), false, false)){
                        opi.setResultName(s.toString());
                        Log.i(TAG, "ResultName changed from " + from + " to " + s.toString() + " (-> " + opi.getResultName() + ")");
                    }
                }
                else if(view.getId() == R.id.et_result_price){
                    double price = C.priceStringToDouble(s.toString());
                    if(opi.getResultPrice() != price){
                        opi.setResultPrice(price);
                        Log.i(TAG, "ResultPrice changed from " + from + " to " + s.toString() + " (-> " + String.valueOf(opi.getResultPrice()) + ")");
                    }
                }
            }
            else
                Log.wtf(TAG, "Tag's OrderedProductItem is null");
        }
    }
}

Now my problem is this: Since ArrayAdapter's uses a getView-recycler (see this post for more information about getView recycling (including picture)), my TextWatcher overrides incorrect data of another OrderedProductItem. For example, let's say my OrderedProductItem's EditTexts are filled like so at creation (nothing is wrong here):

  1. expected "1" -- actual "1"
  2. expected "2" -- actual "2"
  3. expected "3" -- actual "3"
  4. expected "4" -- actual "4"
  5. expected "5" -- actual "5"
  6. expected "6" -- actual "6"
  7. expected "7" -- actual "7"
  8. expected "8" -- actual "8"
  9. expected "9" -- actual "9"
  10. expected "10" -- actual "10"

Now when I scroll down, the following happens (still nothing wrong):

  1. expected "5" -- actual "5"
  2. expected "6" -- actual "6"
  3. expected "7" -- actual "7"
  4. expected "8" -- actual "8"
  5. expected "9" -- actual "9"
  6. expected "10" -- actual "10"
  7. expected "11" -- actual "11"
  8. expected "12" -- actual "12"
  9. expected "13" -- actual "13"
  10. expected "14" -- actual "14"

and when I scroll back up, the problem shows up:

  1. expected "1" -- actual "14"
  2. expected "2" -- actual "13"
  3. expected "3" -- actual "12"
  4. expected "4" -- actual "11"
  5. expected "5" -- actual "5"
  6. expected "6" -- actual "6"
  7. expected "7" -- actual "7"
  8. expected "8" -- actual "8"
  9. expected "9" -- actual "9"
  10. expected "10" -- actual "10"

I did try to temporarily disable the TextWatcher at the start of the getView (after I got the EditTexts), then set the value from the OrderedProductItem and then re-apply MyTextWatcher to them, but then it still changes the OrderedProductItem's values after I re-applied my TextWatcher..

Does anyone know how to deal with the getView recycling when using an TextWatcher to save UserInput of the Item's EditTexts?

PS: I know I can add a button next to the EditTexts so it only saved the data when the button is clicked, but I don't want this.

Community
  • 1
  • 1
Kevin Cruijssen
  • 9,153
  • 9
  • 61
  • 135

1 Answers1

3

It looks like your problem is here:

 etResultPrice.setTag(currentOrderedProductItem);

If you tried to remove the TextWatcher and it still has problems, it references the Tag and if you don't reset your tag properly with a recycled view, you will have problems. In particular, it looks like you are trying to reset it, but not doing it properly because when views are recycled, they should go in order. That means that when you scroll back up, your lines:

 expected "3" -- actual "12"
 expected "4" -- actual "11"
 expected "5" -- actual "5"
 expected "6" -- actual "6"

On "expected "4' -- actual "11" " - your logic for reseting the Tag must be wrong because it should be recycling "14" - so it's not the recycling that is a problem, it's the logic you are using to perform the recycling. Your code is assuming this is item #11 like in the initial scroll instead of recognizing that it should be #4.

Probably somewhere with this: when you create a view, you do this:

 view.setTag(h);

But in your logic, you also do this:

h.etResultPrice.setTag(h.orderedProductItem);

This appears recursive and could be out of sync if you have something wrong in your view holder.

Jim
  • 10,172
  • 1
  • 27
  • 36
  • I've made an Edit to show some missing code inside the `if(h != null){ ... }`. I store the OrderedProductItem inside the ViewHolder by using `h.orderedProductitem = getItem(position)` at the begin of the getView method. This `h.orderedProductItem` is also the one I set as Tag. How can this be wrong for `expected "4" - actual "11"` then? I set the new h.orderedProductItem and EditText's tag as the OrderedProductItem of position 4.. (PS: The reason why I set the OrderedProductItem inside the ViewHolder is because I also use in inside my ListActivity somewhere, where I can't access its position.) – Kevin Cruijssen Aug 11 '14 at 13:17
  • I edited my comment. Maybe it will help. You might have to post more code, but the problem is likely in your holder - I've seen this type of thing before with my own code. – Jim Aug 11 '14 at 13:45
  • 1
    Ok, I found my problem and I feel stupid.. As you can see in my getView method inside the `if(h != null)` I have the following steps: 1 "set ViewHolder's OrderedProductItem", 2 "set Texts and update Views", 3 "set EditText's Tag and TextWatcher". My problem was in step 2 where I also set those of my EditTexts and only after that change my EditTag's Tag. So now that I swapped steps 2 and 3, and first set my EditText's Tag before I set the Text, it works. Since you pointed out that my Tag is probably wrong I found the problem, so I accepted your answer as the correct one. Tyvm! – Kevin Cruijssen Aug 11 '14 at 14:01
  • @Jim please see my related question here http://stackoverflow.com/questions/35939762/edittext-values-in-listview-changes-to-default-on-screen-timeout – Steve Kamau Mar 15 '16 at 14:55