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
相关产品推荐
相关产品推荐

