歡迎回到 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
!
歡迎您 點擊這裏 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支持!