Anthony Stonem
Anthony Stonem

Reputation: 3

Changing one item only in a ListView or RecycleView

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?

Upvotes: 0

Views: 905

Answers (4)

sohan shetty
sohan shetty

Reputation: 310

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

Upvotes: 0

Jiyeh
Jiyeh

Reputation: 5307

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~

Upvotes: 0

maphongba008
maphongba008

Reputation: 2353

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

Upvotes: 0

Karakuri
Karakuri

Reputation: 38605

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.

Upvotes: 1

Related Questions