Paging3向嵌套RecyclerView加载分页数据的最佳实现方案
嵌套RecyclerView + Paging3 多分类分页最优实现
基础架构选型
- 父层RecyclerView直接使用普通列表适配器即可,不需要接入Paging3。探索页的分类条目(热门、高评分、即将上映等)属于固定有限集合,不存在分页加载需求,普通
ListAdapter足够承载,额外套Paging只会徒增生命周期管理复杂度。 - 本地电影数据库表新增
category字符串字段,所有分类的电影数据统一存在同一张表,无需为每个分类单独建表。RemoteMediator拉取到数据后,给对应条目打上当前分类标签再入库,后续新增分类只需要加枚举值,不需要修改表结构。 - 所有Paging逻辑收敛到共享的页面级ViewModel中,不要在ViewHolder、子Adapter里写分页请求逻辑,避免生命周期失控导致重复请求、内存泄漏。
分页流分发核心实现
- ViewModel内按分类维度缓存对应的
PagingData流,所有流通过cachedIn(viewModelScope)托管生命周期,配置变更(转屏、深色模式切换)时不会重复触发请求,也能保证子列表复用时直接取缓存数据。
核心实现代码:// 全量分类枚举 enum class MovieCategory(val tag: String) { HOT("hot"), TOP_RATED("top_rated"), UPCOMING("upcoming") } class ExploreViewModel(private val repo: MovieRepository) : ViewModel() { // 按分类缓存分页流 private val categoryFlows = mutableMapOf<MovieCategory, Flow<PagingData<Movie>>>() fun fetchFlowByCategory(category: MovieCategory): Flow<PagingData<Movie>> { return categoryFlows.getOrPut(category) { repo.getPagingDataByCategory(category).cachedIn(viewModelScope) } } } - 数据层做数据源隔离:PagingSource从数据库查询数据时,按传入的
category字段做过滤,只返回对应分类的条目;RemoteMediator请求API时携带分类参数,返回数据入库前统一打上分类标签,从根源避免不同分类的数据串流。
RemoteMediator核心逻辑片段:class MovieRemoteMediator( private val category: MovieCategory, private val api: MovieApi, private val dao: MovieDao ) : RemoteMediator<Int, Movie>() { private var nextPage = 1 override suspend fun load(loadType: LoadType, state: PagingState<Int, Movie>): MediatorResult { val loadPage = when(loadType) { LoadType.REFRESH -> 1 LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true) LoadType.APPEND -> nextPage } return try { val resp = api.fetchMovieList(category = category.tag, page = loadPage) // 给返回数据打分类标签 val taggedData = resp.results.map { it.copy(categoryTag = category.tag) } if (loadType == LoadType.REFRESH) { dao.deleteByCategory(category.tag) nextPage = 1 } dao.insertAll(taggedData) nextPage++ MediatorResult.Success(endOfPaginationReached = resp.results.isEmpty()) } catch (e: Exception) { MediatorResult.Error(e) } } }
子列表绑定与生命周期管控
- 父Adapter的ViewHolder初始化时就创建好子RecyclerView的Adapter、LayoutManager,不要在
onBind时重复创建实例。所有子RecyclerView共享同一个RecycledViewPool,因为子项布局完全一致,共享池可以大幅减少View创建开销,滑动更流畅。 - 绑定子列表时,通过
findViewTreeLifecycleOwner()获取当前ItemView对应的生命周期,在该生命周期作用域内收集对应分类的Paging流,提交数据给子Adapter。子Item滑出屏幕被回收时,协程会自动取消,不会占用资源。 - 重写父Adapter的
onViewRecycled方法,ViewHolder被回收时手动取消上一次的流收集Job,避免ViewHolder复用时上一个分类的数据串到新分类列表中。
绑定逻辑代码片段:class CategoryViewHolder(itemView: View, private val viewModel: ExploreViewModel) : RecyclerView.ViewHolder(itemView) { private val titleTv = itemView.findViewById<TextView>(R.id.tv_category_title) private val childRv = itemView.findViewById<RecyclerView>(R.id.rv_movie_child) private val childAdapter = MoviePagingAdapter() private var collectJob: Job? = null init { childRv.adapter = childAdapter childRv.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false) } fun bind(category: MovieCategory) { titleTv.text = when(category) { HOT -> "热门电影" TOP_RATED -> "高评分电影" UPCOMING -> "即将上映" } // 取消上一次收集任务,防止串数据 collectJob?.cancel() val lifecycleOwner = itemView.findViewTreeLifecycleOwner() ?: return collectJob = lifecycleOwner.lifecycleScope.launch { viewModel.fetchFlowByCategory(category).collect { childAdapter.submitData(it) } } } fun onViewRecycled() { collectJob?.cancel() } } // 父Adapter中重写回收回调 override fun onViewRecycled(holder: CategoryViewHolder) { super.onViewRecycled(holder) holder.onViewRecycled() }
避坑与优化
- 不要尝试用单一全局Paging流拉取所有分类数据再拆分分发,这种方案会导致所有分类的加载状态强耦合,一个分类加载失败会阻塞所有分类展示,且客户端数据过滤的性能开销远高于分类隔离方案。
- 子RecyclerView关闭嵌套滚动,避免和父RV的滑动冲突,横向滑动的预取距离设置为屏幕可见条目数的1.5倍,减少滑动白块。
- RemoteMediator加请求去重标记,同一分类同一时间只允许一个分页请求,防止快速滑动时触发重复请求。
内容的提问来源于stack exchange,提问作者Basem Osama
相关产品推荐
相关产品推荐

