添加额外/自定义列表项后FirestorePagingAdapter因键复用崩溃问题排查
看起来你遇到的核心问题是自定义SortedList插入标签后,和Paging3的内部位置管理逻辑冲突,导致重复触发相同的分页请求,最终抛出The same value was passed as the nextKey in two sequential Pages loaded from a PagingSource错误。我来帮你拆解问题并给出解决方案:
问题根源分析
你在onBindViewHolder里调用super.getItem(getPagingPosition(position))的操作是关键诱因:
super.getItem()会触发Paging3的预加载逻辑,它依赖内部维护的分页位置映射来请求数据。- 你自己维护的SortedList插入了额外标签项,导致
getPagingPosition计算的分页位置和Paging3内部预期的位置经常不匹配,重复请求同一个分页键(nextKey),触发了Paging3的防重复加载校验,直接崩溃。 - 另外,
getPagingPosition的线性遍历效率极低,频繁调用会加重性能负担,也容易出现计算误差。
官方推荐的解决方案:用Paging3原生API处理标签项
Paging3本身提供了insertSeparators API来处理这类"分隔符/标签"场景,完全不需要自己维护SortedList,既能避免位置冲突,又能简化代码:
步骤1:定义统一的列表项类型
先创建一个基类区分数据项和标签项(Java示例):
public abstract class ListItem { public static final int TYPE_MESSAGE = 0; public static final int TYPE_DATE_LABEL = 1; public abstract int getType(); } // 消息数据项 public class MessageItem extends ListItem { private Message message; public MessageItem(Message message) { this.message = message; } public Message getMessage() { return message; } @Override public int getType() { return TYPE_MESSAGE; } } // 日期标签项 public class DateLabelItem extends ListItem { private String dateText; public DateLabelItem(String dateText) { this.dateText = dateText; } public String getDateText() { return dateText; } @Override public int getType() { return TYPE_DATE_LABEL; } }
步骤2:用PagingData的insertSeparators插入标签
在ViewModel或数据层,将Firestore返回的分页数据转换为包含标签的PagingData<ListItem>:
// 先获取Firestore的分页数据 PagingData<Message> messagePagingData = firestorePagingSource.load(...); // 转换为ListItem并插入日期标签 PagingData<ListItem> finalPagingData = messagePagingData // 先把Message转为MessageItem .map(MessageItem::new) // 插入日期分隔标签 .insertSeparators((previousItem, nextItem) -> { if (nextItem == null) { // 最后一项之后不需要标签 return null; } if (previousItem == null) { // 第一项之前插入日期标签 return new DateLabelItem(formatDate(nextItem.getMessage().getTimestamp())); } // 判断前后项是否属于同一天,不同则插入标签 if (!isSameDay(previousItem.getMessage().getTimestamp(), nextItem.getMessage().getTimestamp())) { return new DateLabelItem(formatDate(nextItem.getMessage().getTimestamp())); } // 同一天则不插入标签 return null; });
步骤3:修改适配器处理混合类型
改用通用的PagerAdapter(而非FirestorePagingAdapter)来处理ListItem,重写getItemViewType区分两种项:
public class MessageListAdapter extends PagingDataAdapter<ListItem, RecyclerView.ViewHolder> { public MessageListAdapter(@NonNull DiffUtil.ItemCallback<ListItem> diffCallback) { super(diffCallback); } @Override public int getItemViewType(int position) { return getItem(position).getType(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (viewType == ListItem.TYPE_MESSAGE) { MessageBinding binding = MessageBinding.inflate(inflater, parent, false); return new MessageViewHolder(binding); } else { DateLabelBinding binding = DateLabelBinding.inflate(inflater, parent, false); return new DateLabelViewHolder(binding); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { ListItem item = getItem(position); if (item instanceof MessageItem) { ((MessageViewHolder) holder).bind(((MessageItem) item).getMessage()); } else if (item instanceof DateLabelItem) { ((DateLabelViewHolder) holder).bind(((DateLabelItem) item).getDateText()); } } // 定义两个ViewHolder类 static class MessageViewHolder extends RecyclerView.ViewHolder { private final MessageBinding binding; public MessageViewHolder(MessageBinding binding) { super(binding.getRoot()); this.binding = binding; } public void bind(Message message) { // 绑定消息数据 } } static class DateLabelViewHolder extends RecyclerView.ViewHolder { private final DateLabelBinding binding; public DateLabelViewHolder(DateLabelBinding binding) { super(binding.getRoot()); this.binding = binding; } public void bind(String dateText) { binding.dateLabel.setText(dateText); } } }
为什么这个方案能解决你的问题?
- 完全由Paging3管理位置:所有项(包括标签)都在
PagingData中统一处理,Paging3内部的分页位置映射不会被破坏,不会出现重复请求同一分页键的情况。 - 避免手动位置计算:不需要再写
getPagingPosition这种容易出错的逻辑,性能也更优。 - 符合Paging3设计规范:
insertSeparators是官方专门为分隔符场景提供的API,稳定性和兼容性更好。
针对你现有代码的临时修复(如果不想重构)
如果暂时不想大规模重构,至少要移除onBindViewHolder中的super.getItem()调用——这个操作是触发分页键重复的直接原因。Paging3的预加载逻辑不需要你手动触发,它会自动根据RecyclerView的滚动位置加载数据。
修改后的onBindViewHolder:
@Override public void onBindViewHolder(@NonNull RecyclerViewHolder<T, ? extends ViewBinding> holder, int position) { // 移除super.getItem()调用,避免触发错误的预加载 holder.bind(getList().get(position)); }
不过这个临时方案只是治标,长期来看还是推荐用官方API重构,避免后续出现更多位置相关的bug。
内容的提问来源于stack exchange,提问作者Avinta

