简体   繁体   中英

Paging 3 - How to scroll to top of RecyclerView after PagingDataAdapter has finished refreshing AND DiffUtil has finished diffing?

I'm using Paging 3 with RemoteMediator that shows cached data while fetching new data from the network. When I refresh my PagingDataAdapter (by calling refresh() on it) I want my RecyclerView to scroll to the top after the refresh is done. In the codelabs they try to handle this via the loadStateFlow the following way:

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) }
    }

This indeed does scroll up, but before DiffUtil has finished. This means that if there is actually new data inserted at the top, the RecyclerView will not scroll all the way up.

I know that RecyclerView adapters have an AdapterDataObserver callback where we can get notified when DiffUtil has finished diffing. But this will cause all kinds of race conditions with PREPEND and APPEND loading states of the adapter which also cause DiffUtil to run (but here we don't want to scroll to the top).

One solution that would work would be to pass PagingData.empty() to the PagingDataAdapter and rerun the same query (just calling refresh won't work because the PagingData is now empty and there is nothing to refresh) but I would prefer to keep my old data visible until I know that refresh actually succeeded.

In cases, such as searching a static content, we can return false inside areItemsTheSame of DiffUtil.ItemCallback as a workaround. I use this also for changing sorting property.

Take a look on the code the condition if loadtype is refreshed.

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)
        }

You should delete the condition if LoadType is refresh clear all the tables.

if (loadType == LoadType.REFRESH) {
            repoDatabase.remoteKeysDao().clearRemoteKeys()
            repoDatabase.reposDao().clearRepos()
        }
 

I've managed the way to improve base code snippet from the topic question. The key is to listen non-combined variants of properties inside 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
        }

where isTrue is Boolean extension fun

fun Boolean?.isTrue() = this != null && this

So the idea here is to track mediator.prepend:endOfPagination flag states. When mediator has completed his part of paging load for the current page, his prepend state will not change (in case if you are loading pages after scroll down). Solution works well both in offline & online modes.

If you need to track prepend paging or paging in both directions it is a good starting point to play around with the another CombinedLoadStates properties append , refresh , mediator and source

@Florian I can confirm we don't need the postDelayed to scroll to top using version 3.1.0-alpha03 released on 21/07/2021. Also, I managed to make further filter the loadStateFlow collection so it doesn't prevent StateRestorationPolicy.PREVENT_WHEN_EMPTY to work based on @Alexandr answer. My solution is:

By the time I am writing this, the latest version of Paging3 is 3.1.0-alpha03 so import:

androidx.paging:paging-runtime-ktx:3.1.0-alpha03

Then set the restoration policy of your adapter as following:

adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY

If you have compilation error for the above mentioned change, make sure you are using at least version 1.2.0-alpha02 of RecyclerView. Any version above that is also good:

androidx.recyclerview:recyclerview:1.2.0-alpha02

Then use the filtered loadStateFlow to scroll the list to top only when you refresh the page and items are prepended in the list:

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)
                }
        }

The GitHub discussion can be found here: https://github.com/googlecodelabs/android-paging/issues/149

Follow 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)
}

}

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM