載入並顯示分頁資料

分頁程式庫提供強大的功能,可載入並顯示大型資料集內的分頁資料。本指南說明如何使用 Paging 程式庫,設定來自網路資料來源的分頁資料串流,並顯示在Lazy 清單中。

定義資料來源

首先,您必須定義 PagingSource 實作來識別資料來源。PagingSource API 類別包含 load 方法,您必須覆寫該方法,指出如何從對應的資料來源擷取分頁資料。

直接使用 PagingSource 類別,透過 Kotlin 協同程式執行非同步載入。

選取鍵和值類型

PagingSource<Key, Value> 有兩個類型參數:Key 和 Value。鍵會定義用於載入資料的 ID,值則是資料本身的類型。舉例來說,假設您將 Int 頁碼傳遞至 Retrofit,從網路載入 User 物件的網頁,就可選取 Int 做為 Key 的類型,並將 User 設為 Value 的類型。

定義 PagingSource

以下範例會實作 PagingSource,依據頁碼載入項目頁面。Key 類型為 Int,Value 類型為 User。

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

一般 PagingSource 實作會將建構函式中的參數傳遞至 load 方法,為查詢載入適合的資料。在上述範例中,相關的參數如下:

  • backend:提供資料的後端服務例項
  • query:要傳送到 backend 指定服務的搜尋查詢

LoadParams 物件包含要執行的載入作業相關資訊。其中包括要載入的鍵和要載入的項目數量。如要區分 load 中的載入類型,請檢查傳遞的 LoadParams 子類別為何:LoadParams.Refresh、LoadParams.Append 或 LoadParams.Prepend。

LoadResult 物件包含載入作業的結果。LoadResult 是密封類別;視 load 呼叫是否成功而定,可採用下列其中一個形式:

  • 如果載入成功,就傳回 LoadResult.Page 物件。如果該方向無法再載入資料,請傳遞 nextKey 或 prevKey 的 null。
  • 如果載入失敗,則傳回 LoadResult.Error 物件。
  • 如果基礎資料變更導致 PagingSource 無效,請傳回 LoadResult.Invalid 物件。

下圖說明這個範例中的 load 函式如何接收每個載入作業的鍵,並提供後續載入作業的鍵。

在每次載入呼叫中,ExamplePagingSource 會接受目前的鍵,並傳回下一個要載入的鍵。
圖 1. 圖表說明 load 如何使用及更新鍵。

PagingSource 實作項目也必須實作 getRefreshKey 方法,後者採用 PagingState 物件做為參數。當資料重新整理或在初始載入作業完成後失效時,這個方法會傳回要傳入 load 方法的鍵。在後續重新整理資料時,Paging 程式庫會自動呼叫這個方法。如果是以鍵代表項目位置的偏移分頁,您可以計算相對於 state.anchorPosition 的重新整理鍵:

// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
  // Center the refreshed load around the user's current viewport so the list
  // does not jump back to the top on invalidation.
  return state.anchorPosition?.let { anchorPosition ->
    (anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
  }
}

處理錯誤

有很多原因會造成資料載入要求失敗,尤其是透過網路載入時。從 load 方法傳回 LoadResult.Error 物件,即可回報載入期間發生的錯誤。

舉例來說,只要將下方程式碼加入 load 方法,你就能擷取及回報前一個範例 ExamplePagingSource 中的載入錯誤:

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

如要進一步瞭解如何處理 Retrofit 錯誤,請參閱 PagingSource API 參考資料中的範例。

PagingSource 會收集 LoadResult.Error 物件並傳送至 UI,方便您執行操作。如要進一步瞭解如何在 UI 中顯示載入狀態,請參閱「管理及顯示載入狀態」一文。

設定 PagingData 串流

接下來,您需要用到來自 PagingSource 實作的串流分頁資料。請在 ViewModel 中設定資料串流。Pager 類別提供的方法,可對來自 PagingSource 的 PagingData 物件顯示回應式串流。Paging 程式庫會將資料串流公開為 Flow。

建立 Pager 執行個體以設定回應式串流時,您必須為執行個體提供 PagingConfig 設定物件和一個用於通知 Pager 如何取得 PagingSource 實作執行個體的函式。您也可以指定選用的 initialKey,例如 1 (用於以 1 為基礎的頁碼) 或 0 (用於以偏移量為基礎的分頁)。在 PagingConfig 中,您可以設定 pageSize、initialLoadSize、prefetchDistance 和 enablePlaceholders。prefetchDistance 屬性定義了載入內容的任一邊緣有多少項目會觸發下一次擷取。

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            // Load 3 pages initially so the user can scroll immediately without
            // hitting a loading state.
            initialLoadSize = 60,
            // Fetch the next page far enough ahead of the scroll position to
            // avoid visible pauses.
            prefetchDistance = 20,
            enablePlaceholders = true
        ),
        initialKey = 1,
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

cachedIn 運算子將使資料串流變得可共用,並透過提供的 CoroutineScope 快取已載入的資料。如果沒有 cachedIn,就無法重新收集 PagingData。這個範例會用到生命週期 lifecycle-viewmodel-ktx 構件提供的 viewModelScope。

Pager 物件會呼叫來自 PagingSource 物件的 load 方法,為其提供 LoadParams 物件,並接收傳回的 LoadResult 物件。

收集資料並顯示在 UI 中

如要將分頁串流連結至 UI,請從 ViewModel 取得流程,並傳遞至清單可組合項。

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

使用 collectAsLazyPagingItems 將 PagingData 流程轉換為 LazyPagingItems。接著,在 LazyColumn 中使用 items API 來配置每個項目。

請務必使用 itemKey 為每個項目提供不重複且固定的 ID。以下範例使用 it.id (參照 User.id 屬性),因為在資料更新期間,User 執行個體會保持穩定。

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

分頁程式庫會在載入頁面時使用 null 做為預留位置,因此如果您已啟用預留位置,就必須處理內容區塊中的 null 值。

現在,清單會顯示分頁資料,且 Paging 程式庫會在使用者捲動時載入其他頁面。

其他資源

如要進一步瞭解 Paging 程式庫,請參閱以下資源:

說明文件

查看內容