Why are highlighted items lost after scrolling in recylerview android

Viewed 187

I want to highlight the item when the action mode is active in the adapter class. I am able to do so but the highlight state is gone after scrolling. I have tried various solutions but I don't know understand why this is happening?

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>  {

   
    class MyViewHolder extends RecyclerView.ViewHolder {
 
     
     public void bind(Items viewHolder_item) {     

       
            itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                isSelectMode = true;
                if (viewHolder_item.getIsSelect()){
                    
                    itemView.setBackgroundColor(Color.TRANSPARENT);
                    item.get(position).setSelect(false);
                    selectedList.remove(item.get(position));
                } else {
                    itemView.setBackgroundColor(Color.GRAY);
                    item.get(position).setSelect(true);
                    selectedList.add(item.get(position));
                }
                if (selectList.size() == 0){
                    isSelectMode = false;
                }
                return true;
            }
        });
        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isSelectMode){
                    if (viewHolder_item.getIsSelect()){
                        itemView.setBackgroundColor(Color.TRANSPARENT);
                        item.get(position).setSelect(false);
                        selectedList.remove(item.get(position));
                    } else {
                                                  
                       itemView.setBackgroundColor(Color.GRAY);
                       item.get(position).setSelect(true);   
                       selectedList.add(item.get(position));                      
                    }
                    if (selectList.size() == 0){
                        isSelectMode = false;
                    }
                }
            }
        });
}

No matter which solution is implemented, the result is always the same. The highlighted color is gone after scrolling. Any help would be appreciated Thanks.

2 Answers

I recommend that you take an alternative approach to solve your problem. In your Item object add a boolean field called public boolean isSelected=false then just set that field to true inside your items list using items.get(position).isSelected=true when an item is clicked and then use this field in order to evaluate which state each item should be in, instead of using your selectedList. you actually wont need this list anymore.

So now you just have to modify your on bindViewHolder as follows:

@Override
    public void onBindViewHolder(@NonNull MyAdapter.MyViewHolder holder, int position) {
        Items item=items.get(position);
            
        if (!item.getIsSelect()){
               holder.itemView.setBackgroundColor(Color.TRANSPARENT);
            } else {
            holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
            }

            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    isSelectMode = true;
                    if (item.getIsSelect()){
                        holder.itemView.setBackgroundColor(Color.TRANSPARENT);
                        items.get(position).setSelect(false);
                        selectedList.remove(items.get(position));
                    } else {
                        holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
                        items.get(position).setSelect(true);
                        selectedList.add(items.get(position));
                    }
                    if (selectList.size() == 0){
                        isSelectMode = false;
                    }
                    return true;
                }
            });
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (isSelectMode){
                        if (item.getIsSelect()){
                            holder.itemView.setBackgroundColor(Color.TRANSPARENT);
                            items.get(position).setSelect(false);
                            selectedList.remove(items.get(position));
                        } else {
                                                      holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
                        items.get(position).setSelect(true);                         
                        }
                        if (selectList.size() == 0){
                            isSelectMode = false;
                        }
                    }
                }
            });

    }    

btw. you dont need to add isSelect to the constructor of Items as this should be an ephemeral session specific variable

there are a bunch of instances that can make contains not work as you would expect we would probably have to dive into your Item object to find a cause here is just one link Why is .contains returning false when I add a duplicate?

A few additions to the existing answer:

  1. Treat your Adapter data as non-mutable. Do NOT mutate data inside the adapter. The Adapter needs to well... adapt data from a Model into a View (Holder). It keeps track of where each data belongs in terms of positioning, and that's pretty much all it needs to do (aside from inflating the views naturally).
  2. If you supply a list of Thing, where isSelected is not part of the model, but rather part of your need to keep track of whether the user has selected it or not, then create a simple data class like
data class ThingSelection(val thing: Thing, val isSelected: Boolean)

And use that in your adapter instead of just a List<Thing>, use List<ThingSelection>.

  1. Do not do what you do in the onBindViewHolder. All you should do there is
val item = getItem(position)
holder.bind(item)

It's the ViewHolder's job to set its properties, background color, etc. based on your business rules.

If your "item" is a ThingSelection then the viewHolder's bind method can do

if (item.isSelected) { ... }

Pass anything else you need to this method as well, bind(...) is whatever you need it to be.

What to do when the user changes the item selection?

You have a click listener, but don't mutate data, have your callback/listener pass what happened to the caller.

I imagine in your viewHolder bind method you'd do something like:

view.setOnClickListener {
   externalListenerThatTheAdapterReceivedFromTheOutside.onThingClicked(thing, thing.isSelected)
}

It's this external listener's responsibility to:

  1. Do what it takes (possibly pass it directly to a ViewModel so it work on this new event), to ensure the adapter receives the new data (now where the Thing.isSelected is correct given what the user did).

  2. Ensure the new immutable list is supplied to the adapter so the adapter can compute its new data and update (hint: use ListAdapter<T, K> with a DiffUtil.ItemCallback<T>, it makes your life easier.

UPDATE: Data Class

data class ItemWithSelection(val item: Item, val isSelected: Boolean)

Then change your adapter to be:

class YourAdapter(): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {
  // You only need to override these
    
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YourViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.your_item_layout, parent, false) // or use ViewBinding, whatever.
        return YourViewHolder(view) // you can pass more stuff if you need here, like a listener...
    }

    override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
        val item = getItem(position)
        holder.bindData(item, item.isSelected) //Item is of type ItemWithSelection...
    }
}

// Somewhere in your ViewHolder...
fun bindData(item: ItemWithSelection, isSelected: Boolean) {
   // do what it needs, e.g.:
   if (isSelected) { ... } 
}


// You also need a DiffUtilCallback...
internal class DiffUtil.ItemCallback<ItemWithSelection>() {

    internal class DiffUtilCallback : DiffUtil.ItemCallback<ItemWithSelection>() {
        override fun areItemsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem.item.xxx == newItem.item.xxx

        override fun areContentsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem == newItem
    }

With all that wired up...

In your ViewModel or "layer where you get the data" (most likely not the fragment/activity) provide a List<ItemWithSelection>...

How to construct this really depends on how you store what is selected.

But let's say you have two lists for the sake of simplicity:

val all: List<Item>
val selected: List<Item>

When it's time to produce a list for the UI, you could (note: I'm gonna assume you use Coroutines... you pick your own flavor of asynchronous/reactive programming:

class YourViewModel: ViewModel() {

fun getYourListWithSelections() {
        viewModelScope.launch(Dispatchers.Computation) {//for e.g.
            val tempSelection = mutableListOf<ItemWithSelection>()
            all.mapTo(tempSelection) {
                    ItemWithSelection(item = it, isSelected = selection.contains(it))
                }

           // Assuming you have a MutableLiveData somewhere in this viewmodel...
           _uiState.postValue(SomeSealedClass.ListAvailable(tempSelection))
        }
}

This is naturally observed from the Fragment...

class YourFragment: Fragment() {

  fun observe() {
       viewModel.uiState.observe(viewLifecycleOwner, { viewState ->
            when (viewState) {
                is SomeSealedClass.ListAvailable -> adapter.submitList(viewState.items.toMutableList())//need to pass a new list or ListAdapter won't refresh the list.
                else ... //exercise for the reader ;)
            }
        })
  }

}

All that is really missing here are the boiler plate stuff.

  1. How does the ViewHolder (which sets a click listener on the View pass the info back to the ViewModel? Supply your adapter with a Listener of your choice:
interface ItemClickListener {
  fun onItemClicked(val item: Item, val isSelected: Boolean)
}

Implement that and pass it to your adapter when you create it:

class YourAdapter(private val listener: ItemClickListener): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {

And pass it in your fragment:

class YourFragment: Fragment() {

private val listener = object : ItemClickListener {
   fun onItemClicked(val item: Item, val isSelected: Boolean) {
      // pass it directly, it's the VM job to deal with this and produce a new list via LiveData.
      viewModel.onItemClicked(item, isSelected)
   }
}
val adapter = YourAdapter(listener)

Hope that helps.

Related