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

Android中Retrofit获取TMDB数据成功但RecyclerView无法显示图片

问题场景

开发电影类Android应用时,通过Retrofit从TMDB接口拉取分类及对应电影数据,使用嵌套RecyclerView展示分类名和分类下横向排列的电影海报,接口请求返回成功,但页面无法正常显示电影数据。

核心问题点
  • 异步请求时序错误:enqueue是异步非阻塞方法,for循环内发起单分类电影列表请求后,代码不会等待接口返回就立刻执行列表添加操作,此时当前分类的电影数据还未拿到,存入列表的results是空值或其他请求残留的旧数据。
  • 数据模型定义错误:两个结构完全不同的接口返回被错误合并到GenreList类中:分类列表接口返回的分类项仅包含id、name字段,无results;分类下电影列表接口返回根结构包含id、results字段,无name字段,Gson解析时会出现字段匹配异常。
  • 无刷新通知:接口返回数据更新列表后,没有调用RecyclerView适配器的刷新方法,界面不会触发重绘。
  • 共享变量数据错乱:使用全局共享的results变量存储多分类的电影数据,多个异步请求并发返回时会互相覆盖值,导致列表数据混乱。

修复步骤

1. 拆分修正数据模型

把不同接口的返回结构、UI展示用的结构拆分定义,避免字段混淆:

// 分类列表接口返回根结构
data class GenreResponse(
    val genres: List<GenreItem>
): Serializable

// 分类基础信息
data class GenreItem(
    val id: Int,
    val name: String
): Serializable

// 分类下电影列表接口返回根结构
data class GenreMovieResponse(
    val id: Int,
    val results: ArrayList<MovieItem>,
    val page: Int,
    val total_pages: Int,
    val total_results: Int
): Serializable

// 单部电影信息
data class MovieItem(
    val id: Int,
    val title: String,
    val poster_path: String,
    val release_date: String,
    val original_language: String,
    val overview: String
): Serializable

// 给外层适配器用的 分类+对应电影列表 合并结构
data class GenreWithMovies(
    val id: Int,
    val name: String,
    val movies: ArrayList<MovieItem> = arrayListOf()
)

2. 修正ApiInterface定义

对应新的数据模型调整接口返回类型:

interface ApiInterface {
    @GET("/3/genre/movie/list")
    fun getGenreList(
        @Query("api_key") apiKey: String
    ): Call<GenreResponse>

    @GET("/3/genre/{id}/movies")
    fun getGenreMovieList(
        @Path("id") genreId: Int,
        @Query("api_key") apiKey: String
    ): Call<GenreMovieResponse>

    // 其余接口按相同逻辑修正返回类型即可
}

3. 修正数据请求逻辑

解决异步时序问题,数据更新后主动通知适配器刷新:

// 适配器数据源提前初始化
private val genreWithMoviesList = ArrayList<GenreWithMovies>()
private lateinit var genreAdapter: GenreFragmentAdapter

private fun loadAllData() {
    val apiInterface = RetrofitClient.getInstance().create(ApiInterface::class.java)
    // 第一步先拉全部分类
    apiInterface.getGenreList(Constants.APP_ID).enqueue(object : Callback<GenreResponse> {
        override fun onResponse(call: Call<GenreResponse>, response: Response<GenreResponse>) {
            if (!response.isSuccessful) {
                when(response.code()) {
                    400 -> Log.e("API_ERROR", "Bad Connection")
                    404 -> Log.e("API_ERROR", "Not Found")
                    else -> Log.e("API_ERROR", "Generic Error")
                }
                return
            }
            val genres = response.body()?.genres ?: return
            // 先清空旧数据,加入所有分类条目,优先展示分类名
            genreWithMoviesList.clear()
            genres.forEach {
                genreWithMoviesList.add(GenreWithMovies(id = it.id, name = it.name))
            }
            genreAdapter.notifyDataSetChanged()

            // 第二步逐个拉取每个分类下的电影
            genres.forEachIndexed { index, genre ->
                apiInterface.getGenreMovieList(genre.id, Constants.APP_ID).enqueue(object : Callback<GenreMovieResponse> {
                    override fun onResponse(call: Call<GenreMovieResponse>, response: Response<GenreMovieResponse>) {
                        if (response.isSuccessful) {
                            val movies = response.body()?.results ?: return
                            // 更新对应位置分类的电影数据
                            genreWithMoviesList[index].movies.addAll(movies)
                            // 仅刷新变化的条目,性能更好
                            genreAdapter.notifyItemChanged(index)
                        }
                    }

                    override fun onFailure(call: Call<GenreMovieResponse>, t: Throwable) {
                        Log.e("MOVIE_LOAD_ERROR", t.message.toString())
                    }
                })
            }
        }

        override fun onFailure(call: Call<GenreResponse>, t: Throwable) {
            Log.e("GENRE_LOAD_ERROR", t.message.toString())
        }
    })
}

4. 修正外层适配器逻辑

适配新的数据结构,优化嵌套RecyclerView性能:

class GenreFragmentAdapter(
    private val genreList: List<GenreWithMovies>,
    val context: Context
): RecyclerView.Adapter<GenreFragmentAdapter.ViewHolder>() {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val genreName: TextView = view.findViewById(R.id.genreName)
        val movieRecyclerView: RecyclerView = view.findViewById(R.id.genreListRecyclerView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.genre_list_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val currentData = genreList[position]
        holder.genreName.text = currentData.name
        // 配置内层横向列表
        holder.movieRecyclerView.layoutManager = LinearLayoutManager(
            context,
            RecyclerView.HORIZONTAL,
            false
        )
        holder.movieRecyclerView.setHasFixedSize(true)
        holder.movieRecyclerView.adapter = GenreResultsAdapter(currentData.movies, context)
    }

    override fun getItemCount() = genreList.size
}

额外优化建议
  • 移除自定义的忽略SSL证书校验的OkHttpClient实现,TMDB持有合法CA颁发的证书,直接使用默认OkHttpClient即可,自定义信任管理器存在数据窃听、篡改的安全风险,部分机型上还会出现兼容性问题。
  • 加载电影海报时,需要给poster_path拼接TMDB官方图片基地址,示例:https://image.tmdb.org/t/p/w500${movie.poster_path},否则仅靠相对路径无法加载图片。
  • 内层RecyclerView可以做回收池复用优化,减少滑动时的性能消耗。

内容的提问来源于stack exchange,提问作者Jajatee Nandinee Sahoo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 10:51:32