获取数据并绑定到 UI | MAD Skills

2021 年 11 月 1 日 谷歌开发者
欢迎回到  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 加载数据时自动对其进行通知,使其可以根据需要在列表顶部或底部插入项目。


而它的精髓是您甚至不需要改变现有的 PagingDataAdapterwithLoadStateHeaderAndFooter 扩展函数可以很方便地使用头部和尾部包裹您已有的 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 库概览



登录查看更多
0

相关内容

专知会员服务
58+阅读 · 2021年1月17日
专知会员服务
39+阅读 · 2020年9月6日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
116+阅读 · 2020年5月10日
【资源】100+本免费数据科学书
专知会员服务
107+阅读 · 2020年3月17日
【干货】用BRET进行多标签文本分类(附代码)
专知会员服务
84+阅读 · 2019年12月27日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
Gradle 与 AGP 构建 API: 进一步完善您的插件!
谷歌开发者
0+阅读 · 2022年1月5日
Gradle 与 AGP 构建 API: 配置您的构建文件
谷歌开发者
0+阅读 · 2021年12月21日
是时候聊一聊ProxySQL功能测试了
InfoQ
2+阅读 · 2021年11月17日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
Paging 3.0 简介 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月29日
在 Android 12 中使用 WorkManager
谷歌开发者
0+阅读 · 2021年10月22日
Hilt 工作原理 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月18日
导航: 多返回栈 | MAD Skills
谷歌开发者
0+阅读 · 2021年9月24日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
4+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
3+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
3+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Arxiv
16+阅读 · 2021年1月27日
Arxiv
101+阅读 · 2020年3月4日
VIP会员
相关VIP内容
专知会员服务
58+阅读 · 2021年1月17日
专知会员服务
39+阅读 · 2020年9月6日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
116+阅读 · 2020年5月10日
【资源】100+本免费数据科学书
专知会员服务
107+阅读 · 2020年3月17日
【干货】用BRET进行多标签文本分类(附代码)
专知会员服务
84+阅读 · 2019年12月27日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
相关资讯
Gradle 与 AGP 构建 API: 进一步完善您的插件!
谷歌开发者
0+阅读 · 2022年1月5日
Gradle 与 AGP 构建 API: 配置您的构建文件
谷歌开发者
0+阅读 · 2021年12月21日
是时候聊一聊ProxySQL功能测试了
InfoQ
2+阅读 · 2021年11月17日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
Paging 3.0 简介 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月29日
在 Android 12 中使用 WorkManager
谷歌开发者
0+阅读 · 2021年10月22日
Hilt 工作原理 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月18日
导航: 多返回栈 | MAD Skills
谷歌开发者
0+阅读 · 2021年9月24日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
相关基金
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
4+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
3+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
3+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Top
微信扫码咨询专知VIP会员