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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 21:30:41