You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Paging 3滚动加载时无法追加后续页面问题求助

Paging3加载2页后无法追加第3页数据问题排查

我首次在项目中使用Paging3组件,结合TMDB API的discover接口获取电影列表,期望滚动时加载更多数据。技术栈采用Retrofit做网络请求、Gson反序列化、Hilt依赖注入,在Fragment中通过Flow收集数据。

目前仅能正常加载2页数据,第3页虽已请求并获取到数据,但无法追加到列表中,无法继续滚动,该情况仅能从Logcat中观测到。我尝试过调整PagingSource、Pager及调用源的实现方式,也将Moshi替换为Gson,但问题仍未解决。


实现代码

Paging Source

private const val STARTING_PAGE_INDEX = 1

class DiscoverMoviesPagingDataSource(private val service: MoviesService): PagingSource<Int, DiscoverMoviesDataDto>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DiscoverMoviesDataDto> {

        return try {
            val currentPage = params.key ?: STARTING_PAGE_INDEX
            val response = service.fetchDiscoverMoviesList(currentPage)
            val movies = response.body()?.data!!

            val movieResponse = mutableListOf<DiscoverMoviesDataDto>()
            movieResponse.addAll(movies)

            Log.d("MyPagingSource", "Loaded ${movies.size} items")

            LoadResult.Page(
                data = movieResponse,
                prevKey = if (currentPage == STARTING_PAGE_INDEX) null else currentPage.minus(1),
                nextKey = currentPage.plus(1)
            )

        } catch (exception: IOException) {
            LoadResult.Error(exception)
        } catch (exception: HttpException) {
            LoadResult.Error(exception)
        } catch (exception: Exception) {
            LoadResult.Error(exception)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, DiscoverMoviesDataDto>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

Repository

@ActivityScoped
class MovieRepositoryImpl @Inject constructor(
    private val moviesService: MoviesService
) : MovieRepository {

    override fun fetchDiscoverMovieList(): Flow<PagingData<DiscoverMoviesDataDto>> {

        return Pager(
            PagingConfig(
                pageSize = NETWORK_PAGE_SIZE,
                prefetchDistance = 5
            )
        ) {
            DiscoverMoviesPagingDataSource(moviesService)
        }.flow
    }
}

ViewModel

@HiltViewModel
class MovieViewModel @Inject constructor(private val repository: MovieRepositoryImpl): ViewModel() {

    private val _movieList = repository.fetchDiscoverMovieList().cachedIn(viewModelScope)
    val movieList get() = _movieList

}

Fragment观测代码

viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.movieList.collect {
                    adapter.submitData(pagingData = it)
                }
            }
        }

Paging Adapter

class MovieListAdapter(
    private val context: Context,
    private val onMovieItemClicked: (id: String) -> Unit
) : PagingDataAdapter<DiscoverMoviesDataDto, MovieListAdapter.MovieListViewHolder>(MoviesAdapterDiffer()) {

    inner class MovieListViewHolder(val binding: ItemCardMovieBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bindItem(movieItem: DiscoverMoviesDataDto) {
            binding.tvMovieName.text = movieItem.title
            binding.tvMovieDate.text = parseFriendlyDate(movieItem.release_date)
            binding.cardItemMovie.setOnClickListener {
                onMovieItemClicked(movieItem.id.toString())
            }

            Glide.with(context)
                .load(Constants.BACKDROP_BASE_URL + movieItem.poster_path)
                .into(binding.ivMovieImage)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieListViewHolder {
        val binding =
            ItemCardMovieBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MovieListViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MovieListViewHolder, position: Int) {
        getItem(position)?.let { holder.bindItem(it) }
    }


    class MoviesAdapterDiffer() : DiffUtil.ItemCallback<DiscoverMoviesDataDto>() {
        override fun areItemsTheSame(
            oldItem: DiscoverMoviesDataDto,
            newItem: DiscoverMoviesDataDto
        ): Boolean {
            return oldItem.id == newItem.id
        }

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

问题排查与解决方案

1. 修正nextKey的生成逻辑

当前代码无条件返回currentPage.plus(1)作为下一页key,但未判断是否已到达最后一页。TMDB接口返回的实体中包含total_pages字段,当当前页等于total_pages时,应返回null告知Paging没有更多数据,否则会导致无效请求或重复数据无法被DiffUtil识别。

修改PagingSource的load方法:

val responseBody = response.body()!!
val movies = responseBody.data!!
val totalPages = responseBody.total_pages // 确保实体类包含该字段

LoadResult.Page(
    data = movieResponse,
    prevKey = if (currentPage == STARTING_PAGE_INDEX) null else currentPage.minus(1),
    nextKey = if (currentPage < totalPages) currentPage.plus(1) else null
)

2. 对齐pageSize与接口返回数量

确认NETWORK_PAGE_SIZE的数值和TMDB接口默认/请求的每页条目数一致(TMDB默认20条/页)。如果两者不匹配,会导致Paging的分页逻辑混乱,无法正确触发下一页加载。

3. 确保数据类正确重写equals/hashCode

当前areContentsTheSame依赖oldItem == newItem判断内容是否一致,若DiscoverMoviesDataDto不是data class且未手动重写equals和hashCode,会导致DiffUtil误判内容相同,从而不更新列表。建议将其定义为data class(默认自动生成这两个方法)。

4. 验证第3页数据唯一性

通过Log打印第3页数据的id列表,确认与前两页无重复。若接口返回重复数据,DiffUtil会认为条目已存在,不会追加到列表中。


内容的提问来源于stack exchange,提问作者Judah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 08:25:41