載入並顯示分頁資料

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

定義資料來源

首先,您必須定義 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 物件包含要執行的載入作業相關資訊。其中包括要載入的鍵和要載入的項目數量。

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

  • 如果載入成功,就傳回 LoadResult.Page 物件。
  • 如果載入失敗,則傳回 LoadResult.Error 物件。
  • 如果 PagingSource 不再有效,且應由新執行個體取代 (例如,由於基礎資料變更),請傳回 LoadResult.Invalid 物件。

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

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

PagingSource 實作項目也必須實作 getRefreshKey 方法,後者採用 PagingState 物件做為參數。當資料重新整理或在初始載入作業完成後失效時,這個方法會傳回要傳入 load 方法的鍵。在後續重新整理資料時,分頁程式庫會自動呼叫這個方法。

處理錯誤

有很多原因會造成資料載入要求失敗,尤其是透過網路載入時。從 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 物件顯示回應式串流。分頁程式庫會將資料串流公開為 Flow。

建立 Pager 執行個體以設定回應式串流時,你必須為執行個體提供 PagingConfig 設定物件和一個用於通知 Pager 如何取得 PagingSource 實作執行個體的函式,如下列範例所示。

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

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        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 程式庫,請參閱以下資源:

說明文件

Views content