查看原文
其他

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

Android Android 开发者 2021-11-05
欢迎回到 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
暴露一个独立于其它 FlowPagingData Flow 这一点非常重要。因为 PagingData 自身是一个可变类型,它内部维护了自己的数据流并且会随着时间的变化而更新。

随着组成 UiState 字段的 Flow 全部被定义,我们可以将其组合成 UiStateStateFlow,并和 PagingDataFlow 一起暴露出来给 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 AdapterListAdapter 切换到 PagingDataAdapterPagingDataAdapter 是为比较 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 暴露的 loadStateFlowUiState 中的 "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 库概览




: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存