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

Paging Library问题:PageKeyedDataSource未自动失效及滚动位置丢失

兄弟,我之前踩过一模一样的坑!当时用PageKeyedDataSource配合Room的BoundaryCallback,数据插库后DataSource死活不自动更新,手动调用invalidate()又导致RecyclerView滚动位置乱跳,折腾了好一阵才搞明白怎么回事。下面给你唠唠正确的实现思路:

问题根源先理清楚

Room内置的LimitOffsetDataSource是和InvalidationTracker深度绑定的,表一更新它会自动处理失效和刷新,但咱们自定义的PageKeyedDataSource没有这个待遇。直接调用invalidate()会让整个DataSource实例被销毁重建,RecyclerView的LayoutManager丢失之前的滚动状态,自然就跳位置了。

最优实现方案(分Paging版本)

1. 能升级Paging 3就直接升(最省心)

Paging 3的RemoteMediator天生就解决了这个问题——它会自动监听数据库变化,在数据插入后触发新的分页请求,而且内部已经处理了滚动位置的保留,不用咱们自己折腾。如果项目允许升级,这是最优解。

2. 坚持用Paging 2的话,这么搞

核心思路:不要全量invalidate,只做局部加载/更新

步骤一:让BoundaryCallback和DataSource联动,插数据后触发新页加载

在你的BoundaryCallback里持有PageKeyedDataSource的引用,当从服务器拉取数据插入数据库完成后,直接通知DataSource加载下一页,而不是让它整个失效。比如:

class StatusBoundaryCallback(private val dataSource: PageKeyedDataSource<Int, Status>) : PagedList.BoundaryCallback<Status>() {
    override fun onItemAtEndLoaded(itemAtEnd: Status) {
        super.onItemAtEndLoaded(itemAtEnd)
        // 假设你的下一页key是当前最后一条的id+1或者页码+1
        val nextPageKey = itemAtEnd.id + 1
        // 直接调用DataSource的loadAfter方法加载新页
        dataSource.loadAfter(
            LoadParams(nextPageKey, PAGE_SIZE),
            object : PageKeyedDataSource.LoadCallback<Int, Status>() {
                override fun onResult(data: MutableList<Status>, adjacentPageKey: Int?) {
                    // 这里不用手动更新Adapter,Paging会自动处理
                }
            }
        )
    }
}

步骤二:用InvalidationTracker监听数据库,但只做增量判断

如果需要监听数据库的其他更新(比如本地修改了数据),不要直接invalidate,而是在Observer里判断是否有新数据,再触发加载:

// 在你的ViewModel或者Repository里
val observer = object : InvalidationTracker.Observer("statuses") {
    private var lastCount = 0

    override fun onInvalidated(tables: MutableSet<String>) {
        // 先查最新的数据总量
        val currentCount = statusDao.getStatusCount()
        if (currentCount > lastCount) {
            // 有新数据,通知DataSource加载
            dataSource?.loadAfter(
                LoadParams(currentLastPageKey, PAGE_SIZE),
                object : PageKeyedDataSource.LoadCallback<Int, Status>() {
                    override fun onResult(data: MutableList<Status>, adjacentPageKey: Int?) {
                        lastCount = currentCount
                        currentLastPageKey = adjacentPageKey ?: currentLastPageKey
                    }
                }
            )
        }
    }
}
// 给Room数据库添加观察者
appDatabase.invalidationTracker.addObserver(observer)

步骤三:万不得已要invalidate时,保存并恢复滚动状态

如果必须全量刷新(比如数据被批量删除/修改),那一定要先保存RecyclerView的滚动状态,等新数据加载完成后再恢复:

// 保存状态
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val scrollState = layoutManager.onSaveInstanceState()
val lastVisiblePos = layoutManager.findLastCompletelyVisibleItemPosition()

// 调用invalidate
dataSource.invalidate()

// 新数据加载完成后恢复
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    override fun onChanged() {
        super.onChanged()
        scrollState?.let { layoutManager.onRestoreInstanceState(it) }
        // 或者直接滚动到之前的位置
        if (lastVisiblePos != -1) {
            layoutManager.scrollToPosition(lastVisiblePos)
        }
        // 记得注销观察者,避免内存泄漏
        adapter.unregisterAdapterDataObserver(this)
    }
})
最后再划个重点

尽量避免调用invalidate(),因为它会破坏Paging的分页状态。优先通过BoundaryCallback和DataSource的联动来加载新数据,或者用Paging 3的RemoteMediator从根源解决问题。

内容的提问来源于stack exchange,提问作者Sinyuk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:56:45