androidkotlinandroid-recyclerviewpaginationalgolia

Unexpected Automatic Scroll to Top in Android Paging 3 Recycler View After Loading Next Page


I'm currently integrating the Paging 3 library into my Android application, utilizing a RecyclerView to display data. While the basic implementation is in place, I'm encountering an unexpected behavior related to scrolling after loading the next page of data. Specifically, the RecyclerView is automatically scrolling to the top position after loading new data, which is not the intended behavior.

The setup involves using the PagingDataAdapter along with the RecyclerView as per Paging 3's guidelines. Pagination is working correctly, but the automatic scroll to the top after fetching and loading subsequent pages is proving to be problematic. The goal is to have users continue viewing their current position without being abruptly taken to the top of the list whenever new data is loaded.

The relevant portion of my code resembles the following:

mSSSPagination(
            searcher = productSearcher,
            pagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false),
            transformer = {
                it.deserialize(ProductAlgolia.serializer())
            }
        )


paginator?.liveData?.observe(viewLifecycleOwner) { pagingData ->
        plpAdapter?.submitData(lifecycle, pagingData)
    }

Paging Source Code Snippet

class SSSSearcherPagingSource<T : Any>(
private val searcher: SearcherForHits<out SearchParameters>,
private val transformer: (ResponseSearch.Hit) -> T
) : PagingSource<Int, T>() {

override fun getRefreshKey(state: PagingState<Int, T>): Int {
    return 0 // on refresh (for new query), start from the first page (number zero)
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
    return try {
        val pageNumber = params.key ?: 0
        searcher.query.page = pageNumber
        searcher.query.hitsPerPage = 20

        val response = search() ?: return emptyPage()
        val data = response.hits.map(transformer)
        val nextKey = if (pageNumber < response.nbPages) pageNumber + 1 else null
        LoadResult.Page(
            data = data,
            prevKey = null, // no paging backward
            nextKey = nextKey
        )
    } catch (exception: Exception) {
        Log.w("Paging search operation failed", exception)
        LoadResult.Error(exception)
    }
}

Pagination Issue Video

Pagination Issue

Update 1

Paginator Code

public fun <T : Any> Paginator(
    searcher: SearcherForHits<out SearchParameters>,
    pagingConfig: PagingConfig = PagingConfig(pageSize = 10),
    transformer: (ResponseSearch.Hit) -> T
): Paginator<T> = SearcherPaginator(
    pagingConfig = pagingConfig,
    pagingSourceFactory = { SearcherPagingSource(searcher, transformer) }
)

/**
 * A cold Flow of [PagingData], emits new instances of [PagingData] once they become invalidated.
 */
public val <T : Any> Paginator<T>.flow: Flow<PagingData<T>> get() = pager.flow

/**
 * A LiveData of [PagingData], which mirrors the stream provided by [flow], but exposes it as a LiveData.
 */
public val <T : Any> Paginator<T>.liveData: LiveData<PagingData<T>> get() = pager.liveData

Diff Util Code Snippet

object ProductDiffUtil : DiffUtil.ItemCallback<ProductAlgolia>() {

        override fun areItemsTheSame(oldItem: ProductAlgolia, newItem: ProductAlgolia): Boolean {
            return oldItem.objectID == newItem.objectID
        }

        override fun areContentsTheSame(oldItem: ProductAlgolia, newItem: ProductAlgolia): Boolean {
            return oldItem == newItem
        }
    }

Update 2

Load State Collection Snippet

private fun setLoadingStates() {
        viewLifecycleOwner.lifecycleScope.launchWhenResumed {
        plpAdapter?.loadStateFlow?.collectLatest { loadStates ->
            //Log.d("loadState", loadStates.toString())
            Log.d("productList", plpAdapter?.getProductAlgoliaList().toString())
            when (loadStates.refresh) {

                !is LoadState.Loading -> {
                        val productList = plpAdapter?.getProductList() ?: arrayListOf()
                        if (productList.isNotEmpty()) {
                            if (!isFromSearch) {
                                createCategoryTabs(productList, selectedCategoryId)
                            }
                            trackAlgoliaPLPView()
                            initMostUsedFilter()
                            trackMoengagePLPViewEvent(productList = productList)
                        } else {
                           noDataFound()
                        }

                        if (isFromSearch) {
                            val suggestion =
                                algoliaViewModel.suggestions.value?.peekContent()?.second
                            val query = algoliaViewModel.suggestions.value?.peekContent()?.first

                            trackMoEngageSearchEvent(
                                selectedQuery = suggestion?.query,
                                searchText = query,
                                productList.isEmpty()
                            )
                        }


                    hideShimmer()
                }

                is LoadState.Error -> {
                    hideShimmer()
                }

                else -> {

                }
            }
        }
    }
}

RecyclerView

<com.cooltechworks.views.shimmer.ShimmerRecyclerView
                android:id="@+id/plpRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clipToPadding="false"
                android:fastScrollEnabled="true"
                android:overScrollMode="never"
                android:paddingStart="@dimen/dimen_10dp"
                android:paddingTop="@dimen/dimen_10dp"
                android:paddingEnd="@dimen/dimen_10dp"
                android:paddingBottom="@dimen/dimen_55dp"
                android:splitMotionEvents="false"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"
                app:shimmer_demo_layout="@layout/plp_placeholder"
                app:shimmer_demo_layout_manager_type="grid"
                app:shimmer_demo_shimmer_color="#21ffffff"
                tools:visibility="invisible" />

Note: I have observed that while loading the next page Paging adapter position start from 0

For example

Page 0 -> 20 products

page 1 -> loading next 20 products -> position start from 0 (Unexpected)


Solution

  • Finally, I found the solution, I am using one shimmer library which is causing the issue the problem is that when I am hiding the shimmer one method which is getting called causing the issue

    binding.plpRecyclerView.hideShimmerAdapter()
    

    This method is replacing the shimmer adapter with the actual adapter

    Library Code Snippet

    public void hideShimmerAdapter() {
        mCanScroll = true;
        setLayoutManager(mActualLayoutManager);
        setAdapter(mActualAdapter);
    }
    
    public void setLayoutManager(LayoutManager manager) {
        if (manager == null) {
            mActualLayoutManager = null;
        } else if (manager != mShimmerLayoutManager) {
            mActualLayoutManager = manager;
        }
    
        super.setLayoutManager(manager);
    }
    

    I removed that library and manage shimmer by my own instead of using library