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

RecyclerView中多倒计时器同时结束删除Item崩溃问题求助

嘿,这个问题我太熟了!多个倒计时同时结束删除RecyclerView Item时触发IndexOutOfBoundsException,本质上是因为数据源和视图位置不同步,加上多倒计时回调的并发触发导致索引混乱。我来给你拆解解决方案:

核心问题分析

你遇到的坑在于:单个Item删除时,position是准确的,但多个Item同时结束时,第一个Item删除后,后面的Item在数据源里的索引已经移位了,可你还在用原来的position去删除,自然就越界了。另外,RecyclerView的ViewHolder复用机制也会让旧的倒计时回调“乱跑”,进一步加剧问题。

具体解决方案

1. 用唯一ID绑定倒计时器,抛弃position依赖

别在ViewHolder里直接存CountDownTimer,而是用一个HashMap把每个数据项的唯一ID和对应的倒计时器绑定起来。这样不管Item位置怎么变,都能通过ID找到正确的倒计时,不会因为复用ViewHolder搞混。

2. 安全删除Item:基于ID找真实索引

在倒计时结束的回调里,绝对不要直接用绑定ViewHolder时的position,而是通过数据项的ID遍历数据源,找到它当前的真实索引,再执行删除和视图更新操作。而且要确保这个操作在主线程执行(CountDownTimer的回调本身是主线程,但批量操作时更稳妥)。

3. 回收ViewHolder时清理倒计时,防止内存泄漏

当ViewHolder被RecyclerView回收时,一定要取消对应的倒计时并从Map里移除,不然旧的倒计时还会继续跑,甚至触发错误的删除操作。

完整示例代码

假设你的数据模型带有唯一ID:

// 数据模型
public class TimerItem {
    public String uniqueId; // 每个Item的唯一标识,比如UUID生成
    public long remainingTime; // 剩余毫秒数
    // 其他业务字段
}

适配器的关键实现:

public class TimerAdapter extends RecyclerView.Adapter<TimerAdapter.TimerViewHolder> {
    private List<TimerItem> mDataList;
    private final Map<String, CountDownTimer> mTimerMap = new HashMap<>(); // ID绑定倒计时

    public TimerAdapter(List<TimerItem> dataList) {
        mDataList = dataList;
    }

    @NonNull
    @Override
    public TimerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_timer_layout, parent, false);
        return new TimerViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull TimerViewHolder holder, int position) {
        TimerItem currentItem = mDataList.get(position);
        
        // 先取消该ID对应的旧倒计时(防止复用ViewHolder时旧回调乱跑)
        if (mTimerMap.containsKey(currentItem.uniqueId)) {
            mTimerMap.get(currentItem.uniqueId).cancel();
        }

        // 创建新的倒计时
        CountDownTimer timer = new CountDownTimer(currentItem.remainingTime, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                currentItem.remainingTime = millisUntilFinished;
                holder.countdownTv.setText(millisUntilFinished / 1000 + "秒");
            }

            @Override
            public void onFinish() {
                // 用ID触发删除,而非position
                removeItemById(currentItem.uniqueId);
            }
        };

        mTimerMap.put(currentItem.uniqueId, timer);
        timer.start();
        holder.countdownTv.setText(currentItem.remainingTime / 1000 + "秒");
    }

    @Override
    public void onViewRecycled(@NonNull TimerViewHolder holder) {
        super.onViewRecycled(holder);
        // 回收ViewHolder时清理倒计时
        int adapterPosition = holder.getAdapterPosition();
        if (adapterPosition != RecyclerView.NO_POSITION && adapterPosition < mDataList.size()) {
            TimerItem item = mDataList.get(adapterPosition);
            if (mTimerMap.containsKey(item.uniqueId)) {
                mTimerMap.get(item.uniqueId).cancel();
                mTimerMap.remove(item.uniqueId);
            }
        }
    }

    // 核心:通过ID找真实索引再删除
    private void removeItemById(String itemId) {
        // 确保在主线程操作(虽然CountDownTimer回调是主线程,但批量删除时更安全)
        new Handler(Looper.getMainLooper()).post(() -> {
            int realIndex = -1;
            // 遍历找到当前Item的真实索引
            for (int i = 0; i < mDataList.size(); i++) {
                if (mDataList.get(i).uniqueId.equals(itemId)) {
                    realIndex = i;
                    break;
                }
            }
            // 找到索引后再执行删除和更新
            if (realIndex != -1) {
                mDataList.remove(realIndex);
                mTimerMap.remove(itemId);
                notifyItemRemoved(realIndex);
                // 可选:如果后面的Item有依赖位置的UI,调用这个更新范围
                // notifyItemRangeChanged(realIndex, getItemCount() - realIndex);
            }
        });
    }

    @Override
    public int getItemCount() {
        return mDataList.size();
    }

    static class TimerViewHolder extends RecyclerView.ViewHolder {
        TextView countdownTv;

        public TimerViewHolder(@NonNull View itemView) {
            super(itemView);
            countdownTv = itemView.findViewById(R.id.tv_countdown);
        }
    }
}
额外注意事项
  • 绝对不要用notifyDataSetChanged():它会刷新整个列表,不仅性能差,还会导致所有倒计时重启,用notifyItemRemoved()更高效准确。
  • 批量删除优化:如果有大量Item同时结束,可以先收集所有要删除的ID,然后从后往前删除索引(避免前面的索引移位),再批量调用notifyItemRemoved()。
  • 内存泄漏防护:除了回收ViewHolder时清理倒计时,在适配器销毁时也要遍历Map取消所有倒计时。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 03:50:34