【Android 面试】Paging3

文章目录

  • [一. 面试题](#一. 面试题)
      • [1. Paging3 的核心组件有哪些?各自职责是什么?](#1. Paging3 的核心组件有哪些?各自职责是什么?)
      • [2. Paging3 相比 Paging2 有哪些核心改进?](#2. Paging3 相比 Paging2 有哪些核心改进?)
      • [3. PagingSource 和 RemoteMediator 的区别与适用场景?**](#3. PagingSource 和 RemoteMediator 的区别与适用场景?**)
      • [4. Paging3 + Room 实现离线分页的完整流程是怎样的? **](#4. Paging3 + Room 实现离线分页的完整流程是怎样的? **)
      • [5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?](#5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?)
      • [6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?](#6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?)
      • [7. Paging3 如何处理加载状态与错误重试?](#7. Paging3 如何处理加载状态与错误重试?)
      • [8. Paging3 使用中有哪些常见的坑与注意事项?](#8. Paging3 使用中有哪些常见的坑与注意事项?)
      • [9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数](#9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数)
  • [二. 代码demo](#二. 代码demo)
        • [1. 数据实体类](#1. 数据实体类)
        • [2. 分页数据源:PagingSource 实现](#2. 分页数据源:PagingSource 实现)
        • [3. 列表适配器:PagingDataAdapter 实现](#3. 列表适配器:PagingDataAdapter 实现)
        • [4. ViewModel 层:Pager 配置与数据流](#4. ViewModel 层:Pager 配置与数据流)
        • [5. UI 层:Activity 中使用](#5. UI 层:Activity 中使用)
  • [三. Room + RemoteMediator 标准 Demo(离线优先分页架构)](#三. Room + RemoteMediator 标准 Demo(离线优先分页架构))
        • [1. 数据库实体类](#1. 数据库实体类)
        • [2. DAO 数据访问接口](#2. DAO 数据访问接口)
        • [3. Room 数据库类](#3. Room 数据库类)
        • [4. 模拟网络请求接口](#4. 模拟网络请求接口)
        • [5. 核心:RemoteMediator 实现](#5. 核心:RemoteMediator 实现)
        • [6. ViewModel 层](#6. ViewModel 层)
        • [7. UI 层(与纯网络版完全一致)](#7. UI 层(与纯网络版完全一致))
      • 架构核心总结(面试可直接说)

一. 面试题

1. Paging3 的核心组件有哪些?各自职责是什么?

答案:

  1. PagingSource:分页数据源,定义单页数据的加载逻辑,支持网络、本地数据库等单一数据源,负责加载指定页码的数据。
  2. PagingData:分页数据的容器,包裹每一页的数据结果,是数据流的基本单位。
  3. PagingDataAdapter:RecyclerView 适配器,接收 PagingData,内置 DiffUtil 自动计算差异更新列表,支持加载状态展示。
  4. RemoteMediator:分层数据源中介,用于「网络 + 本地缓存」的分页场景,负责网络分页数据拉取并写入本地数据库。
  5. Pager:分页配置入口,配置分页大小、预取距离、PagingSource 工厂,构建 PagingData 数据流。

2. Paging3 相比 Paging2 有哪些核心改进?

答案:

  • 响应式流设计:Paging3 基于 Kotlin 协程与 Flow 构建,原生支持 Flow、LiveData、RxJava;Paging2 基于回调,代码冗余且不支持响应式。
  • 统一加载状态管理:内置 LoadState 统一处理首次加载、加载更多、加载失败、空状态,无需自行维护状态。
  • 解耦设计:PagingSource 与 Adapter 完全解耦,支持任意数据源;Paging2 耦合度高。
  • 原生分层缓存支持:RemoteMediator 原生支持网络 + 本地缓存的分页架构,Paging2 需要自行实现缓存逻辑。
  • 完善的错误重试:内置 retry 机制,支持全局重试、单页重试;Paging2 重试逻辑需要自行实现。

3. PagingSource 和 RemoteMediator 的区别与适用场景?**

答案:

表格

维度 PagingSource RemoteMediator
角色 直接的分页数据源 分页中介,协调网络与本地数据源
数据来源 单一数据源(纯网络 / 纯本地) 双层数据源(网络 + 本地缓存)
触发时机 列表滑动到预取阈值时触发加载 本地数据加载到底部时触发网络拉取
结果 直接返回 PagingData 给 UI 将网络数据写入数据库,由本地 PagingSource 驱动 UI 更新

适用场景:

  • 纯网络分页、无本地缓存:只用 PagingSource。
  • 离线优先、网络 + 本地缓存分页:Room 提供 PagingSource + RemoteMediator 处理网络拉取。

4. Paging3 + Room 实现离线分页的完整流程是怎样的? **

答案:

这是 Paging3 最经典的实战场景,遵循单一数据源原则:

  1. Room DAO 定义返回 PagingSource 的查询方法,作为本地分页数据源。
  2. 实现 RemoteMediator,重写 load() 方法:当本地数据加载到底部时,触发网络请求下一页数据。
  3. 网络请求成功后,将新数据插入 Room 数据库,同时记录分页状态(如 nextKey)。
  4. Room 数据变更后,自动通过 PagingSource 触发 PagingData 更新,Adapter 自动刷新列表。
  5. 整个流程 UI 只观察 PagingData 数据流,所有数据变更都来自本地数据库,保证数据一致性。

5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?

答案:

预取是指列表滑动到距离底部还有一定数量的条目时,提前开始加载下一页数据,实现平滑滚动无感知。

  • 配置方式 :通过 Pager 配置中的 prefetchDistance 参数设置,默认值等于分页大小 pageSize。
  • 触发逻辑 :当已加载的最后一个可见条目距离列表末尾的距离小于 prefetchDistance 时,触发下一页加载。
  • 优化建议:分页大小建议设置为一屏可见数量的 2-3 倍,预取距离设置为 1-2 倍分页大小,平衡加载频率与滑动流畅度。

6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?

答案:

PagingDataAdapter 继承自 RecyclerView.Adapter,内置 DiffUtil 实现增量更新:

  • 普通 Adapter 刷新数据通常调用 notifyDataSetChanged(),全量刷新,性能差,无动画。
  • PagingDataAdapter 通过 DiffUtil 在后台线程计算新旧数据集的差异,只更新发生变化的 Item,性能更高,且自带 Item 动画。
  • PagingDataAdapter 接收 PagingData,自动处理分页加载的增量更新,无需开发者手动处理数据集的合并与差异计算。

7. Paging3 如何处理加载状态与错误重试?

答案:

Paging3 通过 LoadState 统一管理三种加载状态:首次加载(Refresh)、上一页加载(Prepend)、下一页加载(Append),每种状态包含 Loading、Error、NotLoading 三种情况。

  • 状态监听 :通过 adapter.loadStateFlow 或 addLoadStateListener 监听加载状态,用于展示加载中、空状态、错误视图。
  • 错误重试 :调用 adapter.retry() 重试所有失败的加载;也可单独处理首次加载失败的重试。
  • 底部加载状态 :可以通过 withLoadStateFooter() 给 Adapter 添加底部加载状态的 Footer,自动展示加载中、加载失败、没有更多数据。

8. Paging3 使用中有哪些常见的坑与注意事项?

答案:

  1. PagingSource 不可复用:PagingSource 是一次性的,数据失效后需要重新创建,因此 Pager 配置传入的是 PagingSource 工厂函数。
  2. 数据去重:分页加载要注意后端数据重复问题,建议通过唯一 id 去重,避免列表出现重复条目。
  3. 状态保存:配置变更后 PagingData 会保留,无需重新加载首页,ViewModel 中缓存 Flow 即可。
  4. 避免内存泄漏:不要在 PagingSource 中持有 Activity/View 上下文,防止内存泄漏。
  5. RemoteMediator 原子性:写入数据库和更新分页钥匙要放在同一个事务中,保证原子性,避免分页状态错乱。

9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数

pagingSourceFactory 负责「读本地数据,供给 UI 渲染」;remoteMediator 负责「本地不够的时候,去网上拉新数据写入本地」。UI 永远只消费 pagingSource 出来的数据。

pagingSourceFactory

它是PagingSource 的工厂 lambda 。

PagingSource 本身是一次性对象,一旦失效(下拉刷新、数据源变更)就不能复用,所以不能直接 new 一个传进去,要传一个工厂函数,需要的时候 Paging3 自动执行 lambda 生成全新 PagingSource。

在 Room+RemoteMediator 场景,这个 PagingSource 就是 Room DAO 返回的,只做查询本地数据库。列表滑动时 Paging3 从这个 PagingSource 拿数据交给 Adapter 渲染。

职责:读、供给 UI;数据源 = 本地 Room。

remoteMediator

远程中介,是一个附加的后台加载控制器 ,属于 Paging3 的可选参数,纯本地分页不需要它。

触发时机:当pagingSource读取本地数据读到末尾,没有更多本地数据了,Paging3 就会回调 RemoteMediator 的 load 方法,在这里执行网络请求,拿到网络数据写入 Room 数据库 ,同时更新分页 key。

写入成功后,Room 表变化,pagingSource 会感知数据库变更,自动加载新入库的数据,推给 UI。

职责:网络拉取 + 写本地数据库;不直接给 UI 返回数据。

两者协作流程(口述)

  1. 页面进来,Pager 执行 pagingSourceFactory,创建 Room 的 PagingSource 读取本地缓存数据展示到列表;
  2. 用户上滑,本地数据读到尽头;
  3. Paging3 触发 remoteMediator.load (),请求网络下一页;
  4. RemoteMediator 拿到网络数据,在事务中写入 Room;
  5. Room 表更新,pagingSource 监听到数据库变化,读取新入库数据,自动交给列表展示。

关键区分点(面试官爱问)

  1. pagingSource 是数据流主线 ,没有它列表就没有数据渲染;remoteMediator 是后台辅助,只负责填充本地缓存。
  2. remoteMediator不会直接返回数据给 Adapter,所有 UI 数据都来自 pagingSource(Room)。
  3. 只有「网络 + 本地缓存、离线优先」架构才需要 remoteMediator;纯网络分页只需要 pagingSourceFactory,不需要 RemoteMediator。

二. 代码demo

1. 数据实体类
kotlin 复制代码
// 列表条目数据实体,模拟文章数据
data class ArticleBean(
    val id: Int,          // 唯一主键,用于DiffUtil比对
    val title: String,    // 标题
    val content: String   // 内容
)

2. 分页数据源:PagingSource 实现
kotlin 复制代码
import androidx.paging.PagingSource
import androidx.paging.PagingState

/**
 * 分页数据源:负责单页数据的加载逻辑
 * 泛型1:页码类型(这里用Int);泛型2:数据条目类型
 */
class ArticlePagingSource(
    private val api: ArticleApi // 业务层网络请求接口
) : PagingSource<Int, ArticleBean>() {

    /**
     * 核心加载方法:每次分页加载都会触发
     * params:包含加载页码、每页条数、加载类型等信息
     * 返回:成功返回 LoadResult.Page,失败返回 LoadResult.Error
     */
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ArticleBean> {
        return try {
            // 获取当前页码,首次加载时key为null,使用初始页码1
            val currentPage = params.key ?: 1
            // 每页加载条目数,由Pager配置的pageSize传入
            val pageSize = params.loadSize

            // 执行网络请求,获取当前页数据
            val response = api.getArticleList(currentPage, pageSize)
            val dataList = response.data.list

            // 计算上一页页码,第一页没有上一页则为null
            val prevKey = if (currentPage > 1) currentPage - 1 else null
            // 计算下一页页码,数据为空/条数不足则代表到底,返回null
            val nextKey = if (dataList.isNullOrEmpty()) null else currentPage + 1

            // 封装分页结果返回给Paging3
            LoadResult.Page(
                data = dataList,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            // 加载异常,返回错误结果,Paging3统一管理错误状态
            LoadResult.Error(e)
        }
    }

    /**
     * 刷新锚点计算:列表刷新后,定位到刷新前的浏览位置
     */
    override fun getRefreshKey(state: PagingState<Int, ArticleBean>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

// 模拟的网络请求接口
interface ArticleApi {
    suspend fun getArticleList(page: Int, pageSize: Int): ApiResponse<List<ArticleBean>>
}

3. 列表适配器:PagingDataAdapter 实现
kotlin 复制代码
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView

/**
 * Paging3专属适配器,内置DiffUtil实现增量更新
 * 无需手动管理数据集合并、差异计算
 */
class ArticleAdapter : PagingDataAdapter<ArticleBean, ArticleAdapter.ArticleViewHolder>(DIFF_CALLBACK) {

    // DiffUtil比对规则:计算新旧数据差异,只更新变化的条目
    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ArticleBean>() {
            // 主键比对:判断两个条目是否为同一条数据
            override fun areItemsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
                return oldItem.id == newItem.id
            }

            // 内容比对:判断同一条数据的内容是否发生变化
            override fun areContentsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
                return oldItem == newItem
            }
        }
    }

    class ArticleViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.item_article, parent, false)
    ) {
        fun bind(bean: ArticleBean?) {
            bean?.let {
                itemView.tv_title.text = it.title
                itemView.tv_content.text = it.content
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(parent)
    }

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
        // getItem()是PagingDataAdapter提供的方法,获取对应位置的数据
        holder.bind(getItem(position))
    }
}

4. ViewModel 层:Pager 配置与数据流
kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow

class ArticleViewModel : ViewModel() {

    /**
     * 分页数据流:PagingData 的 Flow
     * Pager 是Paging3的配置入口,构建分页数据流
     */
    val articleFlow: Flow<PagingData<ArticleBean>> = Pager(
        config = PagingConfig(
            pageSize = 20,           // 每页加载的条目数量
            prefetchDistance = 40,   // 预取距离:距离底部还有多少条时,提前加载下一页
            enablePlaceholders = false // 关闭空占位符,列表不显示空白item
        ),
        // PagingSource工厂:每次刷新都会创建新的PagingSource实例
        pagingSourceFactory = { ArticlePagingSource(RetrofitClient.articleApi) }
    ).flow
        // 缓存到ViewModel作用域,配置变更(如旋转屏幕)不会重新加载数据
        .cachedIn(viewModelScope)
}

5. UI 层:Activity 中使用
kotlin 复制代码
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

class ArticleActivity : AppCompatActivity() {

    private val viewModel: ArticleViewModel by viewModels()
    private val adapter = ArticleAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_article)

        initRecyclerView()
        observePagingData()
        observeLoadState()
    }

    private fun initRecyclerView() {
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter
    }

    /**
     * 订阅分页数据流,提交给适配器渲染
     */
    private fun observePagingData() {
        lifecycleScope.launch {
            // collectLatest:只处理最新数据,避免旧数据积压
            viewModel.articleFlow.collectLatest { pagingData ->
                // 将分页数据提交给适配器,自动增量刷新
                adapter.submitData(pagingData)
            }
        }
    }

    /**
     * 监听加载状态:处理加载中、加载失败、空状态等UI展示
     */
    private fun observeLoadState() {
        lifecycleScope.launch {
            adapter.loadStateFlow.collectLatest { loadStates ->
                // loadStates包含三类状态:
                // refresh:首次加载/下拉刷新
                // append:上滑加载更多
                // prepend:加载上一页(极少场景)

                val refreshState = loadStates.refresh
                // 首次加载中
                if (refreshState is androidx.paging.LoadState.Loading) {
                    showGlobalLoading()
                } else {
                    hideGlobalLoading()
                }

                // 首次加载失败
                if (refreshState is androidx.paging.LoadState.Error) {
                    showErrorView(refreshState.error.message) {
                        adapter.retry() // 调用重试
                    }
                }
            }
        }
    }
}

三. Room + RemoteMediator 标准 Demo(离线优先分页架构)

这是 Paging3 最经典的分层缓存架构 ,遵循单一数据源原则:本地 Room 是唯一可信数据源,UI 只观察数据库数据;RemoteMediator 负责在本地数据不足时拉取网络数据并写入数据库,通过 Room 的响应式查询自动刷新列表。

1. 数据库实体类
kotlin 复制代码
import androidx.room.Entity
import androidx.room.PrimaryKey

/**
 * 文章表实体:本地缓存的核心数据
 */
@Entity(tableName = "article_table")
data class ArticleEntity(
    @PrimaryKey val id: Int,       // 唯一主键,用于DiffUtil和分页关联
    val title: String,
    val content: String,
    val publishTime: Long
)

/**
 * 分页钥匙表:记录每条数据对应的分页页码
 * 用于精准定位上一页/下一页,比全局存页码更可靠,支持刷新后位置恢复
 */
@Entity(tableName = "remote_keys_table")
data class RemoteKeysEntity(
    @PrimaryKey val articleId: Int, // 与文章表主键一一对应
    val prevKey: Int?,              // 上一页页码
    val nextKey: Int?               // 下一页页码
)

2. DAO 数据访问接口
kotlin 复制代码
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction

@Dao
interface ArticleDao {
    /**
     * 分页查询:返回PagingSource,Room原生支持
     * 表数据变更时自动触发PagingSource更新,UI自动刷新
     */
    @Query("SELECT * FROM article_table ORDER BY publishTime DESC")
    fun getArticlePagingSource(): PagingSource<Int, ArticleEntity>

    /**
     * 批量插入文章数据
     */
    @Insert
    suspend fun insertArticles(articles: List<ArticleEntity>)

    /**
     * 清空文章表(刷新时用)
     */
    @Query("DELETE FROM article_table")
    suspend fun clearArticles()
}

@Dao
interface RemoteKeysDao {
    /**
     * 根据文章ID查询对应的分页钥匙
     */
    @Query("SELECT * FROM remote_keys_table WHERE articleId = :articleId")
    suspend fun getRemoteKeyByArticleId(articleId: Int): RemoteKeysEntity?

    /**
     * 批量插入分页钥匙
     */
    @Insert
    suspend fun insertRemoteKeys(keys: List<RemoteKeysEntity>)

    /**
     * 清空分页钥匙表
     */
    @Query("DELETE FROM remote_keys_table")
    suspend fun clearRemoteKeys()
}

3. Room 数据库类
kotlin 复制代码
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(
    entities = [ArticleEntity::class, RemoteKeysEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun articleDao(): ArticleDao
    abstract fun remoteKeysDao(): RemoteKeysDao
}

4. 模拟网络请求接口
kotlin 复制代码
data class ApiResponse<T>(
    val data: List<T>,
    val page: Int,
    val total: Int
)

interface ArticleApi {
    /**
     * 网络分页请求
     * @param page 页码
     * @param pageSize 每页条数
     */
    suspend fun getArticleList(page: Int, pageSize: Int): ApiResponse<ArticleEntity>
}

5. 核心:RemoteMediator 实现
kotlin 复制代码
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
    private val database: AppDatabase,
    private val api: ArticleApi
) : RemoteMediator<Int, ArticleEntity>() {

    private val articleDao = database.articleDao()
    private val keysDao = database.remoteKeysDao()

    /**
     * 核心加载方法:分页加载触发时回调
     * @param loadType 加载类型:REFRESH刷新、APPEND加载更多、PREPEND加载上一页
     * @param state 当前分页状态,包含已加载数据、分页配置等
     * @return MediatorResult:成功/失败,成功时标记是否已到最后一页
     */
    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, ArticleEntity>
    ): MediatorResult {
        return try {
            // 1. 根据加载类型,计算当前请求的页码
            val currentPage = when (loadType) {
                // 首次加载/下拉刷新:从第1页开始
                LoadType.REFRESH -> 1
                
                // 上滑加载更多:取最后一条数据对应的下一页页码
                LoadType.APPEND -> {
                    // 获取已加载的最后一条数据
                    val lastItem = state.lastItemOrNull()
                        ?: return MediatorResult.Success(endOfPagination = true)
                    
                    // 查询该数据对应的下一页页码
                    val nextKey = keysDao.getRemoteKeyByArticleId(lastItem.id)?.nextKey
                        ?: return MediatorResult.Success(endOfPagination = true)
                    nextKey
                }
                
                // 加载上一页:普通列表场景极少用到,直接返回无更多数据
                LoadType.PREPEND -> return MediatorResult.Success(endOfPagination = true)
            }

            // 2. 执行网络请求,获取分页数据
            val pageSize = state.config.pageSize
            val response = api.getArticleList(currentPage, pageSize)
            val dataList = response.data
            val isEndOfList = dataList.isEmpty()

            // 3. 数据库事务:写入数据 + 更新分页钥匙,必须原子操作
            database.withTransaction {
                // 刷新场景:先清空旧数据和旧钥匙
                if (loadType == LoadType.REFRESH) {
                    articleDao.clearArticles()
                    keysDao.clearRemoteKeys()
                }

                // 计算每条数据对应的上一页/下一页页码
                val prevKey = if (currentPage > 1) currentPage - 1 else null
                val nextKey = if (isEndOfList) null else currentPage + 1
                val remoteKeys = dataList.map { article ->
                    RemoteKeysEntity(
                        articleId = article.id,
                        prevKey = prevKey,
                        nextKey = nextKey
                    )
                }

                // 批量插入数据和分页钥匙
                keysDao.insertRemoteKeys(remoteKeys)
                articleDao.insertArticles(dataList)
            }

            // 4. 返回加载结果,标记是否已到最后一页
            MediatorResult.Success(endOfPagination = isEndOfList)

        } catch (e: Exception) {
            // 加载异常,返回错误结果,Paging3统一管理重试
            MediatorResult.Error(e)
        }
    }

    /**
     * 初始化加载策略:可选配置
     * 默认返回LAUNCH_INITIAL_REFRESH,进入页面自动触发首次刷新
     */
    override suspend fun initialize(): InitializeAction {
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }
}

6. ViewModel 层
kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow

class ArticleViewModel : ViewModel() {

    // 注入数据库和API实例(实际项目用Hilt注入)
    private val database = AppDatabase.getInstance()
    private val api = RetrofitClient.articleApi

    /**
     * 分页数据流
     * 配置remoteMediator后,分页逻辑由「本地PagingSource + RemoteMediator」共同驱动
     */
val articleFlow: Flow<PagingData<ArticleEntity>> = Pager(
    // 分页核心配置
    config = PagingConfig(
        pageSize = 20,                 // 每页加载条目数量,同时作为预取计算基准
        prefetchDistance = 40,         // 预取距离:距离列表底部还剩40条时,提前触发下一页加载,做到滑动无感
        enablePlaceholders = false     // 关闭占位符,不渲染空白item;开启时会预先绘制空占位,适合已知总条数场景
    ),
    // 远程中介:处理网络分页逻辑。本地数据加载完毕后,RemoteMediator自动触发网络请求拉取下一页,写入Room
    remoteMediator = ArticleRemoteMediator(database, api),
    // PagingSource工厂函数:每次刷新/重建分页时,生成新的本地PagingSource,从Room查询数据
    // 注意:必须传【工厂lambda】,不能直接传实例,因为PagingSource是一次性对象,失效后需要新建
    pagingSourceFactory = { database.articleDao().getArticlePagingSource() }
)
.flow // Pager暴露flow,发射PagingData分页数据包
.cachedIn(viewModelScope) // 将Flow缓存到ViewModel协程作用域。屏幕旋转等配置变更时,复用已有分页数据,不重新请求网络/数据库
}

7. UI 层(与纯网络版完全一致)

Adapter 直接复用纯网络分页的 PagingDataAdapter,Activity 订阅数据流的逻辑也完全相同,UI 层完全感知不到网络层的存在,只与本地数据库交互。

kotlin 复制代码
// 核心订阅逻辑,其余代码与纯网络版一致
private fun observePagingData() {
    lifecycleScope.launch {
        viewModel.articleFlow.collectLatest { pagingData ->
            adapter.submitData(pagingData)
        }
    }
}

架构核心总结(面试可直接说)

  1. 单一数据源:UI 只从 Room 取数据,网络数据仅写入数据库,保证在线 / 离线数据一致性。
  2. 职责分离:PagingSource 负责本地分页,RemoteMediator 负责网络拉取与缓存写入,互不耦合。
  3. 原子性保证:数据写入与分页钥匙更新放在同一事务,避免分页状态错乱。
  4. 响应式驱动:Room 数据变更自动触发列表刷新,无需手动同步网络数据与 UI 数据。
相关推荐
企业数字化笔记1 小时前
租户停用后数据和任务怎么处理?冻结、恢复与保留策略的实现
android·java·spring boot
.道阻且长.1 小时前
Linux软件包管理器
linux·运维·ubuntu·职场和发展·centos
考研保研资料分享1 小时前
211英语专业跨保清华法学:2022年法学面试与口译考核复盘
面试·职场和发展
考研保研资料分享2 小时前
2024双非材料保研复旦:五校面试题与科研复盘
面试·职场和发展
救救孩子把11 小时前
AI面试八股文-21_21-多模态Agent面试题-Vision-Language-Agent
人工智能·面试·职场和发展·llava·多模态agent
小程故事多_8011 小时前
从 Tokenizer 到 Next Token,穿透数据流,完整理解 Transformer 底层逻辑
android·深度学习·transformer
ShineWinsu13 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
救救孩子把14 小时前
AI面试八股文-13_12-AI 框架与运维面试题
运维·人工智能·面试
Nil20814 小时前
leetcode 5最长回文子串
算法·leetcode·职场和发展