1. 简介
您将学到的内容
- Paging 库的主要组成部分是什么。
- 如何将 Paging 库添加到您的项目。
您将构建什么
在此 Codelab 中,您将从一个已显示文章列表的示例应用开始。该列表是静态的,包含 500 篇文章,所有这些文章都保存在手机内存中
随着您逐步完成此 Codelab,您将
- ...了解分页的概念。
- ...了解 Paging 库的核心组件。
- ...了解如何使用 Paging 库实现分页。
完成后,您将拥有一个应用
- ...成功实现了分页。
- ...在正在提取更多数据时有效地向用户进行沟通。
以下是我们最终完成的 UI 的快速预览
您将需要
建议掌握
- 熟悉以下架构组件:ViewModel、View Binding 以及 应用架构指南中建议的架构。有关架构组件的介绍,请参阅 Room with a View Codelab。
- 熟悉协程 (coroutines) 和 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. 分页简介
向用户显示信息最常见的方式之一是使用列表。然而,有时这些列表只提供了用户可用的全部内容的一小部分窗口。当用户滚动浏览可用信息时,通常期望会提取更多数据来补充已经看到的信息。每次提取数据都需要高效且无缝,这样增量加载才不会损害用户体验。增量加载还提供了性能优势,因为应用无需一次性在内存中保留大量数据。
这种逐步提取信息的过程称为 分页 (pagination),其中每个 页 (page) 对应于要提取的数据块。要请求一个页面,被分页的数据源通常需要一个定义所需信息的 查询 (query)。此 Codelab 的其余部分将介绍 Paging 库,并演示它如何帮助您快速有效地在应用中实现分页。
Paging 库的核心组件
Paging 库的核心组件如下
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. 项目概述
该应用当前形式显示了一个静态文章列表。每篇文章都有标题、描述和创建日期。静态列表适用于少量项目,但随着数据集变大,它的可扩展性不佳。我们将通过使用 Paging 库实现分页来解决这个问题,但首先我们先回顾一下应用中已有的组件。
该应用遵循 应用架构指南 中推荐的架构。以下是您在每个包中将找到的内容
数据层
ArticleRepository
: 负责提供文章列表并将它们保存在内存中。Article
: 表示 数据模型 (data model) 的类,是数据层中提取信息的表示形式。
UI 层:
- The
Activity
,RecyclerView.Adapter
, andRecyclerView.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()
函数返回一个 LoadResult
。 LoadResult
可以是以下类型之一
- 如果结果成功,则为
LoadResult.Page
。 - 如果发生错误,则为
LoadResult.Error
。 - 如果
PagingSource
应因无法再保证其结果的完整性而被废弃,则为LoadResult.Invalid
。
A 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 中更新的情况称为 失效 (invalidation)。失效时,Paging 库会创建一个新的 PagingSource
来重新加载数据,并通过发出新的 PagingData
通知 UI。我们将在后面的章节中了解更多关于失效的信息。
从新的 PagingSource
加载时,会调用 getRefreshKey()
以提供新的 PagingSource
应开始加载的键,确保用户刷新后不会丢失在列表中的当前位置。
Paging 库中的失效发生有两个原因
- 您调用了
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
。如果您想知道 Paging 库如何知道要提取更多项目,这就是线索!当 UI 尝试从 PagingData
读取项目时,它会尝试在某个索引处读取。如果读取到数据,则将数据显示在 UI 中。但是,如果没有数据,则 Paging 库就知道需要提取数据来满足失败的读取请求。成功读取数据的最后一个索引就是 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
,我们将使用 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
,其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. 使 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
,我们需要进行两个小更改
- 我们将来自
ViewModel
的Flow
上的终端运算符从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 库!
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
。
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 库提取更多项目时显示,完成后消失!
11. 总结
让我们快速回顾一下我们涵盖的内容。我们...
- ...探讨了分页概述以及为什么它很重要。
- ...通过创建
Pager
、定义PagingSource
并发出PagingData
,为我们的应用添加了分页功能。 - ...使用
cachedIn
运算符在ViewModel
中缓存了PagingData
。 - ...使用
PagingDataAdapter
在 UI 中消费了PagingData
。 - ...使用
PagingDataAdapter.loadStateFlow
响应了CombinedLoadStates
。
就是这样!要查看更高级的分页概念,请查看高级 Paging Codelab!