Paging Library问题:PageKeyedDataSource未自动失效及滚动位置丢失
兄弟,我之前踩过一模一样的坑!当时用PageKeyedDataSource配合Room的BoundaryCallback,数据插库后DataSource死活不自动更新,手动调用invalidate()又导致RecyclerView滚动位置乱跳,折腾了好一阵才搞明白怎么回事。下面给你唠唠正确的实现思路:
Room内置的LimitOffsetDataSource是和InvalidationTracker深度绑定的,表一更新它会自动处理失效和刷新,但咱们自定义的PageKeyedDataSource没有这个待遇。直接调用invalidate()会让整个DataSource实例被销毁重建,RecyclerView的LayoutManager丢失之前的滚动状态,自然就跳位置了。
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

