0

I have a ListView of a custom object with a custom adapter. Clicking on an individual item of list starts a new activity for result that updates that item and then returns back to the list with a green tick image on the right side of the ListView item which is a TextView and I just use row.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0) However when I scroll down, I see another row with the same drawable. Scrolling back and forth just sets this drawable to different rows randomly. Most answers say that it's because the views are being recycled but what is the way to change one single row irrespective of whether it is visible or not when the other activity returns a result?

code from Activity with the list view -

private ItemAdapter adapter;
private ArrayList<Item> labels;
private ArrayList<Item> updatedItems;
private TextView label;
@Override
protected void onCreate(Bundle savedInstanceState) {
    // initializations

    updatedItems = new ArrayList<>();
    adapter = new ItemAdapter(getBaseContext(), R.layout.label_list_item, labels);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                    @Override
                    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                        label = (TextView) listView.getChildAt(position - listView.getFirstVisiblePosition()).findViewById(R.id.text1);
                        Item item = adapter.getItem(position);
                        Intent myIntent = new Intent(ProviderPutawayActivity.this, PutawayScanLocationActivity.class);
                        myIntent.putExtra("ITEM", item);
                        ProviderPutawayActivity.this.startActivityForResult(myIntent, 1);
                    }
                });
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == 1) {
        Item item = (Item) data.getSerializableExtra("ITEM");
        label.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0);
}

v.findViewById(r.id.text1) and (TextView) listView.getChildAt(position - listView.getFirstVisiblePosition()).findViewById(R.id.text1) as suggested here both have the same problem.

The ItemAdapter class -

public class ItemAdapter extends ArrayAdapter<Item> {
  private View v;

  public ItemAdapter(Context context, int resource, List<Item> items) {
      super(context, resource, items);
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

      v = convertView;
      TextView label;

      if(v == null) {
          LayoutInflater vi;
          vi = LayoutInflater.from(getContext());
          v = vi.inflate(R.layout.label_list_item, null);
      }

      Item item = getItem(position);
      if(item != null) {
          label = (TextView)v.findViewById(R.id.text1);
          if(label != null) label.setText(String.valueOf(item.getLabel()));
      }
      return v;
  }
}

How do I make sure I only update the row that was clicked?

Community
  • 1
  • 1
  • You must implement view holder pattern to fix your issue. There are lots of tutorials where you can learn about it. – Gautam Dec 21 '16 at 06:07
  • I did use it when I tried using `RecyclerView` instead of `ListView` to do the same thing, and yet two separate rows showed the drawable after clicking on only one row. – Anthony Stonem Dec 21 '16 at 06:12

4 Answers4

1

The issue is that you never unset the drawable when the view is recycled. This has nothing to do the ViewHolder pattern, it has to do with the way you are updating the item. Your getView() method should have something like this:

int check = item.isChecked() ? R.drawable.tick_green : 0;
label.setCompountDrawablesWithIntrinsicBounds(0, 0, check, 0);

This is the correct way to bind the item data to the row view.

What you are doing now is saving some views of a clicked row into member fields of the Activity and updating them once when you get to onActivityResult(). The problem is if I scroll away and scroll back, there is nothing identifying which item is actually checked and the adapter has no way to show the proper state.

What you should be doing is modifying the actual data source (the Item in this case, or the structure containing the Items), and then call notifyDatasetChanged() on the adapter. This will cause ListView to rebuild its rows, so your proper binding logic will take care of showing the correct state.


As an aside, switching to RecyclerView has a number of benefits (at the cost of a little more complexity). For one thing, with a RecyclerView.Adapter you can notify that a single item changed, so it will only rebuild one row; this is impossible with ListAdapters. For another, it will force you to use viewholders, which is more performant anyway.

Karakuri
  • 38,365
  • 12
  • 84
  • 104
0

First, you should store a variable that represent the green tick image in your Item (ex: boolean isSelected) Then, in onActivityResult, check the condition, update the isSelected and reload the listView

maphongba008
  • 2,114
  • 4
  • 20
  • 33
0

I was a "victim" to this view recycler mechanism too!

Your problem of

However when I scroll down, I see another row with the same drawable. Scrolling back and forth just sets this drawable to different rows randomly.

is basically because

listView with adapter tenses to recycle/reuse viewItems that are no longer visible.

Check this Answer if you want detailed-explanation on this.

For your case, just put a else for your if (when it comes to setting adapter's views):

if(label != null) 
    label.setText(String.valueOf(item.getLabel()));
else
    label.setText("Default text");

Hope this helps~

Community
  • 1
  • 1
Jiyeh
  • 5,187
  • 2
  • 30
  • 31
0

As like above answer you should notifiyDataSetchange whenever you need to update item row in listview or recyclerview. So here you go

First you have to create setter and getter method in item model class to update your list view row. like below

public class Item {
  private boolean mIsSelected;

  public void setIsSelected(boolean isSelected) {
     mIsSelected = isSelected;
  }

  public boolean isSelected() {
   return mIsSelected;
   }
}

Then in your ItemAdapter class inside getView() method do like this

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

       v = convertView;
       TextView label;

     if(v == null) {
      LayoutInflater vi;
      vi = LayoutInflater.from(getContext());
      v = vi.inflate(R.layout.label_list_item, null);
      }

  Item item = getItem(position);
  if(item != null) {
      label = (TextView)v.findViewById(R.id.text1);
      label.setText(String.valueOf(item.getLabel()));
      if (item.isSelected) {
        label.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0);
      } else { // by default it will be false until you select an item here
      label.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
      }

    }
  return v;
  }

Then in your activity inside setOnItemClickListener update your item model class like below

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View v, int position, long id) {

                    Item item = adapter.getItem(position);
                    item.setIsSelected(true);
                    Intent myIntent = new Intent(ProviderPutawayActivity.this, PutawayScanLocationActivity.class);
                    myIntent.putExtra("ITEM", item);
                    ProviderPutawayActivity.this.startActivityForResult(myIntent, 1);
                }
            });

Then inside startActivityResult update your list with notifyDataSetChanged like this

          @Override
       protected void onActivityResult(int requestCode, in resultCode, Intent data) {
       if(requestCode == 1) {
        adapter.notifyDataSetChanged();
         }

Now you will get expected output

sohan shetty
  • 289
  • 1
  • 16
  • Thanks for the detailed answer! I wasn't sure if notifyDataSetChanged() would work because I thought I was passing the value of the object to the other activity and when I get it from the intent on activity result is it the same object or a new one? – Anthony Stonem Dec 21 '16 at 10:52
  • @AnthonyStonem Is this actually working for you? I don't believe `Serializable` extras produce the same object instance when you extract them, which means you still need to modify the instance used in te adapter's data set. – Karakuri Dec 21 '16 at 15:39
  • In that case it shouldn't work, but the correct row gets updated and stays so even after scrolling – Anthony Stonem Dec 22 '16 at 08:55
  • But it works only because it sets `isSelected` onItemClick, and not on activity result so it is the object in adapter's data set – Anthony Stonem Dec 23 '16 at 02:45