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

获取数据并绑定到 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 全部被定义,我们可以将其组合成 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 Adapter 从 ListAdapter 切换到 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

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

编辑于 2021-11-01 12:24