Paging 3.0 簡介 | MAD Skills

Android開發者發表於2021-11-05

歡迎閱讀 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 以及填充我們的倉庫列表,敬請關注我們後續的文章。

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

相關文章