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

Android中DiffUtil+ListUpdateCallback插入位置错误致越界问题

解决DiffUtil删除非末尾项后插入位置错误导致的IndexOutOfBoundsException问题

这个坑我之前踩过!先给你拆解问题根源,再一步步给解决方案:

核心原因:DiffUtil的操作序列是增量式的

DiffUtil生成的回调(比如onRemoved、onInserted)是按顺序应用到列表上的,每个回调的position参数都是基于「已经处理完前面所有操作后的当前列表状态」,而不是原始旧列表的位置。如果你的回调实现没有实时跟进列表的变化,或者DiffCallback的逻辑有误,就会出现位置计算偏差。

比如你的场景:旧列表是[A, B],删除索引0的A后添加3项,正确的操作序列应该是:

  1. onRemoved(0, 1) → 此时列表变为[B]
  2. onInserted(1, 3) → 在当前列表的末尾(索引1)插入3项

但你看到onInserted的position是2,说明要么DiffUtil计算出了错误的操作序列,要么你的回调处理逻辑用了错误的位置基准。

第一步:检查AnswersDiffCallback的实现是否正确

DiffUtil的判断逻辑完全依赖areItemsTheSame和areContentsTheSame,这两个方法写错了直接导致操作序列混乱:

正确的实现示例:

public class AnswersDiffCallback extends DiffUtil.Callback {
    private final List<Answer> oldList;
    private final List<Answer> newList;

    public AnswersDiffCallback(List<Answer> oldList, List<Answer> newList) {
        this.oldList = oldList;
        this.newList = newList;
    }

    @Override
    public int getOldListSize() {
        return oldList.size();
    }

    @Override
    public int getNewListSize() {
        return newList.size();
    }

    // 必须用唯一id判断是否是同一个项,这是DiffUtil判断增删改的核心
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        Answer oldAnswer = oldList.get(oldItemPosition);
        Answer newAnswer = newList.get(newItemPosition);
        // 注意处理id为null的情况,避免空指针
        return oldAnswer.getId() != null && oldAnswer.getId().equals(newAnswer.getId());
    }

    // 对比内容是否变化,只要是需要通知服务器的字段都要加进来
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        Answer old = oldList.get(oldItemPosition);
        Answer newAns = newList.get(newItemPosition);
        return old.getText().equals(newAns.getText())
                && old.getCount().equals(newAns.getCount())
                && old.getQuestionId().equals(newAns.getQuestionId());
    }
}

第二步:确保回调处理逻辑实时更新列表

不管你是用RecyclerView.Adapter自带的回调,还是自定义ListUpdateCallback,都必须在每个回调里实时更新本地数据集,否则后续操作的位置基准就会错误:

示例1:RecyclerView.Adapter的正确用法

// 计算差异
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AnswersDiffCallback(mOldAnswers, mNewAnswers));
// 先更新Adapter的数据集(必须在分发回调之前!)
mOldAnswers.clear();
mOldAnswers.addAll(mNewAnswers);
// 分发更新回调
diffResult.dispatchUpdatesTo(this);

示例2:自定义ListUpdateCallback的正确用法

List<Answer> localList = new ArrayList<>(oldAnswers);
ListUpdateCallback callback = new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
        // 这里的position是基于当前localList的状态,直接插入对应位置
        localList.addAll(position, newAnswers.subList(position, position + count));
        // 在这里通知服务器插入操作
    }

    @Override
    public void onRemoved(int position, int count) {
        // 移除localList对应位置的项
        localList.subList(position, position + count).clear();
        // 在这里通知服务器删除操作
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        Answer item = localList.remove(fromPosition);
        localList.add(toPosition, item);
        // 通知服务器移动操作(如果需要)
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        // 通知服务器更新操作
    }
};

// 计算并分发差异
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AnswersDiffCallback(oldAnswers, newAnswers));
diffResult.dispatchUpdatesTo(callback);

第三步:排查操作序列是否符合预期

如果上面两步都没问题,可以打印DiffUtil生成的操作序列,看看是否符合预期:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AnswersDiffCallback(oldAnswers, newAnswers));
// 打印所有操作
for (DiffUtil.DiffResult.DiffOp op : diffResult.getOperations()) {
    Log.d("DiffUtilDebug", "操作类型:" + op.getType() + ",位置:" + op.getPosition());
}

如果发现操作序列里出现了不必要的MOVE操作,那肯定是areItemsTheSame的逻辑有问题,比如误把不同id的项判断为相同项了。

总结

你遇到的IndexOutOfBoundsException本质是:要么DiffCallback的核心判断逻辑写错了,导致DiffUtil计算出错误的操作序列;要么回调处理时没有实时更新列表,用了原始旧列表的位置基准来处理后续操作。按照上面的步骤排查,应该能解决问题。

内容的提问来源于stack exchange,提问作者O-BL

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:23:55