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

11

我正在使用带有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
@P1NG2WIN 不,我现在使用的是300毫秒的延迟,有点糟糕。 - Florian Walther
我会创建一个问题来处理它。 - P1NG2WIN
@P1NG2WIN,你创建了这个问题吗?如果是的话,能否在这里提供链接? - Florian Walther
https://github.com/googlecodelabs/android-paging/issues/149 - P1NG2WIN
@P1NG2WIN 和 Florian,请检查一下我的答案 :) - John Santos
6个回答

4

@Florian 我可以确认,在发布于2021年07月21日的版本3.1.0-alpha03中,我们不需要使用postDelayed来滚动到顶部。 此外,我成功地进一步过滤了loadStateFlow集合,使其不会阻止基于@Alexandr答案的StateRestorationPolicy.PREVENT_WHEN_EMPTY工作。 我的解决方案如下:

截至我撰写本文时,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

3
在某些情况下,比如搜索静态内容时,我们可以在 DiffUtil.ItemCallbackareItemsTheSame 方法中返回 false 作为解决方法。我也用这种方法来改变排序属性。

0

看看代码中的条件,如果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为refresh,则清除所有表格。
if (loadType == LoadType.REFRESH) {
            repoDatabase.remoteKeysDao().clearRemoteKeys()
            repoDatabase.reposDao().clearRepos()
        }
 

0

我已经成功地改进了主题问题中的基础代码片段。

关键是要监听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 Boolean?.isTrue() = this != null && this

这里的想法是追踪 mediator.prepend:endOfPagination 标志状态。当中介者完成当前页面的分页加载后,他的 prepend 状态不会改变(如果您在向下滚动后加载页面)。 该解决方案可以在离线和在线模式下很好地工作。

如果您需要跟踪前置分页或双向分页,那么另一个 CombinedLoadStates 属性 appendrefreshmediatorsource 是一个很好的起点。


0
adapter.refresh()
lifecycleScope.launch {
    adapter.loadStateFlow
    .collect {                  
        binding.recycleView.smoothScrollToPosition(0)                           
    }
}

这会破坏以下附加操作。每次附加时,列表将滚动到顶部。 - Lorenzo Vincenzi

0
请点击 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)
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接