Home > Mobile >  Get ArrayIndexOutOfBoundsException: length=10; index=-1 when I try to undo a removal of the Recycler
Get ArrayIndexOutOfBoundsException: length=10; index=-1 when I try to undo a removal of the Recycler

Time:11-16

I have a list of the RecyclerView. And I made a swipe removal. Then I made a Snackbar in MainActivity to undo the removal:

val onSwipe = object : OnSwipe(this) {
    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        when (direction) {
            ItemTouchHelper.RIGHT -> {
                adapter.removeItem(
                    viewHolder.absoluteAdapterPosition
                )
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
                    .apply {
                        setAction("Undo") {
                            adapter.restoreItem(
                                viewHolder.absoluteAdapterPosition)
                        }
                        show()
                    }
            }
        }
    }

}

Code in adapter:

fun removeItem(pos: Int) {
    listArray.removeAt(pos)
    notifyItemRemoved(pos)
    }

    fun restoreItem(pos: Int) {
        listArray.add(pos, listArray[pos])
        notifyItemInserted(pos)
 }

And when I make the undo operation, my app stops, and I see this in a Logcat:

java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
at java.util.ArrayList.get(ArrayList.java:439)
at com.example.databaselesson.recyclerView.ExpensesAdapter.restoreItem(ExpensesAdapter.kt:79)
at com.example.databaselesson.MainActivity2$onSwipe$1.onSwiped$lambda-1$lambda-0(MainActivity2.kt:391)
at com.example.databaselesson.MainActivity2$onSwipe$1.$r8$lambda$AhJR3pu-3ynwFvPp66LdaLyFdB0(Unknown Source:0)
at com.example.databaselesson.MainActivity2$onSwipe$1$$ExternalSyntheticLambda0.onClick(Unknown Source:4)

Please, help

If you need more code, please, write, and I will send you it

CodePudding user response:

When you delete the item and do notifyItemRemoved, the ViewHolder being used to display that item is removed from the list. Since it's not displaying anything, its absoluteAdapterPosition is set to NO_POSITION, or -1:

Returns int

The adapter position of the item from RecyclerView's perspective if it still exists in the adapter and bound to a valid item. NO_POSITION if item has been removed from the adapter, notifyDataSetChanged has been called after the last layout pass or the ViewHolder has already been recycled.

So when you tap your UNDO button, that viewholder is going to return -1, which is not a valid index for your data list!

You should probably store the actual position you're removing:

override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
    // get the position first, and store that value
    val position = viewHolder.absoluteAdapterPosition

    when (direction) {
        ItemTouchHelper.RIGHT -> {
            // using the position we stored
            adapter.removeItem(position)
            // you don't have to use apply here if you don't want - it's designed
            // to be chained (fluent interface where each call returns the Snackbar)
            Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
                // using that fixed position value again
                .setAction("Undo") { adapter.restoreItem(position) }
                .show()
        }
    }
}

This way you're removing a specific item position, and if the undo button is hit, you use the same position value to restore it. You're not relying on the state of the ViewHolder that was being used.


Also this:

fun restoreItem(pos: Int) {
    listArray.add(pos, listArray[pos])
    notifyItemInserted(pos)
}

doesn't seem to restore anything? It just inserts a copy of item pos at the same position. Since your removeItem actually deletes the item from the list, there's no way to get it back unless you store it somewhere. You could have a lastDeletedItem variable that you update in removeItem that restoreItem restores:

var lastDeletedItem: Item? = null

fun removeItem(pos: Int) {
    // store the deleted item
    lastDeletedItem = listArray[pos]
    listArray.removeAt(pos)
    notifyItemRemoved(pos)
}

fun restoreItem(pos: Int) {
    // restore the last thing that was deleted at this position
    lastDeletedItem?.let {
        listArray.add(pos, it)
        notifyItemInserted(pos)
    }
}

But then you have the item that was deleted in one place, and the position in another (the snackbar lambda) so you might want to just put them both together - store the lastDeletedPosition in removeItem and reference that in restoreItem (don't pass pos in), or make restoreItem take a pos and item and fetch the item in your swipe callback, when you store the current adapter position

CodePudding user response:

There are two issues here.

1st: Call viewHolder.absoluteAdapterPosition after notifyItemRemoved shall return -1

This match the exception in your Logcat since it is telling you that you are trying to get index=-1 from listArray.

val onSwipe = object : OnSwipe(this) {
    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        when (direction) {
            ItemTouchHelper.RIGHT -> {
                adapter.removeItem(
                    viewHolder.absoluteAdapterPosition //<==Let's say position return 8
                )
            Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
                    .apply {
                        setAction("Undo") {
                            adapter.restoreItem(
                                viewHolder.absoluteAdapterPosition) //<==Deselected item so it shall return -1
                        }
                        show()
                    }
            }
        }
    }

}

2nd: You haven't cached the item object so it will fail to retrieve the correct data

// Assume that `listArray` = ["A", "B", "C"], `pos` = 1
fun removeItem(pos: Int) {
    listArray.removeAt(pos) = ["A", "C"]
    notifyItemRemoved(pos)
}

// `listArray` = ["A", "C"], `pos` = 1 (Assume you get the correct target pos)
fun restoreItem(pos: Int) { 
    listArray.add(pos, listArray[pos]) //`listArray[1]` = "C", listArray = ["A", "C", "C"]
    notifyItemInserted(pos)
 }

In order to resolve this, you will need to cache both the position and item object in onSwiped call

val onSwipe = object : OnSwipe(this) {
    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        when (direction) {
            ItemTouchHelper.RIGHT -> {
                val cachedPosition = viewHolder.absoluteAdapterPosition // cache position! 
                val cachedItem = listArray[cachedPosition] // cache item!
                adapter.removeItem(cachedPosition)

                Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
                    .apply {
                        setAction("Undo") {
                            adapter.restoreItem(cachedPosition, cachedItem) 
                     }
                        show()
                    }
            }
        }
    }

}



  • Related