androidandroid-recyclerviewandroid-diffutilsandroid-listadapter

DiffUtil.ItemCallback doesn't update item position (after a deleting)


I use a binding for a ListAdapter with the definition of a DiffUtil.ItemCallback. When deleting items (at least 2) I have an IndexOutOfBoundsException. The update of the list works (the number of elements is indeed N-1 after deletion) but not the position of the item, which is kept is the call. The exception's therefore thrown when calling getItem(position) (in the onBindViewHolder). NB: A log of getItemCount() just before the getItem(position) shows that the list contains N-1 elements. I created a small repo: https://github.com/jeremy-giles/DiffListAdapterTest (with a same configuration to my project) which reproduces the problem.

ItemAdapter class

class ItemAdapter(
    var listener: ListAdapterListener) : DataBindingAdapter<Item>(DiffCallback()) {

    class DiffCallback : DiffUtil.ItemCallback<Item>() {

        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
           return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
           return oldItem == newItem
        }
    }

    override fun getItemViewType(position: Int) = R.layout.recycler_item

    override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
        super.onBindViewHolder(holder, position)

        holder.itemView.tv_position.text = "Pos: $position"

        holder.itemView.setOnLongClickListener {
            Timber.d("List item count: ${itemCount}, position: $position")
            listener.onLongViewClick(getItem(position), position)
        }
    }

    interface ListAdapterListener {
        fun onLongViewClick(item: Item, position: Int) : Boolean
    }
}

BindingUtils classes

abstract class DataBindingAdapter<T>(diffCallback: DiffUtil.ItemCallback<T>) :
    ListAdapter<T, DataBindingViewHolder<T>>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder<T> {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)

        return DataBindingViewHolder(binding)
    }

    override fun onBindViewHolder(holder: DataBindingViewHolder<T>, position: Int) {
        holder.bind(getItem(position))
    }
}

class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(item: T) {
        binding.setVariable(BR.item, item)
        binding.executePendingBindings()
    }
}

And in my MainActivity class I use a LiveData to update the recyclerView

itemViewModel.getListObserver().observe(this, Observer {
        Timber.d("List Observer, items count ${it.size}")
        itemAdapter.submitList(it.toList())
    })

Solution

  • In your onBindViewHolder update usage of 'position' to 'holder.getAdapterPosition()':

    override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
            super.onBindViewHolder(holder, position)
    
            holder.itemView.tv_position.text = "Pos: $position"
    
            holder.itemView.setOnLongClickListener {
                Timber.d("List item count: ${itemCount}, position: $position")
                listener.onLongViewClick(getItem(holder.getAdapterPosition()), holder.getAdapterPosition())
            }
        }