javaandroidandroid-recyclerviewindexoutofboundsexceptionitemtouchhelper

How to Fix IndexOutOfBoundsException in RecyclerView Adapter's onRowMoved Function?


I'm facing an issue with the implementation of the onRowMoved function within my RecyclerView adapter.

This function is responsible for handling item movements in the RecyclerView when items are dragged and dropped.

However, I'm encountering a java.lang.IndexOutOfBoundsException.


My code is:


val touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {

    ...

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return imagesAdapter?.onRowMoved(viewHolder, target) ?: false
    }

})

fun onRowMoved(
    fromViewHolder: RecyclerView.ViewHolder,
    toViewHolder: RecyclerView.ViewHolder
): Boolean {
    val fromPosition = fromViewHolder.bindingAdapterPosition
    val toPosition = toViewHolder.bindingAdapterPosition
    val imagesSize = this.images.size

    if (fromPosition < imagesSize && toPosition < imagesSize) {
        if (fromPosition < toPosition) {
            for (i in fromPosition until toPosition) {
                Collections.swap(this.images, i, i + 1)
            }
            notifyItemMoved(fromPosition, toPosition)
        } else {
            for (i in fromPosition downTo toPosition + 1) {
                Collections.swap(this.images, i, i - 1)
            }
            notifyItemMoved(toPosition, fromPosition)
        }

        Handler().postDelayed({
            notifyDataSetChanged()
        }, 1000)
        return true
    }
    return false
}

Crash logs are following:

Fatal Exception: java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 8
       at jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
       at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
       at jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
       at java.util.Objects.checkIndex(Objects.java:359)
       at java.util.ArrayList.get(ArrayList.java:434)
       at java.util.Collections.swap(Collections.java:548)
       at com....newProduct.NewProductImagesAdapter.onRowMoved(NewProductImagesAdapter.kt:208)
       at com....newProduct.NewProduct$createImagesAdapter$touchHelper$1.onMove(NewProduct.kt:927)
       at androidx.recyclerview.widget.ItemTouchHelper.moveIfNecessary(ItemTouchHelper.java:891)
       at androidx.recyclerview.widget.ItemTouchHelper$2.onTouchEvent(ItemTouchHelper.java:390)
       at androidx.recyclerview.widget.RecyclerView.dispatchToOnItemTouchListeners(RecyclerView.java:3515)
       at androidx.recyclerview.widget.RecyclerView.onTouchEvent(RecyclerView.java:3713)
       at android.view.View.dispatchTouchEvent(View.java:14879)

Solution

  • It looks like you're using bindingAdapterPosition to get the adapter position of the ViewHolders, but keep in mind that bindingAdapterPosition can return NO_POSITION if the ViewHolder is not currently in the adapter. When you move an item, the adapter positions of the affected items might change.

    fun onRowMoved(
            fromViewHolder: RecyclerView.ViewHolder,
            toViewHolder: RecyclerView.ViewHolder
                    Boolean {
        val fromPosition = fromViewHolder.adapterPosition
        val toPosition = toViewHolder.adapterPosition
        val imagesSize = this.images.size
    
        if (fromPosition != RecyclerView.NO_POSITION && toPosition !=
                RecyclerView.NO_POSITION &&
                fromPosition < imagesSize && toPosition < imagesSize) {
            if (fromPosition < toPosition) {
                for (i in fromPosition until toPosition) {
                    Collections.swap(this.images, i, i + 1)
                }
                notifyItemMoved(fromPosition, toPosition)
            } else {
                for (i in fromPosition downTo toPosition + 1) {
                    Collections.swap(this.images, i, i - 1)
                }
                notifyItemMoved(fromPosition, toPosition)
            }
    
            Handler().postDelayed({
                    notifyDataSetChanged()
            }, 1000)
            return true
        }
        return false
    }