Paging3调用adapter.retry时RecyclerView闪烁问题求助
问题描述
分页功能运行正常,但断开网络后调用adapter.retry()时,RecyclerView会出现闪烁,不过新加载的电影能正确添加到对应位置。我严格遵循Android Paging Codelab实现,仅针对NYTimes电影API做了适配,正常滚动时分页表现正常。
代码片段
RemoteMediator
override suspend fun load( loadType: LoadType, state: PagingState<Int, Movie> ): MediatorResult { val page = when(loadType) { LoadType.APPEND -> { val remoteKeys = getRemoteKeyForLastItem(state) val nextKey = remoteKeys?.nextKey if (nextKey == null) { return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) } nextKey } LoadType.PREPEND -> { val remoteKeys = getRemoteKeyForFirstItem(state) val prevKey = remoteKeys?.prevKey if (prevKey == null) { return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) } prevKey } LoadType.REFRESH -> { val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX } } try { val response = api.fetchMovieCatalog(offset).toMovieCatalog() offset+=20 val movies = response.movieCatalog val endOfPaginationReached = movies.isEmpty() db.withTransaction { if (loadType == LoadType.REFRESH) { db.remoteKeysDao.clearRemoteKeys() db.moviesDao.clearMovies() } val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1 val nextKey = if (endOfPaginationReached) null else page + 1 val keys = movies.map { RemoteKeys(movie = it.title, prevKey = prevKey, nextKey = nextKey) } db.remoteKeysDao.insertAll(keys) db.moviesDao.insertMovies(movies) } return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: IOException) { return MediatorResult.Error(e) } catch (e: HttpException) { return MediatorResult.Error(e) } } private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): RemoteKeys? { // 获取最后一个包含数据的分页,再取其中最后一项 return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull() ?.let { movie -> Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie.title)}, last item") db.remoteKeysDao.remoteKeysMovieId(movie.title) } } private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Movie>): RemoteKeys? { // 获取第一个包含数据的分页,再取其中第一项 return state.pages.firstOrNull() { it.data.isNotEmpty() }?.data?.firstOrNull() ?.let { movie -> Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie.title)}, first item") db.remoteKeysDao.remoteKeysMovieId(movie.title) } } private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Movie>): RemoteKeys? { // 获取锚点位置附近的条目 return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.title?.let { movie -> Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie)}, refresh") db.remoteKeysDao.remoteKeysMovieId(movie) } } }
Fragment
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentMovieCatalogBinding.inflate(inflater, container, false) binding.recyclerView.addItemDecoration( DividerItemDecoration( context, DividerItemDecoration.VERTICAL ) ) binding.recyclerView.itemAnimator = DefaultItemAnimator() binding.bindState( uiAction = viewModel.accept, uiState = viewModel.state, pagingData = viewModel.pagingDataFLow ) return binding.root } private fun FragmentMovieCatalogBinding.bindState( uiAction: (UiAction) -> Unit, pagingData: Flow<PagingData<UiModel>>, uiState: StateFlow<UiState> ) { val adapter = MovieCatalogAdapter(requireActivity()) val header = MoviesLoadStateAdapter { adapter.retry() } val footer = MoviesLoadStateAdapter { adapter.retry() } recyclerView.adapter = adapter.withLoadStateHeaderAndFooter( header = header, footer = footer ) bindList(adapter, pagingData, header) } private fun FragmentMovieCatalogBinding.bindList( adapter: MovieCatalogAdapter, pagingData: Flow<PagingData<UiModel>>, header: MoviesLoadStateAdapter ) { swipeRefresh.setOnRefreshListener { adapter.refresh() } lifecycleScope.launch { pagingData.collectLatest(adapter::submitData) } lifecycleScope.launch { adapter.loadStateFlow.collect { loadState -> header.loadState = loadState.mediator ?.refresh ?.takeIf { it is LoadState.Error && adapter.itemCount > 0 } ?: loadState.prepend // 显示空列表 emptyList.isVisible = loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 // 仅在刷新成功后显示列表 recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading // 初始加载或刷新时显示进度条 swipeRefresh.isRefreshing = loadState.mediator?.refresh is LoadState.Loading } } }
原因分析与解决方法
1. DefaultItemAnimator 动画触发闪烁
DefaultItemAnimator的默认添加/移除动画会在重试加载时触发不必要的视图过渡效果,导致闪烁。可以通过两种方式解决:
- 直接禁用ItemAnimator:
binding.recyclerView.itemAnimator = null
- 自定义ItemAnimator关闭添加动画:
class NoFlickerItemAnimator : DefaultItemAnimator() { override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean { dispatchAddFinished(holder) return false } } // 替换默认动画 binding.recyclerView.itemAnimator = NoFlickerItemAnimator()
2. RemoteMediator 中 offset 变量的逻辑问题
代码中用成员变量offset控制API请求偏移量,但page是通过RemoteKeys计算的,两者可能出现不一致。重试时offset已累加过一次,会导致请求错误页码,建议改为用page计算偏移量:
const val PAGE_SIZE = 20 // 替换原offset使用逻辑 val requestOffset = (page - 1) * PAGE_SIZE val response = api.fetchMovieCatalog(requestOffset).toMovieCatalog() // 移除offset+=20这行代码
3. PagingAdapter 的 DiffUtil 实现错误
确保MovieCatalogAdapter的DiffUtil.ItemCallback实现正确,错误的条目判断会导致Paging错误刷新视图引发闪烁:
object MovieDiffCallback : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { // 用电影唯一标识判断,比如title或ID return when { oldItem is UiModel.MovieItem && newItem is UiModel.MovieItem -> oldItem.movie.title == newItem.movie.title else -> oldItem == newItem } } override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return oldItem == newItem } }
内容的提问来源于stack exchange,提问作者Ricardo Junior
相关产品推荐
相关产品推荐

