Paging3 PagingDataAdapter getItemCount栈溢出及列表不显示问题
问题描述
在应用中尝试使用Paging3、Retrofit与RxJava加载热门电影列表,调用的是TMDB官方热门电影接口。问题出现在MoviesDataAdapter类中:
- 不重写
getItemCount方法时,RecyclerView无法展示任何内容,但接口返回结果可以在Log中正常打印 - 重写该方法返回
itemCount时,Android Studio会对该方法给出警告,运行时触发栈溢出错误

错误日志如下:
java.lang.StackOverflowError: stack size 8192KB at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32) 2022-06-10 22:48:58.080 10543-10543/com.mml.moviemvvm E/AndroidRuntime: at com.mml.moviemvvm.ui.MoviesDataAdapter.getItemCount(MoviesDataAdapter.kt:32)
不重写getItemCount运行时页面无列表展示,效果如下:
相关实现代码
模型类
Movie
data class Movie( val id: Int, @SerializedName("poster_path") val posterPath: String, @SerializedName("release_date") val releaseDate: String, val title: String )
MovieResponse
data class MovieResponse( val page: Int, @SerializedName("results") val movieList: List<Movie>, @SerializedName("total_pages") val totalPages: Int, @SerializedName("total_results") val totalResults: Int )
状态类
enum class Status { LOADING, SUCCESS, FAILED } class NetworkState(val status: Status, val msg: String) { companion object { val SUCCEED: NetworkState = NetworkState(Status.SUCCESS, "Success") val LOADING: NetworkState = NetworkState(Status.LOADING, "Running") val ERROR: NetworkState = NetworkState(Status.FAILED, "Something went wrong") val ENDOFLIST = NetworkState(Status.FAILED, "You have reached the end") } }
网络接口定义
interface TheMovieDbApi { @GET("movie/popular") fun getPopularMovies(@Query("page") page: Int): Single<MovieResponse> @GET("movie/{movie_id}") fun getMovieDetails(@Path("movie_id") id: Int): Single<MovieDetails> }
网络客户端实现
const val API_KEY = "some key" const val BASE_URL = "https://api.themoviedb.org/3/" const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342" const val FIRST_PAGE = 1 const val POST_PER_PAGE = 20 object TheMovieDBClient { fun getClient(): TheMovieDbApi { val requestInterceptor = Interceptor { chain -> val url = chain.request() .url() .newBuilder() .addQueryParameter("api_key", API_KEY) .build() val request = chain.request() .newBuilder() .url(url) .build() return@Interceptor chain.proceed(request) } val okHttpClient = OkHttpClient.Builder() .addInterceptor(requestInterceptor) .connectTimeout(60, TimeUnit.SECONDS) .build() return Retrofit.Builder() .client(okHttpClient) .baseUrl(BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build() .create(TheMovieDbApi::class.java) } }
分页数据源
class MoviesPagingSource( private val apiService: TheMovieDbApi, ) : PagingSource<Int, Movie>() { override fun getRefreshKey(state: PagingState<Int, Movie>): Int? { TODO("Not yet implemented") } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> { val position = params.key ?: FIRST_PAGE return try { var movieResponse = MovieResponse(3, listOf(),1,1) apiService.getPopularMovies(position) .subscribeOn(Schedulers.io()) .subscribe( { if(it != null){ movieResponse = it Log.e(TAG, "load: $it" ) } }, { throwable -> Log.e(TAG, "load: ${throwable.cause?.message}") } ) LoadResult.Page( data = movieResponse.movieList, prevKey = if (position == FIRST_PAGE) null else position - 1, nextKey = if (movieResponse.movieList.isEmpty()) null else position + 1 ) } catch (ex: IOException) { LoadResult.Error(ex) } catch (ex: HttpException) { LoadResult.Error(ex) } } }
加载状态适配器
class MoviesLoadStateAdapter(private val retry: () -> Unit) : LoadStateAdapter<MoviesLoadStateAdapter.LoadStateViewHolder>() { inner class LoadStateViewHolder(private val binding: MoviesLoadStateFooterBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.buttonRetry.setOnClickListener { retry.invoke() } } fun bind(loadState: LoadState) { binding.apply { progressBar.isVisible = loadState is LoadState.Loading buttonRetry.isVisible = loadState !is LoadState.Loading textViewError.isVisible = loadState !is LoadState.Loading } } } override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { holder.bind(loadState) } override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { val binding: MoviesLoadStateFooterBinding = MoviesLoadStateFooterBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return LoadStateViewHolder(binding) } }
数据仓库
class MoviesRepository(private val api: TheMovieDbApi) { fun getMoviesResults() = Pager( config = PagingConfig( pageSize = 20, maxSize = 100, enablePlaceholders = false, ), pagingSourceFactory = { MoviesPagingSource(api) } ).liveData }
列表适配器
class MoviesDataAdapter(private val listener: MainActivity) : PagingDataAdapter<Movie, MoviesDataAdapter.MovieItemViewHolder>(MOVIE_COMPARATOR) { override fun onBindViewHolder(holder: MovieItemViewHolder, position: Int) { val movie = getItem(position) holder.bind(movie) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieItemViewHolder { val binding = MovieListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return MovieItemViewHolder(binding) } override fun getItemCount(): Int { return itemCount } companion object { private val MOVIE_COMPARATOR = object : DiffUtil.ItemCallback<Movie>() { override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = oldItem == newItem } } interface OnClickListener{ fun onItemClick(movie: Movie?) } inner class MovieItemViewHolder(private val binding: MovieListItemBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { val item = getItem(position) if (item != null) { listener.onItemClick(item) } } } } fun bind(movie: Movie?) { binding.apply { cvMovieTitle.text = movie?.title cvMovieReleaseDate.text = movie?.releaseDate val moviePosterURL = POSTER_BASE_URL + movie?.posterPath Glide.with(cardView.context) .load(moviePosterURL) .into(cvIvMoviePoster) } } } }
问题根因
一共两个核心错误:
- 栈溢出问题:重写的
getItemCount方法里直接return itemCount,会递归调用当前方法本身,没有终止条件,最终触发栈溢出。PagingDataAdapter本身已经实现了正确的getItemCount逻辑,完全不需要手动重写这个方法。 - 列表不显示问题:
PagingSource的load方法是挂起函数,在里面用RxJava的subscribe发起异步请求,没有阻塞等待请求结果就直接执行了LoadResult.Page返回逻辑,此时movieResponse还是初始化的空列表,所以Paging拿到的数据永远是空的,自然不会渲染列表。Log中能看到接口返回,是因为异步请求晚于return逻辑执行,结果返回时已经来不及传给Paging组件了。另外未实现的getRefreshKey方法也会导致刷新功能异常。
修复方案
- 删除
MoviesDataAdapter里重写的getItemCount方法,直接使用父类PagingDataAdapter的实现即可。 - 修改
MoviesPagingSource的请求逻辑,不要在挂起函数里用RxJava的异步订阅,直接用await等待RxJava请求结果,或者改用Retrofit的挂起函数实现。
修正后的MoviesPagingSource代码参考:
class MoviesPagingSource( private val apiService: TheMovieDbApi, ) : PagingSource<Int, Movie>() { override fun getRefreshKey(state: PagingState<Int, Movie>): Int? { // 实现刷新key逻辑,取最近访问的页码作为刷新起始页 return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> { val position = params.key ?: FIRST_PAGE return try { // 直接等待RxJava请求返回结果,不要用异步subscribe val movieResponse = apiService.getPopularMovies(position) .subscribeOn(Schedulers.io()) .await() // 依赖rxkotlin扩展,引入io.reactivex.rxjava2:rxkotlin:2.4.0即可使用 LoadResult.Page( data = movieResponse.movieList, prevKey = if (position == FIRST_PAGE) null else position - 1, nextKey = if (movieResponse.movieList.isEmpty()) null else position + 1 ) } catch (ex: IOException) { LoadResult.Error(ex) } catch (ex: HttpException) { LoadResult.Error(ex) } } }
如果不想用RxJava的await,也可以直接把
TheMovieDbApi里的getPopularMovies改成suspend fun,去掉RxJava返回值,Retrofit原生支持挂起函数,代码会更简洁。
- 检查Activity/Fragment中提交数据给适配器的逻辑,确保调用了
adapter.submitData(lifecycle, pagingData)把Paging数据提交给适配器,同时给RecyclerView设置LayoutManager,通过withLoadStateFooter绑定加载状态尾部适配器。
内容的提问来源于stack exchange,提问作者Dr Mido
相关产品推荐
相关产品推荐

