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 完成比较时,我们可以在其中得到通知。但这会导致适配器的PREPENDAPPEND 加载状态的各种竞争条件,这也会导致 DiffUtil 运行(但这里我们不想滚动到顶部)。

一种可行的解决方案是将PagingData.empty() 传递给PagingDataAdapter 并重新运行相同的查询(仅调用refresh 将不起作用,因为PagingData 现在是空的并且没有什么可刷新的)但我更愿意让我的旧数据保持可见,直到我知道刷新确实成功了。

【问题讨论】:

你找到解决办法了吗? @P1NG2WIN 不,我现在使用 300 毫秒的延迟,这有点糟糕 :(我会创建关于它的问题 @P1NG2WIN 是您造成的问题吗?如果是的话,你能把它链接在这里吗? github.com/googlecodelabs/android-paging/issues/149 【参考方案1】:

在某些情况下,例如搜索静态内容,我们可以在 areItemsTheSameDiffUtil.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,mediatorsource

【讨论】:

【参考方案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 的顶部?的主要内容,如果未能解决你的问题,请参考以下文章

获取数据并绑定到 UI | MAD Skills

Paging 3 - 如何在 PagingDataAdapter 完成刷新并且 DiffUtil 完成差异后滚动到 RecyclerView 的顶部?

如何在 Paging 3 中使用网络绑定资源?

如何使用 Paging 3 库更新单个项目

如何检查 Paging 3 库中的列表大小或空列表

如何在 PagingSource LoadResult Page Paging 3 中使用 itemAfter 或 itemBefore