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

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)
            }
        }
    }
    
}

问题根因

一共两个核心错误:

  1. 栈溢出问题:重写的getItemCount方法里直接return itemCount,会递归调用当前方法本身,没有终止条件,最终触发栈溢出。PagingDataAdapter本身已经实现了正确的getItemCount逻辑,完全不需要手动重写这个方法。
  2. 列表不显示问题:PagingSource的load方法是挂起函数,在里面用RxJava的subscribe发起异步请求,没有阻塞等待请求结果就直接执行了LoadResult.Page返回逻辑,此时movieResponse还是初始化的空列表,所以Paging拿到的数据永远是空的,自然不会渲染列表。Log中能看到接口返回,是因为异步请求晚于return逻辑执行,结果返回时已经来不及传给Paging组件了。另外未实现的getRefreshKey方法也会导致刷新功能异常。

修复方案
  1. 删除MoviesDataAdapter里重写的getItemCount方法,直接使用父类PagingDataAdapter的实现即可。
  2. 修改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原生支持挂起函数,代码会更简洁。

  1. 检查Activity/Fragment中提交数据给适配器的逻辑,确保调用了adapter.submitData(lifecycle, pagingData)把Paging数据提交给适配器,同时给RecyclerView设置LayoutManager,通过withLoadStateFooter绑定加载状态尾部适配器。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 15:48:23