使用自定义DataSource的Android Paging Library引发RecyclerView偏移问题
我之前做带日期分组的待办列表时,也踩过几乎一模一样的坑——用Paging Library搭配Room存数据,自定义DataSource插入日期头部,结果更新数据库后就出现RecyclerView条目偏移、重复,顶部还冒空占位,查来查去发现DataSource返回的数据没问题,但Adapter的getItem()要么偏移要么返回null,最后定位到PagedStorage里的mLoadingNullCount(这个确实没官方文档,属于内部实现的坑)。
结合我踩坑的经验,给你几个针对性的解决方案:
1. 把头部逻辑移到Adapter层(最稳妥的方案)
Paging的设计理念是DataSource只负责加载真实业务数据(也就是你的Ukol),头部、分隔符这类UI相关的逻辑应该交给Adapter处理。这样能彻底避开Paging内部索引计算和虚拟条目(头部)的冲突:
- 让你的DataSource只返回Ukol列表,不要混合Header对象;
- 在Adapter里,提前计算好每个Ukol对应的日期分组,通过
getItemViewType判断当前位置是头部还是任务项; - 重写
getItemCount,等于真实任务数 + 头部的数量; - 在
onBindViewHolder里,根据当前位置计算对应的真实任务索引(减去前面的头部数量),再从PagedList里取数据。
这种方式不仅能解决当前的偏移问题,后续维护也更省心,不会和Paging的加载逻辑纠缠。
2. 修复自定义DataSource的索引映射(如果坚持在DataSource里处理头部)
如果你一定要在DataSource里混合返回Header和Ukol,那必须严格对齐真实数据和虚拟条目的索引:
- 在
loadInitial方法中,调用onResult时,position参数要传真实数据的起始索引(也就是减去你插入的头部数量后的位置),而不是包含头部的总偏移; - 确保
loadBefore和loadAfter方法里的索引转换正确,比如加载更多时,要把Paging请求的位置转换成对应真实数据的位置,再去数据库查询; - 检查
getKey方法是否能正确返回条目的键值,Paging靠这个来定位加载位置,一旦键值和位置对应错了,就会出现重复或偏移。
举个loadInitial的示例:
// 从数据库加载真实的Ukol列表 val ukols = loadUkolsFromRoom(requestedStart, requestedLoadSize) // 插入日期头部 val itemsWithHeaders = insertDateHeaders(ukols) // 计算真实数据的起始位置:请求的起始位置 - 前面插入的头部数量 val realStartPosition = requestedStart - countOfHeadersBeforeFirstUkol // 调用onResult时,position传真实起始位置,totalCount传真实Ukol的总数 onResult(itemsWithHeaders, realStartPosition, ukols.size)
3. 检查Adapter的getItem和数据绑定逻辑
从你提供的Adapter代码来看,要确保getItem没有错误的偏移计算:
- 如果是Adapter层处理头部,当位置是头部时直接返回对应的Header数据;当是任务项时,用
position - 已出现的头部数量去PagedList里取Ukol; - 避免直接对PagedList的索引做无依据的加减,否则很容易返回null或者错误的条目,进而触发
mLoadingNullCount的问题。
另外,还要确认submitList在数据库更新时是否正确提交了新的PagedList,有没有重复提交或者提交空列表的情况。
4. 验证Room的DataSource配置
最后,检查Room侧的配置是否正确:
- DAO的查询语句是否按日期正确排序(这是分组的基础,排序乱了头部肯定会错);
- 当数据库数据变化时,Room的DataSource是否正确触发了
invalidate(),让Paging重新加载数据; - 避免用不稳定的排序条件(比如没有主键的排序),否则数据顺序会随机变化,导致Paging的索引混乱。
总的来说,把头部逻辑移到Adapter层是最省心的方案,能避开大部分Paging内部的坑。如果一定要在DataSource里处理,那必须把索引映射的每一步都理清楚,确保和Paging的加载逻辑对齐。
内容的提问来源于stack exchange,提问作者Vít Skalický

