Android 分页基础

1. 简介

学习内容

  • 分页库的主要组件是什么。
  • 如何将分页库添加到您的项目中。

您将构建什么

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

7d256d9c74e3b3f5.png

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

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

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

  • ...成功实现了分页。
  • ...在获取更多数据时有效地与用户通信。

以下是我们将最终获得的 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. 分页简介

向用户显示信息最常见的方法之一是使用列表。但是,有时这些列表仅提供用户可访问的所有内容的一小部分。当用户滚动浏览可用的信息时,通常会期望获取更多数据来补充已查看的信息。每次获取数据时,都需要高效且无缝,以便增量加载不会影响用户体验。增量加载还提供性能优势,因为应用不需要一次在内存中保存大量数据。

这种增量获取信息的过程称为分页,其中每个页面对应于要获取的一块数据。要请求页面,正在分页的数据源通常需要一个查询来定义所需的信息。本 Codelab 的其余部分将介绍分页库,并演示它如何帮助您快速有效地在应用中实现分页。

分页库的核心组件

分页库的核心组件如下

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

566d0f6506f39480.jpeg

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

4. 项目概述

当前形式的应用显示文章的静态列表。每篇文章都有标题、描述和创建日期。静态列表对于少量项目来说效果很好,但随着数据集变大,它无法很好地扩展。我们将通过使用分页库实现分页来解决此问题,但首先让我们回顾一下应用中已有的组件。

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

数据层

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

UI 层:

  • ActivityRecyclerView.AdapterRecyclerView.ViewHolder:负责在 UI 中显示列表的类。
  • ViewModel:状态持有者,负责创建 UI 需要显示的状态。

存储库在articleStream字段中以Flow的形式公开其所有文章。这反过来又由 UI 层中的ArticleViewModel读取,然后在ArticleActivity中使用state字段(一个StateFlow)将其准备供 UI 使用。

从存储库公开文章作为Flow使存储库能够随着时间的推移更新呈现的文章。例如,如果文章的标题发生更改,则该更改可以轻松地传达给articleStream的收集器。在ViewModel中使用StateFlow作为 UI 状态可确保即使我们停止收集 UI 状态(例如,当Activity在配置更改期间重新创建时),我们也可以在我们停止的地方继续收集它。

如前所述,存储库中的当前articleStream仅显示当天的新闻。虽然这可能对某些用户来说足够了,但其他用户可能希望在滚动浏览当天所有可用文章后查看较旧的文章。此期望使文章的显示成为分页的理想选择。我们应该探索文章分页的其他原因包括以下内容

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

分页库有助于解决所有这些问题,同时为在您的应用中增量获取数据(分页)提供一致的 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
  • **数据从哪里获取** - 通常,这将是数据库、网络资源或任何其他分页数据源。但是,在本代码实验室中,我们使用本地生成的数据。

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()

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

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

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

  • LoadResult.Page,如果结果成功。
  • LoadResult.Error,如果发生错误。
  • LoadResult.Invalid,如果 PagingSource 应该失效,因为它无法再保证其结果的完整性。

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 中更新的情况称为失效。失效时,Paging 库会创建一个新的 PagingSource 来重新加载数据,并通过发出新的 PagingData 来通知 UI。我们将在后面的部分详细了解失效。

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

分页库中的失效有两种原因

  • 您在 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。如果您想知道分页库如何知道获取更多项目,这是一个线索!当 UI 尝试从 PagingData 读取项目时,它会尝试在特定索引处读取。如果读取了数据,则该数据将显示在 UI 中。但是,如果没有数据,则分页库知道它需要获取数据以满足失败的读取请求。读取时成功获取数据的最后一个索引是 anchorPosition

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

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

6. 为 UI 生成 PagingData

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

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

要构建 PagingData,我们将根据我们要使用哪个 API 将 PagingData 传递到应用程序的其他层,使用 Pager 类的几种不同的构建器方法之一

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

由于我们已经在应用程序中使用了 Flow,因此我们将继续使用这种方法;但我们将使用 Flow<PagingData<Article>> 而不是 Flow<List<Article>>

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

  • PagingConfig。此类设置有关如何从 PagingSource 加载内容的选项,例如提前加载多远、初始加载的请求大小等。您必须定义的唯一必需参数是页面大小——每个页面应加载多少个项目。默认情况下,Paging 会将您加载的所有页面都保存在内存中。为了确保在用户滚动时不会浪费内存,请在 PagingConfig 中设置 maxSize 参数。默认情况下,如果 Paging 可以计算未加载的项目并且 enablePlaceholders 配置标志为 true,则 Paging 将返回空项目作为未加载内容的占位符。这样,您就可以在适配器中显示占位符视图。为了简化本代码实验室中的工作,让我们通过传递 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. 使适配器与 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,因此我们进行两个小的更改

  • 我们将 Flow 上的终端运算符从 ViewModel 切换到 collectLatest 而不是 collect
  • 我们使用 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

每个 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