Paging 3.0 + Kotlin 分页加载终极指南:告别手动分页,拥抱现代化列表

avatar
莫雨IP属地:上海
02026-01-27:12:34:09字数 5975阅读 1

在 Android 开发中,“分页加载”几乎是每个 App 都绕不开的需求。无论是微博的时间线、电商的商品列表,还是新闻资讯流,数据量一大,我们就不能一次性加载全部内容——内存会炸、流量会爆、用户体验也会变差

过去,我们可能用 RecyclerView.OnScrollListener 手动监听滚动、维护页码、处理加载状态……代码冗长、逻辑复杂、容易出错。

但现在,Google 官方推出的 Paging 3.0(基于 Kotlin 协程和 Flow)彻底改变了这一切。

今天,我们就手把手教你用 Paging 3.0 + Kotlin 实现一套健壮、简洁、可测试的分页加载方案。


一、为什么选择 Paging 3.0?

Paging 3.0 是 Google Jetpack 的一部分,相比旧版(Paging 2.x),它有三大核心优势:

全面协程化:使用 Flowsuspend 函数,天然支持异步与取消
内存高效:自动管理页面缓存,避免重复请求
状态统一:内置加载状态(Loading / Error / Refresh)、重试机制、占位符支持
架构清晰:分离数据源、UI 层和业务逻辑,符合 MVVM 原则

💡 注意:Paging 3.0 要求最低 API 21,且强烈建议配合 Room 或 Retrofit 使用。


二、核心组件速览

Paging 3.0 的架构非常清晰,主要由三部分组成:

组件作用
PagingSource数据源实现,负责从网络或数据库加载分页数据
Pager配置分页参数(如页面大小),并生成 Flow<PagingData<T>>
PagingDataAdapter继承自 RecyclerView.Adapter,自动处理分页数据的 Diff 和更新

整个数据流如下:

PagingSource → Pager → Flow<PagingData> → collect → submitData() → PagingDataAdapter

三、实战:从零搭建一个分页列表

假设我们要实现一个“用户列表”,每次加载 20 条,支持下拉刷新和上拉加载更多。

第一步:添加依赖(build.gradle)

implementation "androidx.paging:paging-runtime:3.3.0"
// 如果用 RxJava 或 Guava,可选 paging-rxjava3 / paging-guava

第二步:定义数据模型

data class User(
    val id: Int,
    val name: String,
    val email: String
)

第三步:实现 PagingSource

这是最关键的一步!你需要继承 PagingSource<Key, Value>

  • Key:通常是页码(Int)或游标(String)
  • Value:你的数据类型(如 User
class UserPagingSource(
    private val api: UserService // 假设你有一个 Retrofit 接口
) : PagingSource<Int, User>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        return try {
            // 获取当前页码,默认第一页为 1
            val page = params.key ?: 1

            // 调用网络接口(必须是 suspend 函数)
            val response = api.getUsers(page = page, size = params.loadSize)

            LoadResult.Page(
                data = response.users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    // 可选:用于识别数据源(比如缓存时)
    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.let { anchorPos ->
            state.closestItemToPosition(anchorPos)?.id?.minus(1)
        }
    }
}

🔍 说明:

  • params.loadSize 由 Pager 配置决定(通常就是 pageSize)
  • prevKey / nextKey 控制是否还能向前/向后加载
  • 返回 LoadResult.Page 表示成功,LoadResult.Error 表示失败

第四步:在 Repository 中创建 Pager

class UserRepository(private val api: UserService) {

    fun getUsers(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false, // 是否显示占位符
                maxSize = 200 // 缓存最大条目数
            ),
            pagingSourceFactory = { UserPagingSource(api) }
        ).flow
    }
}

第五步:ViewModel 中暴露数据流

@HiltViewModel
class UserListViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    val users = repository.getUsers().cachedIn(viewModelScope)
}

cachedIn(viewModelScope) 很重要!它会让 Flow 在 ViewModel 生命周期内复用,避免配置变更(如旋转屏幕)时重复请求。

第六步:实现 PagingDataAdapter

class UserAdapter : PagingDataAdapter<User, UserViewHolder>(UserDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false))
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        getItem(position)?.let { user -> holder.bind(user) }
    }
}

// DiffUtil 回调
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean = oldItem == newItem
}

第七步:Activity / Fragment 中收集数据

class UserListFragment : Fragment() {

    private lateinit var adapter: UserAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        adapter = UserAdapter()
        recyclerView.adapter = adapter

        lifecycleScope.launch {
            viewModel.users.collectLatest { pagingData ->
                adapter.submitData(pagingData)
            }
        }
    }
}

⚠️ 注意:一定要用 collectLatest!它会在新数据到来时自动取消旧的收集,避免竞态问题。


四、进阶:处理加载状态与错误重试

Paging 3.0 内置了强大的状态监听能力。

监听加载状态(如显示“正在加载更多”)

lifecycleScope.launch {
    adapter.loadStateFlow.collect { loadStates ->
        val refreshState = loadStates.refresh
        val appendState = loadStates.append // 上拉加载更多

        when {
            refreshState is LoadState.Loading -> showLoading()
            refreshState is LoadState.Error -> showError(refreshState.error)
            appendState.endOfPaginationReached -> hideLoadMoreIndicator()
        }
    }
}

添加“重试”按钮

retryButton.setOnClickListener {
    adapter.retry() // 自动重试失败的加载
}

下拉刷新支持

配合 SwipeRefreshLayout

swipeRefreshLayout.setOnRefreshListener {
    adapter.refresh() // 触发重新加载第一页
}

// 监听刷新完成以关闭刷新动画
lifecycleScope.launch {
    adapter.loadStateFlow.collect { loadStates ->
        swipeRefreshLayout.isRefreshing = loadStates.refresh is LoadState.Loading
    }
}

五、常见问题 & 最佳实践

❓ Q1:如何实现“滑到底部自动加载”?

A:Paging 3.0 默认就支持!只要 nextKey != null,滑到底部就会自动触发 load()

❓ Q2:能同时从网络和数据库加载吗?

A:可以!推荐使用 RemoteMediator(适用于 Room + 网络混合场景),但逻辑更复杂,适合离线优先应用。

❓ Q3:如何预加载(提前加载下一页)?

A:通过 PagingConfig.prefetchDistance 设置(默认是 pageSize 的一半)。

✅ 最佳实践:

  • 网络请求务必用 suspend 函数(Retrofit 2.6+ 支持)
  • 不要在 PagingSource 中持有 Activity/Fragment 引用
  • 使用 cachedIn(viewModelScope) 避免重复请求
  • collectLatest 而不是 collect

六、结语

Paging 3.0 把复杂的分页逻辑封装得如此优雅,让我们开发者可以专注业务,而非样板代码

它不仅是“分页工具”,更是现代 Android 架构(协程 + Flow + MVVM)的最佳实践体现。

如果你还在手写分页逻辑,是时候升级了!


🎁 附:完整示例代码已开源
GitHub 地址:github.com/yourname/paging3-demo

👉 关注【Kotlin 实战派】,回复“Paging3”获取完整项目模板!


下期预告:《Room + Paging 3.0 实现离线优先的分页列表》


让代码更 Kotlin,让开发更高效。

总资产 0
暂无其他文章

热门文章

暂无热门文章