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):
Now when I scroll down, the following happens (still nothing wrong):
and when I scroll back up, the problem shows up:
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.
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.