kotlincheckboxandroid-recyclerviewvisibilitynotifydatasetchanged

RecyclerView and notifyDataSetChanged LongClick mismatch


I'm having a weird problem with notifyDataSetChanged() in my Recycler Adapter. If I keep 5 items in an array the code works fine and I can check the checkbox at the item I LongClick, but when I add 5 items or more to the array other checkboxes get checked in my list.

I am using a boolean to toggle between VISIBLE and GONE on the checkboxes when the user LongClicks as well.

Here is my code:

class RecyclerAdapter(private val listActivity: ListActivity) : RecyclerView.Adapter<RecyclerAdapter.Holder>() {

    lateinit var binding: ActivityListItemRowBinding
    var checkboxesVisibility = false
    val dummyArrayWorks = arrayOf("000", "111", "222", "333", "444")
    val dummyArrayFails = arrayOf("000", "111", "222", "333", "444", "555")

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        binding = ActivityListItemRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }

    override fun getItemCount(): Int = dummyArrayFails.size

    @SuppressLint("NotifyDataSetChanged")
    override fun onBindViewHolder(holder: Holder, position: Int) {

        val item = dummyArrayFails[position]
        
        holder.binding.checkbox.visibility = if (checkboxesVisibility) VISIBLE else GONE
        holder.bindItem(item)

        holder.itemView.setOnLongClickListener {
            if (!checkboxesVisibility) {
                checkboxesVisibility = true
                holder.binding.checkbox.isChecked = true
                notifyDataSetChanged()
                true
            } else {
                false
            }
        }
        holder.itemView.setOnClickListener {
            if (!checkboxesVisibility) {
                //Some other unrelated code
            } else {
                holder.binding.checkbox.isChecked = !holder.binding.checkbox.isChecked
                notifyDataSetChanged()
            }
        }
    }

    class Holder(internal val binding: ActivityListItemRowBinding) : RecyclerView.ViewHolder(binding.root) {

        var item = String()

        fun bindItem(item: String) {
            this.item = item
            binding.itemPlaceHolder.text = item
        }
    }
}

I should add that when I remove the toggle for the checkboxes, and just show the checkboxes on first load, the clicks match the checkmarks without a problem.

Does anybody have any idea of what is going on? All help will be much appreciated!


Solution

  • The problem is you're holding your checked state in the ViewHolder itself - you're toggling its checkbox on and off depending on clicks, right?

    The way a RecyclerView works is that instead of having a ViewHolder for every single item (like a ListView does), it only creates a handful of them - enough for what's on screen and a few more for scrolling - and recycles those, using them to display different items.

    That's what onBindViewHolder is about - when it needs to display the item at position, it hands you a ViewHolder from its pool and says here you go, use that to display this item's details. This is where you do things like setting text, changing images, and setting things like checkbox state to reflect that particular item.


    What you're doing is you're not storing the item's state anywhere, you're just setting the checkbox on the view holder. So if you check it, every item that happens to be displayed in that reusable holder object will have its box ticked. That's why you're seeing it pop up on other items - that checked state has nothing to do with the items themselves, just which view holder they all happen to use because of their position in the list.

    So instead, you need to keep their checked state somewhere - it could be as simple as a boolean array that matches the length of your item list. Then you just set and get from that when binding your data (displaying it). Working with what you've got:

    // all default to false
    val itemChecked = BooleanArray(items.size)
    
    override fun onBindViewHolder(holder: Holder, position: Int) {
        ...
        // when displaying the data, refer to the checked state we're holding
        holder.binding.checkbox.checked = itemChecked[position]
        ...
        holder.itemView.setOnLongClickListener {
            ...
            // when checking the box, update our checked state
            // since we're calling notifyDataSetChanged, the item will be redisplayed
            // and onBindViewHolder will be called again (which sets the checkbox)
            itemChecked[position] = true
            // notifyItemChanged(position) is better here btw, just refreshes this one
            notifyDataSetChanged()
            ...
        }
    }