Paging 3 Library:按顺序合并三张表的查询结果
使用Paging 3 Library按顺序合并三张表查询结果的实现方案
核心思路
通过自定义PagingSource,内部维护当前的加载阶段(依次对应Table1、Table2、Table3),每次加载时根据当前阶段查询对应表的分页数据;当某张表的分页数据耗尽(返回条目数小于分页大小),自动切换到下一张表的加载阶段,直到所有表加载完成后停止。
1. 定义统一数据实体类
由于三张表的数据结构可能不同,需要用密封类封装所有可能的条目类型,方便后续Adapter统一处理:
sealed class CombinedData { data class Table1Item(val id: Int, val content: String) : CombinedData() data class Table2Item(val id: Int, val value: Double) : CombinedData() data class Table3Item(val id: Int, val name: String) : CombinedData() }
2. 实现多阶段加载的PagingSource
自定义PagingSource,内部管理加载阶段和各表的分页偏移量,核心逻辑在load方法中:
class MultiTablePagingSource( private val table1Dao: Table1Dao, private val table2Dao: Table2Dao, private val table3Dao: Table3Dao ) : PagingSource<Int, CombinedData>() { // 定义加载阶段 private enum class LoadStage { TABLE1, TABLE2, TABLE3, FINISHED } private var currentStage = LoadStage.TABLE1 // 各表的当前分页页码 private var table1Page = 0 private var table2Page = 0 private var table3Page = 0 private val pageSize = 20 // 统一分页大小 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CombinedData> { return try { // 确定当前加载的页码 val currentPage = params.key ?: when(currentStage) { LoadStage.TABLE1 -> table1Page LoadStage.TABLE2 -> table2Page LoadStage.TABLE3 -> table3Page LoadStage.FINISHED -> return LoadResult.Page(emptyList(), null, null) } // 根据当前阶段加载对应表的数据 val data = when(currentStage) { LoadStage.TABLE1 -> { val items = table1Dao.getPagedItems(currentPage * pageSize, pageSize) .map { CombinedData.Table1Item(it.id, it.content) } // 判断Table1是否加载完成:返回条目数小于分页大小则切换阶段 if (items.size < pageSize) currentStage = LoadStage.TABLE2 table1Page = currentPage + 1 items } LoadStage.TABLE2 -> { val items = table2Dao.getPagedItems(currentPage * pageSize, pageSize) .map { CombinedData.Table2Item(it.id, it.value) } if (items.size < pageSize) currentStage = LoadStage.TABLE3 table2Page = currentPage + 1 items } LoadStage.TABLE3 -> { val items = table3Dao.getPagedItems(currentPage * pageSize, pageSize) .map { CombinedData.Table3Item(it.id, it.name) } if (items.size < pageSize) currentStage = LoadStage.FINISHED table3Page = currentPage + 1 items } LoadStage.FINISHED -> emptyList() } // 计算下一页key:未到最后阶段则继续分页,否则返回null终止加载 val nextKey = if (currentStage != LoadStage.FINISHED) currentPage + 1 else null // 上一页key:仅Table1阶段允许回退,其他阶段无需返回(因为是顺序加载) val prevKey = if (currentStage == LoadStage.TABLE1 && currentPage > 0) currentPage - 1 else null LoadResult.Page(data = data, prevKey = prevKey, nextKey = nextKey) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, CombinedData>): Int? { // 刷新时重置所有状态,重新从Table1开始加载 currentStage = LoadStage.TABLE1 table1Page = 0 table2Page = 0 table3Page = 0 return 0 } }
3. Dao层配置分页查询
为每张表提供支持分页的Dao方法(以Room为例):
@Dao interface Table1Dao { @Query("SELECT * FROM table1 LIMIT :limit OFFSET :offset") suspend fun getPagedItems(offset: Int, limit: Int): List<Table1Entity> } // Table2Dao、Table3Dao同理,替换表名和实体类即可
4. ViewModel中配置Pager
将自定义的PagingSource传入Pager,生成可观察的PagingData流:
class CombinedViewModel( private val table1Dao: Table1Dao, private val table2Dao: Table2Dao, private val table3Dao: Table3Dao ) : ViewModel() { val combinedDataFlow = Pager( config = PagingConfig( pageSize = 20, enablePlaceholders = false // 关闭占位符,避免空数据展示 ) ) { MultiTablePagingSource(table1Dao, table2Dao, table3Dao) }.flow.cachedIn(viewModelScope) // 缓存数据,避免屏幕旋转时重新加载 }
5. UI层实现多类型Adapter
在RecyclerView Adapter中处理不同类型的CombinedData,绑定对应的布局:
class CombinedAdapter : PagingDataAdapter<CombinedData, RecyclerView.ViewHolder>(diffCallback) { companion object { private val diffCallback = object : DiffUtil.ItemCallback<CombinedData>() { override fun areItemsTheSame(oldItem: CombinedData, newItem: CombinedData): Boolean { return when { oldItem is CombinedData.Table1Item && newItem is CombinedData.Table1Item -> oldItem.id == newItem.id oldItem is CombinedData.Table2Item && newItem is CombinedData.Table2Item -> oldItem.id == newItem.id oldItem is CombinedData.Table3Item && newItem is CombinedData.Table3Item -> oldItem.id == newItem.id else -> false } } override fun areContentsTheSame(oldItem: CombinedData, newItem: CombinedData): Boolean { return oldItem == newItem } } } override fun getItemViewType(position: Int): Int { return when (getItem(position)) { is CombinedData.Table1Item -> 1 is CombinedData.Table2Item -> 2 is CombinedData.Table3Item -> 3 null -> throw IllegalArgumentException("Item cannot be null") } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { 1 -> Table1ViewHolder(inflater.inflate(R.layout.item_table1, parent, false)) 2 -> Table2ViewHolder(inflater.inflate(R.layout.item_table2, parent, false)) 3 -> Table3ViewHolder(inflater.inflate(R.layout.item_table3, parent, false)) else -> throw IllegalArgumentException("Unknown view type") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) ?: return when (holder) { is Table1ViewHolder -> holder.bind(item as CombinedData.Table1Item) is Table2ViewHolder -> holder.bind(item as CombinedData.Table2Item) is Table3ViewHolder -> holder.bind(item as CombinedData.Table3Item) } } // 各表对应的ViewHolder class Table1ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: CombinedData.Table1Item) { itemView.tv_content.text = item.content } } class Table2ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: CombinedData.Table2Item) { itemView.tv_value.text = item.value.toString() } } class Table3ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: CombinedData.Table3Item) { itemView.tv_name.text = item.name } } }
注意事项
- 如果表数据是实时更新的,可以结合Room的
@Query返回Flow<List<Entity>>,但需要在PagingSource中处理数据变化的刷新逻辑。 - 分页大小需保持统一,确保判断表数据是否耗尽的逻辑准确(返回条目数小于分页大小则认为当前表无更多数据)。
- 若需要支持下拉刷新,直接调用
PagingDataAdapter.refresh()即可,getRefreshKey方法会重置加载状态。
内容的提问来源于stack exchange,提问作者Cpp crusaders
相关产品推荐
相关产品推荐

