欢迎回到 MAD Skills 系列 课程之 Paging 3.0!在上一篇 Paging 3.0 简介 的文章中,我们讨论了 Paging 库,了解了如何将它融入到应用架构中,并将其整合进了应用的数据层。我们使用了 PagingSource
来为我们的应用获取并使用数据,以及用 PagingConfig
来创建能够提供 Flow<PagingData>
给 UI 消费的 Pager
对象。在本文中我将介绍如何在您的 UI 中实际使用 Flow<PagingData>
。
为 UI 准备 PagingData
应用现有的 ViewModel
暴露了能够提供渲染 UI 所需信息的 UiState
数据类,它包含一个 searchResult
字段,用于将搜索结果缓存在内存中,可在配置变更后提供数据。
data class UiState(
val query: String,
val searchResult: RepoSearchResult
)
sealed class RepoSearchResult {
data class Success(val data: List<Repo>) : RepoSearchResult()
data class Error(val error: Exception) : RepoSearchResult()
}
△ 初始 UiState 定义
现在接入 Paging 3.0,我们移除了 UiState
中的 searchResult
,并选择在 UiState
之外单独暴露出一个 PagingData<Repo>
的 Flow
来代替它。这个新的 Flow
功能与 searchResult
相同: 提供一个让 UI 渲染的项目列表。
ViewModel
中添加了一个私有的 "searchRepo()"
方法,它调用 Repository
来提供 Pager
中的 PagingData Flow
。我们可以调用该方法来创建基于用户输入搜索词的 Flow<PagingData<Repo>>
。我们还在生成的 PagingData Flow
上使用了 cachedIn
操作符,使其能够通过 ViewModelScope
快速复用。
class SearchRepositoriesViewModel(
private val repository: GithubRepository,
…
) : ViewModel() {
…
private fun searchRepo(queryString: String): Flow<PagingData<Repo>> =
repository.getSearchResultStream(queryString)
}
△ 为仓库集成 PagingData Flow
暴露一个独立于其它 Flow 的 PagingData Flow 这一点非常重要 。因为 PagingData
自身是一个可变类型,它内部维护了自己的数据流并且会随着时间的变化而更新。
随着组成 UiState
字段的 Flow
全部被定义,我们可以将其组合成 UiState
的 StateFlow
,并和 PagingData
的 Flow
一起暴露出来给 UI 消费。完成这些之后,现在我们可以开始在 UI 中消费我们的 Flow
了。
class SearchRepositoriesViewModel(
…
) : ViewModel() {
val state: StateFlow<UiState>
val pagingDataFlow: Flow<PagingData<Repo>>
init {
…
pagingDataFlow = searches
.flatMapLatest { searchRepo(queryString = it.query) }
.cachedIn(viewModelScope)
state = combine(...)
}
}
△ 暴露 PagingData Flow 给 UI 注意 cachedIn 运算符的使用
在 UI 中消费 PagingData
首先我们要做的就是将 RecyclerView Adapter
从 ListAdapter 切换到 PagingDataAdapter
。PagingDataAdapter
是为比较 PagingData
的差异并聚合更新而优化的 RecyclerView Adapter
,用以确保后台数据集的变化能够尽可能高效地传递。
// 之前
// class ReposAdapter : ListAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
// …
// }
// 之后
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
…
}
view raw
△ 从 ListAdapter 切换到 PagingDataAdapter
接下来,我们开始从 PagingData Flow
中收集数据,我们可以这样使用 submitData
挂起函数将它的发射绑定到 PagingDataAdapter
。
private fun ActivitySearchRepositoriesBinding.bindList(
…
pagingData: Flow<PagingData<Repo>>,
) {
…
lifecycleScope.launch {
pagingData.collectLatest(repoAdapter::submitData)
}
}
△ 使用 PagingDataAdapter 消费 PagingData 注意 colletLatest 的使用
此外,为了用户体验着想,我们希望确保当用户搜索新内容时,将回到 列表的顶部 以展示第一条搜索结果。我们期望在 我们加载完成并已将数据展示到 UI 时做到这一点。我们通过利用 PagingDataAdapter
暴露的 loadStateFlow
和 UiState
中的 "hasNotScrolledForCurrentSearch"
字段来跟踪用户是否手动滚动列表。结合这两者可以创建一个标记让我们知道是否应该触发自动滚动。
由于 loadStateFlow
提供的加载状态与 UI 显示的内容同步,我们可以有把握地在每次 loadStateFlow
通知我们新的查询处于 NotLoading
状态时滚动到列表顶部。
private fun ActivitySearchRepositoriesBinding.bindList(
repoAdapter: ReposAdapter,
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
…
) {
…
val notLoading = repoAdapter.loadStateFlow
// 仅当 PagingSource 的 refresh (LoadState 类型) 发生改变时发射
.distinctUntilChangedBy { it.source.refresh }
// 仅响应 refresh 完成,也就是 NotLoading。
.map { it.source.refresh is LoadState.NotLoading }
val hasNotScrolledForCurrentSearch = uiState
.map { it.hasNotScrolledForCurrentSearch }
.distinctUntilChanged()
val shouldScrollToTop = combine(
notLoading,
hasNotScrolledForCurrentSearch,
Boolean::and
)
.distinctUntilChanged()
lifecycleScope.launch {
shouldScrollToTop.collect { shouldScroll ->
if (shouldScroll) list.scrollToPosition(0)
}
}
}
△ 实现有新查询时自动滚动到顶部
添加头部和尾部
Paging 库的另一个优点是在 LoadStateAdapter
的帮助下,能够在页面的顶部或底部显示进度指示器。RecyclerView.Adapter
的这一实现能够在 Pager
加载数据时自动对其进行通知,使其可以根据需要在列表顶部或底部插入项目。
而它的精髓是您甚至不需要改变现有的 PagingDataAdapter
。withLoadStateHeaderAndFooter
扩展函数可以很方便地使用头部和尾部包裹您已有的 PagingDataAdapter
。
private fun ActivitySearchRepositoriesBinding.bindState(
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
uiActions: (UiAction) -> Unit
) {
val repoAdapter = ReposAdapter()
list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
header = ReposLoadStateAdapter { repoAdapter.retry() },
footer = ReposLoadStateAdapter { repoAdapter.retry() }
)
}
△ 头部和尾部
withLoadStateHeaderAndFooter
函数的参数中为头部和尾部都定义了 LoadStateAdapter
。这些 LoadStateAdapter
相应地托管了自身的 ViewHolder
,这些 ViewHolder
与最新的加载状态绑定,因此很容易定义视图行为。我们还可以传入参数实现当出现错误时重试加载,我将会在下一篇文章中详细介绍。
后续
我们已经将 PagingData 绑定到了 UI 上!来快速回顾一下:
- 使用 PagingDataAdapter 将我们的 Paging 集成到 UI 上
- 使用 PagingDataAdapter 暴露的 LoadStateFlow 来保证仅当 Pager 结束加载时滚动到列表的顶部
- 使用 withLoadStateHeaderAndFooter() 实现当获取数据时将加载栏添加到 UI 上
感谢您的阅读!敬请关注下一篇文章,我们将探讨用 Paging 实现以数据库作为单一来源,并详细讨论 LoadStateFlow
!
欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!