0

I have a list with RecyclerView + Adapter + ViewHolder and each item list contains a description and a CheckBox. If I select an item and scroll down the list RecyclerView does not keep the selection when I scroll up again.

Here is my code:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: MyAdapter
    private val items = mutableListOf<Item>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        adapter = MyAdapter()
        binding.recyclerView.adapter = adapter
    }

    override fun onResume() {
        super.onResume()

        for (i in 1..100) {
            items.add(Item("Item $i", false))
        }
    }

    inner class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
            return MyViewHolder(view)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            val item = items[position]
            holder.textView.text = item.description
            holder.checkBox.isChecked = item.isSelected
            holder.checkBox.setOnCheckedChangeListener {_, isChecked ->
                item.isSelected = isChecked
                holder.checkBox.isChecked = isChecked
            }
        }

        override fun getItemCount(): Int {
            return items.size
        }
    }

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        val textView: TextView = itemView.findViewById(R.id.textView)
        val checkBox: CheckBox = itemView.findViewById(R.id.checkBox)
    }

    data class Item(val description: String, var isSelected: Boolean)
}

2 Answers2

0

You need to notify the RecyclerView of the changes. In your case, you can call notifyDataSetChanged() inside the setOnCheckedChangeListener of your CheckBox to tell the RecyclerView to refresh its entire content:

holder.checkBox.setOnCheckedChangeListener {_, isChecked ->
    //...
    notifyDataSetChanged()
}

Keep in mind that notifyDataSetChanged() is a simple but less efficient way of updating the RecyclerView. If you only update a specific item in the list, consider using notifyItemChanged(position) instead. This way, the RecyclerView will only update the affected item, resulting in better performance.

Edit: You need also to use those two lines within a constructor:

val textView: TextView = itemView.findViewById(R.id.textView)
val checkBox: CheckBox = itemView.findViewById(R.id.checkBox)

Like this:

inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val textView: TextView
    val checkBox: CheckBox

    init {
        textView = itemView.findViewById(R.id.textView)
        checkBox = itemView.findViewById(R.id.checkBox)
    }
}
Abdo21
  • 498
  • 4
  • 14
  • Thanks for your help @Abdo21. I already did it and Id got this error: `java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.recyclerview.widget.RecyclerView`. Then I tried to apply this solution: [here](https://stackoverflow.com/a/45182852/7691581) wich did not worked too. – Anderson Gabriel Ferreira Apr 19 '23 at 01:53
  • You shouldn't have to notify the adapter of the change because the UI is already changed by the time the listener is called, and when the item scrolls off the screen and back on, then `onBindViewHolder` will be called again for the item anyway. Notifying is to force `onBindViewHolder` to get called on the item, but it's not necessary if it has already been bound and the check box was manually changed. – Tenfour04 Apr 19 '23 at 02:01
  • @Abdo21 That edit is completely unnecessary. It does exactly the same thing as the OP's code, except more verbose. – Tenfour04 Apr 19 '23 at 02:30
0

I have a theory about what's going wrong, but I didn't test it. It has to do with this code:

holder.checkBox.isChecked = item.isSelected
holder.checkBox.setOnCheckedChangeListener {_, isChecked ->
    item.isSelected = isChecked
    holder.checkBox.isChecked = isChecked
}

The first time you bind a view holder, it has this change listener added to it. The change listener is capturing a reference to the current item.

When the view holder scrolls off the screen and then back onto the screen, the old listener is still attached and capturing the old item. So when you call holder.checkBox.isChecked = item.isSelected, you might be triggering the old listener, which is mutating the old item to the wrong state.

So to fix this, set your listener before you update the check box state. You can also remove the useless holder.checkBox.isChecked = isChecked line. The check box state is automatically updated to the new state before the listener is called.

Fixed version of above code:

holder.checkBox.setOnCheckedChangeListener {_, isChecked ->
    item.isSelected = isChecked
}
holder.checkBox.isChecked = item.isSelected

An alternate solution is to set your click listener only once when the ViewHolder is created. This is more efficient anyway, because it doesn't have to allocate a new listener every time a view scrolls onto the screen. To do this, we find the current item at the time the listener is invoked by using absoluteAdapterPosition. Then it isn't capturing a specific item, so the same listener is usable at any time.

inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    val textView: TextView = itemView.findViewById(R.id.textView)
    val checkBox: CheckBox = itemView.findViewById(R.id.checkBox).apply {
        setOnCheckedChangeListner { _, isChecked ->
            items[absoluteAdapterPosition].isSelected = isChecked
        }
    }
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks for your help @Tenfour04! your theory was right, I have changed the order and called the listener before update and it worked. I just got curious about the second approuch, i need to create a variable `absoluteAdapterPosition`? Because if I just add it to the code, Android Studio returns me an `Unresolved reference`, I also tried with `adapterPositionAbsolute`. Thak you very much! – Anderson Gabriel Ferreira Apr 19 '23 at 19:26
  • It should be available: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ViewHolder#getAbsoluteAdapterPosition() Make sure you have the latest versions of the Jetpack libraries. – Tenfour04 Apr 19 '23 at 19:31