(已弃用) Android Paging 基础知识

1. 简介

您将学到的内容

  • Paging 库的主要组成部分是什么。
  • 如何将 Paging 库添加到您的项目。

您将构建什么

在此 Codelab 中,您将从一个已显示文章列表的示例应用开始。该列表是静态的,包含 500 篇文章,所有这些文章都保存在手机内存中

7d256d9c74e3b3f5.png

随着您逐步完成此 Codelab,您将

  • ...了解分页的概念。
  • ...了解 Paging 库的核心组件。
  • ...了解如何使用 Paging 库实现分页。

完成后,您将拥有一个应用

  • ...成功实现了分页。
  • ...在正在提取更多数据时有效地向用户进行沟通。

以下是我们最终完成的 UI 的快速预览

6277154193f7580.gif

您将需要

建议掌握

2. 设置您的环境

在此步骤中,您将下载整个 Codelab 的代码,然后运行一个简单的示例应用。

为了让您尽快开始,我们为您准备了一个入门项目,供您在此基础上构建。

如果您已安装 git,只需运行以下命令即可。要检查 git 是否已安装,请在终端或命令行中输入 git --version 并验证其是否正确执行。

 git clone https://github.com/googlecodelabs/android-paging

如果您没有 git,可以点击以下按钮下载此 Codelab 的所有代码:

代码组织在两个文件夹中,basicadvanced。对于此 Codelab,我们只关注 basic 文件夹。

basic 文件夹中,还有另外两个文件夹:startend。我们将开始处理 start 文件夹中的代码,到 Codelab 结束时,start 文件夹中的代码应与 end 文件夹中的代码相同。

  1. 在 Android Studio 中打开 basic/start 目录中的项目。
  2. 在设备或模拟器上运行 app 运行配置。

89af884fa2d4e709.png

我们应该能看到一个文章列表!滚动到列表末尾以验证列表是静态的——换句话说,当我们到达列表末尾时不会提取更多项目。滚回到顶部以验证我们仍然拥有所有项目。

3. 分页简介

向用户显示信息最常见的方式之一是使用列表。然而,有时这些列表只提供了用户可用的全部内容的一小部分窗口。当用户滚动浏览可用信息时,通常期望会提取更多数据来补充已经看到的信息。每次提取数据都需要高效且无缝,这样增量加载才不会损害用户体验。增量加载还提供了性能优势,因为应用无需一次性在内存中保留大量数据。

这种逐步提取信息的过程称为 分页 (pagination),其中每个 页 (page) 对应于要提取的数据块。要请求一个页面,被分页的数据源通常需要一个定义所需信息的 查询 (query)。此 Codelab 的其余部分将介绍 Paging 库,并演示它如何帮助您快速有效地在应用中实现分页。

Paging 库的核心组件

Paging 库的核心组件如下

  • PagingSource - 用于加载特定页面查询的数据块的基类。它是数据层的一部分,通常从 DataSource 类公开,随后由 Repository 公开,供 ViewModel 使用。
  • PagingConfig - 一个定义决定分页行为参数的类。这包括页面大小、是否启用占位符等。
  • Pager - 负责生成 PagingData 流的类。它依赖于 PagingSource 来完成此操作,并且应该在 ViewModel 中创建。
  • PagingData - 分页数据的容器。每次数据刷新都会有单独对应的 PagingData 发出,并由其自己的 PagingSource 支持。
  • PagingDataAdapter - RecyclerView.Adapter 的子类,用于在 RecyclerView 中显示 PagingDataPagingDataAdapter 可以通过工厂方法连接到 Kotlin FlowLiveData、RxJava Flowable、RxJava Observable,甚至静态列表。 PagingDataAdapter 监听内部 PagingData 加载事件,并在加载页面时有效地更新 UI。

566d0f6506f39480.jpeg

在接下来的部分,您将实现上述每个组件的示例。

4. 项目概述

该应用当前形式显示了一个静态文章列表。每篇文章都有标题、描述和创建日期。静态列表适用于少量项目,但随着数据集变大,它的可扩展性不佳。我们将通过使用 Paging 库实现分页来解决这个问题,但首先我们先回顾一下应用中已有的组件。

该应用遵循 应用架构指南 中推荐的架构。以下是您在每个包中将找到的内容

数据层

  • ArticleRepository: 负责提供文章列表并将它们保存在内存中。
  • Article: 表示 数据模型 (data model) 的类,是数据层中提取信息的表示形式。

UI 层:

  • The Activity, RecyclerView.Adapter, and RecyclerView.ViewHolder: 负责在 UI 中显示列表的类。
  • The ViewModel: 负责创建 UI 需要显示的状态的状态持有者。

repository 通过 articleStream 字段在一个 Flow 中公开其所有文章。UI 层中的 ArticleViewModel 会读取此内容,然后使用 state 字段(一个 StateFlow)准备好供 ArticleActivity 中的 UI 使用。

从 repository 将文章公开为 Flow 使得 repository 能够在文章随时间变化时更新呈现的文章。例如,如果文章的标题发生变化,此变化可以轻松地传达给 articleStream 的收集者。在 ViewModel 中使用 StateFlow 作为 UI 状态可确保即使我们停止收集 UI 状态(例如,在配置更改期间重新创建 Activity 时),我们也可以在再次开始收集时从上次离开的地方继续。

如前所述,repository 中当前的 articleStream 只显示当天的新闻。虽然这对于某些用户来说可能足够,但其他用户在滚动浏览完当天所有可用的文章后,可能想要查看更早的文章。这种期望使得文章显示成为分页的理想候选。我们应该探索分页查看文章的其他原因包括以下几点

  • ViewModel 将所有已加载的项目保存在内存中的 items StateFlow 中。当数据集非常大时,这是一个主要问题,因为它会影响性能。
  • 当列表中的一篇文章或多篇文章发生变化时更新它们,随着文章列表的增大而变得更加昂贵。

Paging 库有助于解决所有这些问题,同时提供一致的 API 来在您的应用中逐步提取数据(分页)。

5. 定义数据源

实现分页时,我们希望确保满足以下条件

  • 正确处理来自 UI 的数据请求,确保不会针对同一查询同时触发多个请求。
  • 在内存中保留可管理数量的已检索数据。
  • 触发请求以提取更多数据来补充我们已提取的数据。

我们可以通过 PagingSource 实现所有这些。 PagingSource 通过指定如何以增量块检索数据来定义数据源。然后,PagingData 对象响应用户在 RecyclerView 中滚动时生成的加载提示,从 PagingSource 拉取数据。

我们的 PagingSource 将加载文章。在 data/Article.kt 中,您将找到如下定义的模型:

data class Article(
    val id: Int,
    val title: String,
    val description: String,
    val created: LocalDateTime,
)

要构建 PagingSource,您需要定义以下内容

  • 分页键的类型 - 我们用于请求更多数据的页面查询类型的定义。在我们的例子中,由于文章 ID 保证有序且递增,因此我们在特定文章 ID 之后或之前提取文章。
  • 加载的数据类型 - 每个页面返回一个文章的 List,因此类型是 Article
  • 数据从何处检索 - 通常,这可以是数据库、网络资源或任何其他分页数据源。然而,在此 Codelab 中,我们使用的是本地生成的数据。

data 包中,让我们在一个名为 ArticlePagingSource.kt 的新文件中创建一个 PagingSource 实现

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource : PagingSource<Int, Article>() { 
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        TODO("Not yet implemented")
    }
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? { 
        TODO("Not yet implemented")
    }
}

PagingSource 要求我们实现两个函数:load()getRefreshKey()

Paging 库将调用 load() 函数,以便在用户滚动时异步提取更多要显示的数据。 LoadParams 对象包含与加载操作相关的信息,包括以下内容

  • 要加载页面的键 - 如果这是第一次调用 load()LoadParams.key 将为 null。在这种情况下,您将必须定义初始页面键。对于我们的项目,我们使用文章 ID 作为键。我们还在 ArticlePagingSource 文件顶部添加一个 STARTING_KEY 常量 0 作为初始页面键。
  • 加载大小 - 请求加载的项目数量。

The load() 函数返回一个 LoadResultLoadResult 可以是以下类型之一

  • 如果结果成功,则为 LoadResult.Page
  • 如果发生错误,则为 LoadResult.Error
  • 如果 PagingSource 应因无法再保证其结果的完整性而被废弃,则为 LoadResult.Invalid

A LoadResult.Page 有三个必需参数

  • data: 提取的项目列表 (List)。
  • prevKey: 如果 load() 方法需要提取当前页面 之前 的项目,则使用的键。
  • nextKey: 如果 load() 方法需要提取当前页面 之后 的项目,则使用的键。

...以及两个可选参数

  • itemsBefore: 在加载数据之前显示的占位符数量。
  • itemsAfter: 在加载数据之后显示的占位符数量。

我们的加载键是 Article.id 字段。我们可以将其用作键,因为每篇文章的 Article ID 都递增 1;也就是说,文章 ID 是连续单调递增的整数。

如果对应方向没有更多数据可加载,则 nextKeyprevKeynull。在我们的例子中,对于 prevKey

  • 如果 startKeySTARTING_KEY 相同,我们返回 null,因为无法在此键之前加载更多项目。
  • 否则,我们取列表中的第一个项目,并在其之前加载 LoadParams.loadSize 个项目,确保永远不会返回小于 STARTING_KEY 的键。我们通过定义 ensureValidKey() 方法来做到这一点。

添加以下函数,用于检查分页键是否有效

class ArticlePagingSource : PagingSource<Int, Article>() {
   ... 
   /**
     * Makes sure the paging key is never less than [STARTING_KEY]
     */
    private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}

对于 nextKey

  • 由于我们支持加载无限项目,因此我们传入 range.last + 1

另外,由于每篇文章都有一个 created 字段,我们还需要为其生成一个值。将以下内容添加到文件顶部

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
}

有了所有这些代码,我们现在就可以实现 load() 函数了

import kotlin.math.max
...

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        // Start paging with the STARTING_KEY if this is the first load
        val start = params.key ?: STARTING_KEY
        // Load as many items as hinted by params.loadSize
        val range = start.until(start + params.loadSize)

        return LoadResult.Page(
            data = range.map { number ->
                Article(
                    // Generate consecutive increasing numbers as the article id
                    id = number,
                    title = "Article $number",
                    description = "This describes article $number",
                    created = firstArticleCreatedTime.minusDays(number.toLong())
                )
            },
           
            // Make sure we don't try to load items behind the STARTING_KEY
            prevKey = when (start) {
                STARTING_KEY -> null
                else -> ensureValidKey(key = range.first - params.loadSize)
            },
            nextKey = range.last + 1
        )
    }

    ...
}

接下来我们需要实现 getRefreshKey()。当 Paging 库需要为 UI 重新加载项目时会调用此方法,因为其底层 PagingSource 中的数据已更改。这种 PagingSource 的底层数据发生更改并需要在 UI 中更新的情况称为 失效 (invalidation)。失效时,Paging 库会创建一个新的 PagingSource 来重新加载数据,并通过发出新的 PagingData 通知 UI。我们将在后面的章节中了解更多关于失效的信息。

从新的 PagingSource 加载时,会调用 getRefreshKey() 以提供新的 PagingSource 应开始加载的键,确保用户刷新后不会丢失在列表中的当前位置。

Paging 库中的失效发生有两个原因

  • 您调用了 PagingAdapter 上的 refresh()
  • 您调用了 PagingSource 上的 invalidate()

返回的键(在我们的例子中是 Int)将通过 LoadParams 参数传递给新 PagingSourceload() 方法的下一次调用。为了防止失效后项目跳动,我们需要确保返回的键会加载足够的项目以填满屏幕。这增加了新项目集包含已失效数据中存在的项目的可能性,这有助于保持当前的滚动位置。让我们看看我们应用中的实现

   // The refresh key is used for the initial load of the next PagingSource, after invalidation
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // In our case we grab the item closest to the anchor position
        // then return its id - (state.config.pageSize / 2) as a buffer
        val anchorPosition = state.anchorPosition ?: return null
        val article = state.closestItemToPosition(anchorPosition) ?: return null
        return ensureValidKey(key = article.id - (state.config.pageSize / 2))
    }

在上面的代码片段中,我们使用了 PagingState.anchorPosition。如果您想知道 Paging 库如何知道要提取更多项目,这就是线索!当 UI 尝试从 PagingData 读取项目时,它会尝试在某个索引处读取。如果读取到数据,则将数据显示在 UI 中。但是,如果没有数据,则 Paging 库就知道需要提取数据来满足失败的读取请求。成功读取数据的最后一个索引就是 anchorPosition

刷新时,我们获取最接近 anchorPositionArticle 的键,用作加载键。这样,当我们从新的 PagingSource 重新开始加载时,提取的项目集包含已加载的项目,从而确保流畅一致的用户体验。

完成这些后,您就完全定义了一个 PagingSource。下一步是将其连接到 UI。

6. 为 UI 生成 PagingData

在当前的实现中,我们在 ArticleRepository 中使用 Flow<List<Article>> 来将加载的数据公开给 ViewModelViewModel 再通过 stateIn 运算符维护数据的始终可用状态,以便暴露给 UI。

使用 Paging 库,我们将改为从 ViewModel 公开一个 Flow<PagingData<Article>>PagingData 是一种类型,它包装了我们已加载的数据,并帮助 Paging 库决定何时提取更多数据,同时确保我们不会两次请求同一页面。

要构建 PagingData,我们将使用 Pager 类中的几种不同构建器方法之一,具体取决于我们希望使用哪种 API 将 PagingData 传递给应用的其他层

  • Kotlin Flow - 使用 Pager.flow
  • LiveData - 使用 Pager.liveData
  • RxJava Flowable - 使用 Pager.flowable
  • RxJava Observable - 使用 Pager.observable

由于我们已经在应用中使用了 Flow,我们将继续采用这种方法;但不再使用 Flow<List<Article>>,我们将使用 Flow<PagingData<Article>>

无论您使用哪种 PagingData 构建器,都必须传递以下参数

  • PagingConfig。此类设置了关于如何从 PagingSource 加载内容的选项,例如预加载多远、初始加载的大小请求等。您唯一必须定义的必需参数是页面大小——每页应该加载多少项目。默认情况下,Paging 会将加载的所有页面保存在内存中。为了确保用户滚动时不会浪费内存,请在 PagingConfig 中设置 maxSize 参数。默认情况下,如果 Paging 可以计算未加载的项目并且 enablePlaceholders 配置标志为 true,Paging 会将 null 项目作为尚未加载内容的占位符返回。这样,您就可以在适配器中显示占位视图。为了简化本 Codelab 的工作,我们通过传递 enablePlaceholders = false 来禁用占位符。
  • 一个定义如何创建 PagingSource 的函数。在我们的例子中,我们将创建一个 ArticlePagingSource,因此我们需要一个函数来告诉 Paging 库如何做到这一点。

让我们修改 ArticleRepository

更新 ArticleRepository

  • 删除 articlesStream 字段。
  • 添加一个名为 articlePagingSource() 的方法,该方法返回我们刚刚创建的 ArticlePagingSource
class ArticleRepository {

    fun articlePagingSource() = ArticlePagingSource()
}

清理 ArticleRepository

Paging 库为我们做了很多事情

  • 处理内存缓存。
  • 当用户接近列表末尾时请求数据。

这意味着 ArticleRepository 中的所有其他内容都可以删除,除了 articlePagingSource()。您的 ArticleRepository 文件现在应该如下所示

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource

class ArticleRepository {
    fun articlePagingSource() = ArticlePagingSource()
}

现在您的 ArticleViewModel 中应该有编译错误。让我们看看需要进行哪些更改!

7. 在 ViewModel 中请求和缓存 PagingData

在解决编译错误之前,让我们回顾一下 ViewModel

class ArticleViewModel(...) : ViewModel() {

    val items: StateFlow<List<Article>> = ...
}

要在 ViewModel 中集成 Paging 库,我们将把 items 的返回类型从 StateFlow<List<Article>> 更改为 Flow<PagingData<Article>>。为此,首先在文件顶部添加一个名为 ITEMS_PER_PAGE 的私有常量

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel {
    ...
}

接下来,我们将 items 更新为 Pager 实例的输出结果。我们通过向 Pager 传递两个参数来完成此操作

  • 一个 PagingConfig,其 pageSizeITEMS_PER_PAGE 并禁用占位符
  • 一个 PagingSourceFactory,它提供我们刚刚创建的 ArticlePagingSource 实例。
class ArticleViewModel(...) : ViewModel() {

   val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        ...
}

接下来,为了在配置或导航更改时维护分页状态,我们使用 cachedIn() 方法并向其传入 androidx.lifecycle.viewModelScope

完成上述更改后,我们的 ViewModel 应该如下所示

package com.example.android.codelabs.paging.ui

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 com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel(
    private val repository: ArticleRepository,
) : ViewModel() {

    val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        .cachedIn(viewModelScope)
}

关于 PagingData 需要注意的另一件事是,它是一个自包含的类型,包含要显示在 RecyclerView 中的数据的可变更新流。每次 PagingData 的发出都是完全独立的,如果底层数据集的变化导致支持的 PagingSource 失效,则单个查询可能会发出多个 PagingData 实例。因此,PagingDataFlows 应该独立于其他 Flows 公开。

就是这样!现在我们在 ViewModel 中拥有了分页功能!

8. 使 Adapter 支持 PagingData

要将 PagingData 绑定到 RecyclerView,请使用 PagingDataAdapter。当 PagingData 内容加载时,PagingDataAdapter 会收到通知,然后通知 RecyclerView 进行更新。

更新 ArticleAdapter 以支持 PagingData

  • 现在,ArticleAdapter 实现了 ListAdapter。改为让它实现 PagingDataAdapter。类体的其余部分保持不变
import androidx.paging.PagingDataAdapter
...

class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}

到目前为止,我们已经做了很多更改,但现在距离能够运行应用仅一步之遥——我们只需要连接 UI!

9. 在 UI 中消费 PagingData

在当前的实现中,我们有一个名为 binding.setupScrollListener() 的方法,该方法在满足某些条件时调用 ViewModel 以加载更多数据。Paging 库会自动完成所有这些操作,因此我们可以删除此方法及其用法。

接下来,由于 ArticleAdapter 不再是 ListAdapter,而是 PagingDataAdapter,我们需要进行两个小更改

  • 我们将来自 ViewModelFlow 上的终端运算符从 collect 切换到 collectLatest
  • 我们使用 submitData() 而不是 submitList() 通知 ArticleAdapter 更改。

我们在 pagingData Flow 上使用 collectLatest,以便在新 pagingData 实例发出时取消对先前 pagingData 发出的收集。

完成这些更改后,Activity 应该如下所示

import kotlinx.coroutines.flow.collectLatest


class ArticleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityArticlesBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val viewModel by viewModels<ArticleViewModel>(
            factoryProducer = { Injection.provideViewModelFactory(owner = this) }
        )

        val items = viewModel.items
        val articleAdapter = ArticleAdapter()

        binding.bindAdapter(articleAdapter = articleAdapter)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                items.collectLatest {
                    articleAdapter.submitData(it)
                }
            }
        }
    }
}

private fun ActivityArticlesBinding.bindAdapter(
    articleAdapter: ArticleAdapter
) {
    list.adapter = articleAdapter
    list.layoutManager = LinearLayoutManager(list.context)
    val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
    list.addItemDecoration(decoration)
}

应用现在应该可以编译并运行了。您已成功将应用迁移到 Paging 库!

f97136863cfa19a0.gif

10. 在 UI 中显示加载状态

当 Paging 库正在提取更多项目以在 UI 中显示时,最佳实践是向用户表明更多数据正在加载中。幸运的是,Paging 库提供了一种方便的方式来通过 CombinedLoadStates 类型访问其加载状态。

CombinedLoadStates 实例描述了 Paging 库中所有加载数据组件的加载状态。在我们的例子中,我们只对 ArticlePagingSourceLoadState 感兴趣,因此我们将主要处理 CombinedLoadStates.source 字段中的 LoadStates 类型。您可以通过 PagingDataAdapter.loadStateFlow 通过 PagingDataAdapter 访问 CombinedLoadStates

CombinedLoadStates.source 是一个 LoadStates 类型,包含三种不同类型的 LoadState 字段

  • LoadStates.append: 表示用户当前位置之后正在提取项目的 LoadState
  • LoadStates.prepend: 表示用户当前位置之前正在提取项目的 LoadState
  • LoadStates.refresh: 表示初始加载的 LoadState

Each LoadState 本身可以是以下之一

  • LoadState.Loading: 项目正在加载中。
  • LoadState.NotLoading: 项目未加载。
  • LoadState.Error: 加载发生错误。

在我们的例子中,我们只关心 LoadState 是否为 LoadState.Loading,因为我们的 ArticlePagingSource 不包含错误情况。

我们要做的第一件事是在 UI 的顶部和底部添加进度条,以指示任一方向的提取加载状态。

activity_articles.xml 中,添加两个 LinearProgressIndicator 进度条,如下所示

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ArticleActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/prepend_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/append_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

接下来,我们通过从 PagingDataAdapter 收集 LoadStatesFlow 来响应 CombinedLoadState。在 ArticleActivity.kt 中收集状态

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                articleAdapter.loadStateFlow.collect {
                    binding.prependProgress.isVisible = it.source.prepend is Loading
                    binding.appendProgress.isVisible = it.source.append is Loading
                }
            }
        }
        lifecycleScope.launch {
        ...
    }

最后,我们在 ArticlePagingSource 中添加一些延迟来模拟加载

private const val LOAD_DELAY_MILLIS = 3_000L

class ArticlePagingSource : PagingSource<Int, Article>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        val start = params.key ?: STARTING_KEY
        val range = startKey.until(startKey + params.loadSize)

        if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
        return ...

}

再次运行应用并滚动到列表底部。您应该会看到底部进度条在 Paging 库提取更多项目时显示,完成后消失!

6277154193f7580.gif

11. 总结

让我们快速回顾一下我们涵盖的内容。我们...

  • ...探讨了分页概述以及为什么它很重要。
  • ...通过创建 Pager、定义 PagingSource 并发出 PagingData,为我们的应用添加了分页功能。
  • ...使用 cachedIn 运算符在 ViewModel 中缓存了 PagingData
  • ...使用 PagingDataAdapter 在 UI 中消费了 PagingData
  • ...使用 PagingDataAdapter.loadStateFlow 响应了 CombinedLoadStates

就是这样!要查看更高级的分页概念,请查看高级 Paging Codelab