Paging 3.0 簡介 | MAD Skills

歡迎閱讀 MAD Skills 系列 之 Paging 3.0!在本文中,我將介紹 Paging 3.0 並重點說明如何將其集成至您應用的數據層。如果您更喜歡通過視頻瞭解此內容,請 點擊此處 查看。

爲什麼使用 Paging 3.0?

向用戶展示一列數據是最常見的 UI 模式之一。當您需要加載大量數據時,可以通過分塊異步獲取/顯示數據來提升應用性能。這一模式是如此常見,如果有依賴庫可以提供促進實現該模式的抽象,將會爲開發者帶來巨大的便利。這便是 Paging 3.0 致力解決的用例。作爲額外的好處,它還讓您的應用可以支持無限的數據集合;而如果您的應用通過網絡加載數據,它也爲支持本地緩存提供了方便。

如果您正在使用 Paging 2.0,那麼 Paging 3.0 也爲其前任所包含的功能提供了一系列改進:

  • 優先支持 Kotlin 協程和 Flow。
  • 支持通過 RxJava Single 或 Guava ListenableFuture 原語進行異步加載。
  • 爲響應式 UI 設計提供了內建的加載狀態和錯誤信號,包括重試和刷新功能。
  • 改進倉庫層,包含對於可取消的支持及簡化數據源接口。
  • 改進表現層、列表分隔符、自定義頁面轉換以及加載狀態頭、腳標。

如需獲取更多內容信息,請查閱 Paging 2.0 到 Paging 3.0 的 遷移文檔

置入數據

在您應用的架構方案中,Paging 3.0 最適合作爲從數據層獲取數據並通過 ViewModel 在 UI 層傳輸數據來對其進行轉換和呈現的一種方式。在 Paging 3.0 中,我們通過名爲 PagingSource 的類型訪問您的數據層,該類型定義瞭如何圍繞 PagingConfig 所定義的範圍獲取和刷新數據。

PagingSourceMap 類似,都需要定義兩個泛型類型: 分頁的 Key 的類型和加載的數據的類型。舉例來說,從基於 Github API 的頁面獲取 Repo 項目的 PagingSource 的聲明,可以定義爲:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

class GithubPagingSource(
    …
) : PagingSource<Int, Repo>()

△ PagingSource 聲明

功能完整的 PagingSource 需要實現兩個抽象方法:

  1. load()
  2. getRefreshKey()

load 方法

load() 方法正如其名,是由 Paging 庫所調用的,用於異步加載要顯示的數據的方法。這一方法會在初始加載或者響應用戶滑動至邊界時調用。load 方法會傳入一個 LoadParams 對象,您可以通過它來確定如何觸發 load 方法的調用。此對象中包含了有關 load 操作的信息,包括:

  • 將要加載的頁面的 Key: 如果這是 load 方法第一次被調用 (初始加載),LoadParams.key 將會是 null。在這種情況下,您必須定義初始頁面 Key。
  • 加載大小: 請求所要加載的項目的數量。

load 方法的返回類型是 LoadResult。它可以是:

  • LoadResult.Page: 針對加載成功。
  • LoadResult.Error: 針對加載失敗。
/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */   

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
        val apiQuery = query + IN_QUALIFIER
        return try {
            val response = service.searchRepos(apiQuery, position, params.loadSize)
            val repos = response.items
            val nextKey = if (repos.isEmpty()) {
                null
            } else {
                // 初始加載大小爲 3 * NETWORK_PAGE_SIZE
                // 要保證我們在第二次加載時不會去請求重複的項目。
                position + (params.loadSize / NETWORK_PAGE_SIZE)
            }
            LoadResult.Page(
                data = repos,
                prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
                nextKey = nextKey
            )
        } catch (exception: IOException) {
            LoadResult.Error(exception)
        } catch (exception: HttpException) {
            LoadResult.Error(exception)
        }
    }

△ load 方法實現

注意,默認情況下,初始加載大小爲分頁大小的三倍。這樣可以保證在列表第一次加載時,即使用戶稍作滾動,也能看到足夠的數據,從而避免觸發太多網絡請求。這也是在 PagingSource 實現中計算下一個 Key 時所需要考慮的事情。

getRefreshKey 方法

刷新 Key 用於 PagingSource.load() 方法後續的刷新調用 (第一次調用是初始加載,使用爲 Pager 提供的初始 Key)。每當 Paging 庫想要加載新的數據來替代當前列表 (例如,下拉刷新或數據庫更新、配置變更、進程終止等情況的發生而導致數據失效) 時,便會發生刷新操作。通常,後續刷新調用會想要重新加載以 PagingState.anchorPosition 爲中心的數據,而 PagingState.anchorPosition 則代表了最近所訪問的索引位置。

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

   // 刷新 Key 用於在初始加載的數據失效後下一個 PagingSource 的加載。
    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
        // 我們需要獲取與最新訪問索引最接近頁面的前一個 Key(如果上一個 Key 爲空,則爲下一個 Key)
        // anchorPosition 即爲最近訪問的索引
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

△ getRefreshKey 方法實現

Pager 對象

在定義了 PagingSource 後,我們現在可以創建 Pager 了。Pager 類負責根據 UI 的請求從 PagingSource 中增量拉取數據集合。由於 Pager 需要訪問 PagingSource,所以它通常創建在定義 PagingSource 的數據層中。

構造 Pager 所需的另一個類是 PagingConfig,它定義了控制 Pager 獲取數據方式的參數。除了必選的 pageSize 參數外,PagingConfig 還暴露了許多可選參數,您可以通過它們微調 Pager 的行爲:

/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

private const val NETWORK_PAGE_SIZE = 30

class GithubRepository(private val service: GithubService) {

    fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
        Log.d("GithubRepository", "New query: $query")
        return Pager(
            config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE,
                enablePlaceholders = false
            ),
            pagingSourceFactory = { GithubPagingSource(service, query) }
        ).flow
    }
}

△ 創建 Pager

上面構造 PagingConfig 的代碼中所使用參數的簡要說明如下:

  • pageSize: 每次要從 PagingSource 加載項目的數量。
  • enablePlaceholders: 是否需要 PagingData 爲尚未加載的數據返回 null。

通常我們會希望 pageSize 足夠的大 (至少足夠填充界面的可視區域,但最好是這一數量的 2 到 3 倍),這樣 Pager 就不必爲了在屏幕上顯示足夠的內容,而在用戶進行滾動操作時一遍又一遍地獲取數據了。

獲取您的數據

Pager 所產生的類型是 PagingData,該類型提供了進入其背後 PagingSource 的不同窗口。當用戶滾動列表時,PagingData 會持續從 PagingSource 中獲取數據以提供內容。如果 PagingSource 失效,Pager 會發出一個新的 PagingData 以確保已經分頁的項目與 UI 中顯示的內容同步。將 PagingData 視爲某個時間節點中 PagingSource 的快照可能會對您的理解有所幫助。

由於 PagingSource 是在 PagingSource 失效時發生改變的快照,因此 Paging 庫提供了多種以流的形式使用 PagingData 的方式:

  • Kotlin Flow 通過 Pager.flow
  • LiveData 通過 Pager.liveData
  • RxJava Flowable 通過 Pager.flowable
  • RxJava Observable 通過 Pager.observable

PagingData 的流可以在展示分頁項目到 UI 前通過 ViewModel 進行操作和轉換。

後續

按照如上步驟,我們已經將 Paging 3.0 集成到了您應用的數據層中!如何在 UI 中消費 PagingData 以及填充我們的倉庫列表,敬請關注我們後續的文章。

歡迎您 點擊這裏 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章