You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView删除含最后项的多选中项时抛出IndexOutOfBound异常

RecyclerView多选删除最后一项索引越界修复方案

问题核心原因

当前代码触发越界是四个逻辑错误共同导致的:

  • mSelectedItems没有去重逻辑,多次长按同一项会重复存入相同索引,删除时重复移除同位置项直接触发越界
  • sortedDescending()是返回新排序列表的非原地排序方法,原代码没有接收返回值,实际遍历的还是乱序的索引集合:先删靠前位置的项后列表长度缩短,原本靠后的索引(比如最后一项的索引)会大于当前列表最大下标,直接触发越界
  • 监听全部写在onBindViewHolder中,会随列表绑定反复注册,导致选中状态、删除逻辑重复触发,进一步加剧索引错乱;同时原多选逻辑写死了selectedPosition = position,根本没有实现选中/取消的状态切换,本质还是错误的单选逻辑。
  • 删除时同时调用notifyItemRemoved和notifyDataSetChanged属于冗余操作,会打断RecyclerView的位置修正流程,导致动画和索引计算异常。

可直接落地的修复代码

第一步:调整监听绑定位置,修正多选逻辑

把所有item点击、长按、删除按钮的监听从onBindViewHolder移到onCreateViewHolder中,避免重复绑定,同时用HashSet存选中索引自动去重,实现真正的多选切换:

// 选中索引用HashSet自动去重,避免重复存储
val mSelectedItems = HashSet<Int>()
val deletList: ArrayList<String> = ArrayList()
var items: ArrayList<GetListModel> = arrayListOf()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OutagesViewHolder {
    val itemView = LayoutInflater.from(parent.context)
        .inflate(R.layout.chats_list_item, parent, false)
    val holder = OutagesViewHolder(itemView)

    // Item长按切换选中状态
    holder.itemView.setOnLongClickListener {
        val currentPos = holder.adapterPosition
        if (currentPos == RecyclerView.NO_POSITION) return@setOnLongClickListener true
        
        if (mSelectedItems.contains(currentPos)) {
            // 已选中则取消选中
            mSelectedItems.remove(currentPos)
            items[currentPos].id?.let { deletList.remove(it) }
            holder.itemView.setBackgroundColor(Color.parseColor("#00000000"))
        } else {
            // 未选中则加入选中集合
            mSelectedItems.add(currentPos)
            items[currentPos].id?.let { deletList.add(it) }
            holder.itemView.setBackgroundColor(Color.parseColor("#FFECECEC"))
        }
        notifyItemChanged(currentPos)
        true
    }

    // 删除按钮点击触发删除
    holder.deletebutton.setOnClickListener {
        removeSelectedItems()
    }

    return holder
}

override fun getItemCount(): Int = items.size

// 注意:如果没有多类型布局需求,直接删掉原代码里写的getItemId、getItemViewType重写,返回position的写法会导致列表复用异常
override fun onBindViewHolder(holder: OutagesViewHolder, position: Int) {
    // 这里只做数据绑定,不要注册任何点击监听
    val itemData = items[position]
    // 补充业务数据绑定逻辑,比如赋值文本、加载图片等
}

第二步:修正删除逻辑,从根源避免索引错位

倒序删除时必须接收排序后的新列表,同时增加边界校验做双重保险:

fun removeSelectedItems(){
    // 必须接收sortedDescending的返回值,才能拿到倒序排列的索引
    val sortedDeleteIndices = mSelectedItems.sortedDescending()
    sortedDeleteIndices.forEach { index ->
        // 双重边界校验,极端场景下也不会触发越界
        if (index in 0 until items.size) {
            items.removeAt(index)
            notifyItemRemoved(index)
        }
    }
    // 删完统一刷新剩余item的位置,不需要调用notifyDataSetChanged全量刷新
    notifyItemRangeChanged(0, items.size)
    // 清空选中状态
    mSelectedItems.clear()
    deletList.clear()
}

额外注意点

  • 如果需要支持滚动后选中态不丢失,需要在onBindViewHolder里根据mSelectedItems是否包含当前position,主动设置item的背景状态,不要只在点击时修改
  • 不要在删除单个item后立刻调用notifyDataSetChanged,会让RecyclerView无法计算item动画,还会导致位置计算失效

内容的提问来源于stack exchange,提问作者Prateek Sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 04:06:21