获取数据并绑定到 UI | MAD Skills
Posted 谷歌开发者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了获取数据并绑定到 UI | MAD Skills相关的知识,希望对你有一定的参考价值。
欢迎回到 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!
欢迎您通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!
推荐阅读
如页面未加载,请刷新重试
点击屏末 | 阅读原文 | 即刻查看 Paging 库概览
以上是关于获取数据并绑定到 UI | MAD Skills的主要内容,如果未能解决你的问题,请参考以下文章