1. 简介
学习内容
- 分页库的主要组件是什么。
- 如何将分页库添加到您的项目中。
您将构建什么
在本 Codelab 中,您将从一个示例应用开始,该应用已显示文章列表。该列表是静态的,它包含 500 篇文章,并且所有文章都保存在手机内存中
随着您逐步完成本 Codelab,您将
- ...了解分页的概念。
- ...了解分页库的核心组件。
- ...了解如何使用分页库实现分页。
完成后,您将拥有一个应用
- ...成功实现了分页。
- ...在获取更多数据时有效地与用户通信。
以下是我们将最终获得的 UI 的快速预览
您需要什么
好的
- 熟悉以下架构组件:ViewModel、View Binding以及应用架构指南中建议的架构。有关架构组件的介绍,请查看Room with a View Codelab。
- 熟悉协程和 Kotlin Flow。有关 Flow 的介绍,请查看使用 Kotlin Flow 和 LiveData 的高级协程 Codelab。
2. 设置您的环境
在此步骤中,您将下载整个 Codelab 的代码,然后运行一个简单的示例应用。
为了让您尽快入门,我们为您准备了一个启动项目供您构建。
如果您已安装 git,则只需运行以下命令。要检查是否已安装 git,请在终端或命令行中键入git --version
并验证其是否正确执行。
git clone https://github.com/googlecodelabs/android-paging
如果您没有 git,可以点击以下按钮下载此 Codelab 的所有代码:
代码组织在两个文件夹中,basic
和advanced
。对于此 Codelab,我们只关注basic
文件夹。
在basic
文件夹中,还有另外两个文件夹:start
和end
。我们将从start
文件夹中的代码开始工作,在本 Codelab 结束时,start
文件夹中的代码应与end
文件夹中的代码相同。
- 在 Android Studio 中打开
basic/start
目录中的项目。 - 在设备或模拟器上运行
app
运行配置。
我们应该看到一个文章列表!滚动到末尾以验证列表是否为静态的——换句话说,当我们到达列表末尾时不会获取更多项目。滚动回顶部以验证我们是否仍然拥有所有项目。
3. 分页简介
向用户显示信息最常见的方法之一是使用列表。但是,有时这些列表仅提供用户可访问的所有内容的一小部分。当用户滚动浏览可用的信息时,通常会期望获取更多数据来补充已查看的信息。每次获取数据时,都需要高效且无缝,以便增量加载不会影响用户体验。增量加载还提供性能优势,因为应用不需要一次在内存中保存大量数据。
这种增量获取信息的过程称为分页,其中每个页面对应于要获取的一块数据。要请求页面,正在分页的数据源通常需要一个查询来定义所需的信息。本 Codelab 的其余部分将介绍分页库,并演示它如何帮助您快速有效地在应用中实现分页。
分页库的核心组件
分页库的核心组件如下
PagingSource
- 加载特定页面查询的数据块的基本类。它是数据层的一部分,通常从DataSource
类公开,随后由Repository
在ViewModel
中使用。PagingConfig
- 定义确定分页行为的参数的类。这包括页面大小、是否启用占位符等。Pager
- 负责生成PagingData
流的类。它依赖于PagingSource
来执行此操作,应在ViewModel
中创建。PagingData
- 分页数据的容器。数据的每次刷新都将有一个单独的相应PagingData
发射,由其自己的PagingSource
支持。PagingDataAdapter
- 一个RecyclerView.Adapter
子类,用于在RecyclerView
中呈现PagingData
。可以使用工厂方法将PagingDataAdapter
连接到 KotlinFlow
、LiveData
、RxJavaFlowable
、RxJavaObservable
甚至静态列表。PagingDataAdapter
侦听内部PagingData
加载事件,并在加载页面时有效地更新 UI。
在以下部分,您将实现上述每个组件的示例。
4. 项目概述
当前形式的应用显示文章的静态列表。每篇文章都有标题、描述和创建日期。静态列表对于少量项目来说效果很好,但随着数据集变大,它无法很好地扩展。我们将通过使用分页库实现分页来解决此问题,但首先让我们回顾一下应用中已有的组件。
该应用遵循应用架构指南中推荐的架构。以下是您在每个包中将找到的内容
数据层
ArticleRepository
:负责提供文章列表并在内存中保存它们。Article
:表示数据模型的类,它是从数据层提取的信息的表示。
UI 层:
Activity
、RecyclerView.Adapter
和RecyclerView.ViewHolder
:负责在 UI 中显示列表的类。ViewModel
:状态持有者,负责创建 UI 需要显示的状态。
存储库在articleStream
字段中以Flow
的形式公开其所有文章。这反过来又由 UI 层中的ArticleViewModel
读取,然后在ArticleActivity
中使用state
字段(一个StateFlow
)将其准备供 UI 使用。
从存储库公开文章作为Flow
使存储库能够随着时间的推移更新呈现的文章。例如,如果文章的标题发生更改,则该更改可以轻松地传达给articleStream
的收集器。在ViewModel
中使用StateFlow
作为 UI 状态可确保即使我们停止收集 UI 状态(例如,当Activity
在配置更改期间重新创建时),我们也可以在我们停止的地方继续收集它。
如前所述,存储库中的当前articleStream
仅显示当天的新闻。虽然这可能对某些用户来说足够了,但其他用户可能希望在滚动浏览当天所有可用文章后查看较旧的文章。此期望使文章的显示成为分页的理想选择。我们应该探索文章分页的其他原因包括以下内容
ViewModel
在items
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()
函数返回一个 LoadResult
。 LoadResult
可以是以下类型之一
LoadResult.Page
,如果结果成功。LoadResult.Error
,如果发生错误。LoadResult.Invalid
,如果PagingSource
应该失效,因为它无法再保证其结果的完整性。
LoadResult.Page
有三个必需的参数
data
:获取的项目的List
。prevKey
:如果需要获取当前页面之前的项目,则load()
方法使用的键。nextKey
:如果需要获取当前页面之后的项目,则load()
方法使用的键。
...以及两个可选参数
itemsBefore
:在加载的数据之前显示的占位符数量。itemsAfter
:在加载的数据之后显示的占位符数量。
我们的加载键是 Article.id
字段。我们可以将其用作键,因为 Article
ID 对于每篇文章都会增加 1;也就是说,文章 ID 是连续的单调递增整数。
nextKey
或 prevKey
为 null
,如果在相应方向上没有更多数据要加载。在我们的例子中,对于 prevKey
- 如果
startKey
与STARTING_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
参数传递给新 PagingSource
中 load()
方法的下次调用。为了防止项目在失效后跳动,我们需要确保返回的键将加载足够多的项目以填充屏幕。这增加了新项目集包含失效数据中存在的项目的可能性,这有助于保持当前滚动位置。让我们看看我们应用中的实现
// 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
。
刷新时,我们获取最接近 anchorPosition
的 Article
的键用作加载键。这样,当我们从新的 PagingSource
重新开始加载时,获取的项目集包含已加载的项目,从而确保流畅一致的用户体验。
完成这些操作后,您就完全定义了一个 PagingSource
。下一步是将其连接到 UI。
6. 为 UI 生成 PagingData
在我们的当前实现中,我们在 ArticleRepository
中使用 Flow<List<Article>>
将加载的数据公开给 ViewModel
。 ViewModel
反过来使用 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
,其pageSize
为ITEMS_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
实例。因此,PagingData
的 Flows
应独立于其他 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 库!
10. 在 UI 中显示加载状态
当 Paging 库正在获取更多要显示在 UI 中的项目时,最佳实践是向用户指示更多数据正在加载。幸运的是,Paging 库提供了一种方便的方法来使用 CombinedLoadStates
类型访问其加载状态。
CombinedLoadStates
实例描述了加载数据的 Paging 库中所有组件的加载状态。在我们的例子中,我们只对 ArticlePagingSource
的 LoadState
感兴趣,因此我们将主要使用 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 库获取更多项目时显示,并在完成后消失!
11. 总结
让我们快速回顾一下我们所涵盖的内容。我们...
- ...探讨了分页的概述以及为什么它是有必要的。
- ...通过创建
Pager
、定义PagingSource
并发出PagingData
来向我们的应用程序添加分页。 - ...使用
cachedIn
运算符在ViewModel
中缓存PagingData
。 - ...使用
PagingDataAdapter
在 UI 中使用PagingData
。 - ...使用
PagingDataAdapter.loadStateFlow
响应CombinedLoadStates
。
就是这样!要查看更多高级分页概念,请查看高级 Paging codelab!