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
相关产品推荐
相关产品推荐

