Android中DiffUtil+ListUpdateCallback插入位置错误致越界问题
这个坑我之前踩过!先给你拆解问题根源,再一步步给解决方案:
核心原因:DiffUtil的操作序列是增量式的
DiffUtil生成的回调(比如onRemoved、onInserted)是按顺序应用到列表上的,每个回调的position参数都是基于「已经处理完前面所有操作后的当前列表状态」,而不是原始旧列表的位置。如果你的回调实现没有实时跟进列表的变化,或者DiffCallback的逻辑有误,就会出现位置计算偏差。
比如你的场景:旧列表是[A, B],删除索引0的A后添加3项,正确的操作序列应该是:
onRemoved(0, 1)→ 此时列表变为[B]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

