Paging 3 - 如何在 PagingDataAdapter 完成刷新并且 DiffUtil 完成差异后滚动到 RecyclerView 的顶部?
Posted
技术标签:
【中文标题】Paging 3 - 如何在 PagingDataAdapter 完成刷新并且 DiffUtil 完成差异后滚动到 RecyclerView 的顶部?【英文标题】:Paging 3 - How to scroll to top of RecyclerView after PagingDataAdapter has finished refreshing AND DiffUtil has finished diffing? 【发布时间】:2021-04-29 03:26:15 【问题描述】:我正在使用带有 RemoteMediator 的 Paging 3,它在从网络获取新数据时显示缓存数据。
当我刷新我的PagingDataAdapter
(通过调用refresh()
)时,我希望我的 RecyclerView 在刷新完成后滚动到顶部。在codelabs 中,他们尝试通过loadStateFlow
通过以下方式处理此问题:
lifecycleScope.launch
adapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy it.refresh
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter it.refresh is LoadState.NotLoading
.collect binding.list.scrollToPosition(0)
这确实会向上滚动,但 在 DiffUtil 完成之前。这意味着如果顶部确实插入了新数据,则 RecyclerView 不会一直向上滚动。
我知道 RecyclerView 适配器有一个 AdapterDataObserver
回调,当 DiffUtil 完成比较时,我们可以在其中得到通知。但这会导致适配器的PREPEND
和APPEND
加载状态的各种竞争条件,这也会导致 DiffUtil 运行(但这里我们不想滚动到顶部)。
一种可行的解决方案是将PagingData.empty()
传递给PagingDataAdapter
并重新运行相同的查询(仅调用refresh
将不起作用,因为PagingData
现在是空的并且没有什么可刷新的)但我更愿意让我的旧数据保持可见,直到我知道刷新确实成功了。
【问题讨论】:
你找到解决办法了吗? @P1NG2WIN 不,我现在使用 300 毫秒的延迟,这有点糟糕 :(我会创建关于它的问题 @P1NG2WIN 是您造成的问题吗?如果是的话,你能把它链接在这里吗? github.com/googlecodelabs/android-paging/issues/149 【参考方案1】:在某些情况下,例如搜索静态内容,我们可以在 areItemsTheSame
或 DiffUtil.ItemCallback
中返回 false
作为解决方法。我也用它来改变排序属性。
【讨论】:
【参考方案2】:看看代码是否刷新了loadtype的条件。
repoDatabase.withTransaction
// clear all tables in the database
if (loadType == LoadType.REFRESH)
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = repos.map
Log.e("RemoteKeys", "repoId: $it.id prevKey: $prevKey nextKey: $nextKey")
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
repoDatabase.remoteKeysDao().insertAll(keys)
repoDatabase.reposDao().insertAll(repos)
如果LoadType是刷新清除所有表,你应该删除条件。
if (loadType == LoadType.REFRESH)
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
【讨论】:
【参考方案3】:我已经设法从主题问题中改进基本代码 sn-p。
关键是监听CombinedLoadStates
内的非组合属性变体
viewLifecycleOwner.lifecycleScope.launchWhenCreated
adapter?.loadStateFlow
?.distinctUntilChanged old, new ->
old.mediator?.prepend?.endOfPaginationReached.isTrue() ==
new.mediator?.prepend?.endOfPaginationReached.isTrue()
?.filter it.refresh is LoadState.NotLoading
..
// next flow pipeline operators
isTrue
是布尔扩展 fun 的地方
fun Boolean?.isTrue() = this != null && this
所以这里的想法是跟踪mediator.prepend:endOfPagination
标志状态。当中介完成当前页面的分页加载部分时,他的prepend
状态不会改变(如果您在向下滚动后加载页面)。
解决方案适用于离线和在线模式。
如果您需要跟踪前置分页或双向分页,这是一个很好的起点,可以尝试使用另一个 CombinedLoadStates
属性 append
,refresh
,mediator
和 source
【讨论】:
【参考方案4】:@Florian 我可以确认我们不需要 postDelayed
使用 2021 年 7 月 21 日发布的版本 3.1.0-alpha03
滚动到顶部。
另外,我设法进一步过滤了loadStateFlow
集合,因此它不会阻止StateRestorationPolicy.PREVENT_WHEN_EMPTY
根据@Alexandr 的答案工作。我的解决方案是:
到我写这篇文章的时候,最新版本的 Paging3 是3.1.0-alpha03
所以导入:
androidx.paging:paging-runtime-ktx:3.1.0-alpha03
然后将您的适配器的恢复策略设置如下:
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
如果您对上述更改有编译错误,请确保您使用的是至少 1.2.0-alpha02 版本的 RecyclerView。上面的任何版本也不错:
androidx.recyclerview:recyclerview:1.2.0-alpha02
然后使用过滤后的loadStateFlow
仅在您刷新页面并且列表中添加项目时才将列表滚动到顶部:
viewLifecycleOwner.lifecycleScope.launch
challengesAdapter.loadStateFlow
.distinctUntilChanged old, new ->
old.mediator?.prepend?.endOfPaginationReached.isTrue() ==
new.mediator?.prepend?.endOfPaginationReached.isTrue()
.filter it.refresh is LoadState.NotLoading && it.prepend.endOfPaginationReached && !it.append.endOfPaginationReached
.collect
mBinding.fragmentChallengesByLocationList.scrollToPosition(0)
GitHub 讨论可以在这里找到:https://github.com/googlecodelabs/android-paging/issues/149
【讨论】:
【参考方案5】:关注https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter
val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>()
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
// User ID serves as unique ID
oldItem.userId == newItem.userId
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
// Compare full contents (note: Java users should call .equals())
oldItem == newItem
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder
return UserViewHolder.create(parent)
override fun onBindViewHolder(holder: UserViewHolder, position: Int)
val repoItem = getItem(position)
// Note that item may be null, ViewHolder must support binding null item as placeholder
holder.bind(repoItem)
【讨论】:
【参考方案6】:adapter.refresh()
lifecycleScope.launch
adapter.loadStateFlow
.collect
binding.recycleView.smoothScrollToPosition(0)
【讨论】:
以上是关于Paging 3 - 如何在 PagingDataAdapter 完成刷新并且 DiffUtil 完成差异后滚动到 RecyclerView 的顶部?的主要内容,如果未能解决你的问题,请参考以下文章
Paging 3 - 如何在 PagingDataAdapter 完成刷新并且 DiffUtil 完成差异后滚动到 RecyclerView 的顶部?
如何在 PagingSource LoadResult Page Paging 3 中使用 itemAfter 或 itemBefore