Android Paging 3加载下一页后RecyclerView意外自动滚动至顶部
问题分析与解决方案
从你描述的现象(加载下一页后RecyclerView滚到顶部,适配器位置从0开始)来看,核心问题是加载下一页时意外触发了Paging的refresh操作,而非预期的append操作,导致适配器重新加载整个数据集,而非追加新数据。
可能的触发原因
- PagingSource被意外Invalidate:你的
SSSSearcherPagingSource中修改了共享的searcher.query,如果SearcherForHits会在query变化时自动触发PagingSource失效,那么每次加载下一页修改page参数时,都会导致PagingSource被invalidate,进而触发refresh。 - refresh状态误判与不当操作:你在
setLoadingStates中仅监听loadStates.refresh状态,并且在refresh完成后执行createCategoryTabs等操作——如果加载下一页时错误触发了refresh,这些操作可能间接导致RecyclerView滚动重置(比如重新配置LayoutManager)。 - getRefreshKey逻辑过于简单:固定返回0的refreshKey会导致任何refresh操作都从第0页开始加载,即使原本是要加载下一页。
- PagingData重复提交:如果
paginator.liveData在加载下一页时重新发射了新的PagingData,会触发适配器的submitData重新执行,相当于重置整个数据集。
分步解决方案
1. 修复PagingSource的Invalidate问题
避免在load方法中修改共享的searcher.query,改为传递独立的查询对象,防止触发PagingSource失效:
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> { return try { val pageNumber = params.key ?: 0 // 创建独立查询副本,不修改共享的searcher.query val tempQuery = searcher.query.copy( page = pageNumber, hitsPerPage = 20 ) // 使用独立查询执行搜索 val response = searchWithTempQuery(tempQuery) ?: return emptyPage() val data = response.hits.map(transformer) val nextKey = if (pageNumber < response.nbPages) pageNumber + 1 else null LoadResult.Page( data = data, prevKey = null, nextKey = nextKey ) } catch (exception: Exception) { Log.w("Paging search operation failed", exception) LoadResult.Error(exception) } }
如果无法创建独立查询,需检查SearcherForHits的文档,确认修改query是否会自动触发PagingSource invalidate,若有则需关闭该逻辑。
2. 区分refresh与append状态,避免不当操作
在setLoadingStates中,仅在初始加载或下拉刷新时执行初始化操作,加载下一页(append)时不执行重置逻辑:
private fun setLoadingStates() { viewLifecycleOwner.lifecycleScope.launchWhenResumed { plpAdapter?.loadStateFlow?.collectLatest { loadStates -> // 仅处理初始加载/下拉刷新完成的情况 when (loadStates.refresh) { is LoadState.NotLoading -> { 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() } // 单独处理加载下一页的状态,不执行重置操作 when (loadStates.append) { is LoadState.Error -> { // 处理下一页加载失败逻辑,比如显示重试按钮 } } } } }
3. 优化getRefreshKey逻辑
让refresh操作回到当前可见的页面位置,而非固定从第0页开始:
override fun getRefreshKey(state: PagingState<Int, T>): Int { return state.anchorPosition?.let { anchorPosition -> val closestPage = state.closestPageToPosition(anchorPosition) closestPage?.prevKey ?: closestPage?.nextKey ?: 0 } ?: 0 }
4. 确保submitData仅在必要时调用
检查paginator的创建逻辑,确保同一个查询生命周期内只创建一次paginator,避免重复提交PagingData:
// 确保观察者仅设置一次,paginator仅在新查询时重新创建 paginator?.liveData?.observe(viewLifecycleOwner) { pagingData -> plpAdapter?.submitData(lifecycle, pagingData) }
验证要点
- 观察
loadStateFlow的状态,确认加载下一页时触发的是append而非refresh - 检查适配器的item数量,加载下一页后应该是累加而非重置为20
内容的提问来源于stack exchange,提问作者Fazal Hussain
相关产品推荐
相关产品推荐

