Android Academy · 课时

PagingSource 与 Pager

定义页面的加载方式

第 2 / 4 课13 个步骤

PagingSource 与 Pager 是 CoddyKit 上的免费 Android Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。

定义页面的加载方式

要从网络 API 分页获取数据,需要编写一个 PagingSource。它需要回答 Paging 3 的两个问题:

  • 如何加载指定键对应的页面?
  • 用户刷新时,应从哪里继续加载?

然后,将它包装在一个 Pager 中,将其转换为 Flow<PagingData>。

PagingSource 类型参数

PagingSource<Key, Value> 接受两个类型参数:

  • Key - 用于标识页面。对于页码 API,它是 Int;对于游标 API,它是一个 String 令牌。
  • Value - 项目类型,例如 Article。
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

实现 load()

load() 是一个 suspend 函数。它接收 params.key(要获取的页面),并返回一个 LoadResult。

成功时,返回包含项目以及上一页和下一页键的 LoadResult.Page。某个键为 null,表示该方向没有页面。

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

为什么 prevKey 和 nextKey 很重要

用户向下滚动时,Paging 使用 nextKey 向前加载;使用 prevKey 向后加载(从列表中间开始时很有用)。

将 nextKey 返回为 null 会告诉 Paging 没有更多页面,因此它会停止请求。忘记这一点可能导致无限的空请求。

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

实现 getRefreshKey()

列表刷新时(下拉刷新或失效),Paging 需要知道应重新加载哪个页面,以便用户大致停留在原来的位置。

getRefreshKey() 使用当前的 anchorPosition(距离视口最近的项目)来选择合理的键。

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    return state.anchorPosition?.let { anchor ->
        val closestPage = state.closestPageToPosition(anchor)
        closestPage?.prevKey?.plus(1)
            ?: closestPage?.nextKey?.minus(1)
    }
}

妥善处理错误

请将网络调用放在 try/catch 中,并在失败时返回 LoadResult.Error(e)。Paging 会在界面中将其显示为 LoadState.Error,这样您就可以显示重试按钮。

不要让异常从 load() 中逸出 - 请捕获异常,并将其转换为 LoadResult.Error。

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

创建 Pager

Pager 将您的 PagingConfig 与一个用于创建全新 PagingSource 的工厂连接起来。它的 .flow 属性是一个由界面收集的 Flow<PagingData>。

工厂 lambda 每次都必须创建一个新的数据源,因为 Paging 会在刷新时使当前数据源失效并重新创建它。

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

用于 ViewModel 的 cachedIn

收集 PagingData 是一次性操作;再次收集会重新开始加载。为了在配置更改后继续工作,并让多个收集者共享数据,请通过 cachedIn 将数据流缓存到 viewModelScope 中。

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize 与 pageSize

请注意,load() 读取的是 params.loadSize,而不是您配置的 pageSize。

第一次加载时,Paging 可能会请求更大的初始数据块(由 PagingConfig 中的 initialLoadSize 控制,默认是页面大小的 3 倍)。请始终将 params.loadSize 传给您的 API,以便请求大小与 Paging 期望返回的数据量一致。

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

基于游标的 API

并非所有 API 都使用页码。有些 API 会返回指向下一页的游标或令牌。模式完全相同 - 只需将 Key 类型改为 String,并使用响应中的令牌。

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

整合起来

现在您已经拥有完整的数据层:一个加载单个页面的 PagingSource、一个流式传送页面的 Pager,以及一个缓存数据流的 ViewModel。

界面层只需收集 viewModel.articles - 下一课将使用 Compose LazyColumn 呈现这些内容。

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

快速检查

在基于页码的 PagingSource 中,从 load() 返回 nextKey = null 表示什么?

回顾:PagingSource 与 Pager

您已经构建了分页所需的数据层:

  • PagingSource<Key, Value> 实现 load() 和 getRefreshKey()
  • load() 返回带有 prevKey/nextKey 的 LoadResult.Page,或返回 LoadResult.Error
  • 将键设为 null 会停止该方向上的分页
  • Pager(config, factory).flow 生成一个 Flow<PagingData>
  • cachedIn(viewModelScope) 会在配置更改后继续保留数据

下一步:在 Compose 列表中呈现这个数据流。

免费开始

用 AI 导师学习 Kotlin — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
36
课程
152

常见问题解答

「PagingSource 与 Pager」课时是免费的吗?

是的 — 「PagingSource 与 Pager」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。

「PagingSource 与 Pager」这节课中我会学到什么?

定义页面的加载方式 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Android Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「PagingSource 与 Pager」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Android Academy 课中编写并运行代码吗?

能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 为什么需要分页
  2. PagingSource 与 Pager
  3. 在 Compose 列表中实现分页
  4. RemoteMediator 与缓存
← 返回 Android Academy