ページング データを読み込んで表示する

ページング ライブラリには、大規模なデータセットからページング データを読み込み、表示する強力な機能があります。このガイドでは、ページング ライブラリを使用し、ネットワーク データソースからのページングデータのストリームを設定して Lazy Listで表示する方法について説明します。

データソースを定義する

最初のステップは、データソースを識別する PagingSource 実装を定義することです。PagingSource API クラスには load メソッドが含まれています。このメソッドをオーバーライドして、対応するデータソースからページング データを取得する方法を示します。

非同期読み込みに Kotlin コルーチンを使用するには、PagingSource クラスを直接使います。

キーと値の型を選択する

PagingSource<Key, Value> には、KeyValue の 2 種類のパラメータがあります。キーは、データの読み込みに使用される識別子を定義します。値はデータ自体の型です。たとえば、User 型のページ番号を Retrofit に渡してネットワークから User オブジェクトのページを読み込む場合は、Int 型として Key を選択し、User 型として Value を選択します。Int

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 の呼び出しが成功したかどうかに応じて、次の 3 つの形式のいずれかになるシールクラスです。

  • 読み込みが正常に完了すると、LoadResult.Page オブジェクトが返されます。
  • 読み込みが成功しなかった場合は、LoadResult.Error オブジェクトが返されます。
  • PagingSource が無効になり、新しいインスタンスに置き換える必要がある場合(基盤となるデータが変更された場合など)、LoadResult.Invalid オブジェクトが返されます。

次の図は、この例の load 関数が各読み込みのキーを受け取り、後続の読み込みのキーを提供する方法を示しています。

load 呼び出しごとに、ExamplePagingSource が現在のキーを受け取り、次に読み込むキーを返します。
図 1.load がキーをどのように使用および更新するかを示す図。

PagingSource の実装では、PagingState オブジェクトをパラメータとして受け取る getRefreshKey メソッドも実装する必要があります。初期読み込み後にデータが更新または無効化されたときに 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 クラスには PagingData オブジェクトの PagingSource からのリアクティブストリームを表示するメソッドが用意されています。ページング ライブラリは、データのストリームを Flow として公開します。

リアクティブ ストリームを設定するために Pager インスタンスを作成する場合は、 インスタンスに PagingConfig 構成オブジェクトと、 PagerPagingSource 実装のインスタンスを取得する方法を指示する関数を提供する必要があります。次の例をご覧ください。

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 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 を使用して、各アイテムに一意の永続的な識別子を指定してください。次の例では、it.idUser.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()
            }
        }
    }
}

ページが読み込まれている間、Paging ライブラリはプレースホルダに null を使用します。そのため、プレースホルダを有効にしている場合は、コンテンツ ブロックで null 値を処理する必要があります。

これで、リストにページング データが表示され、ユーザーがスクロールすると、ページング ライブラリが追加のページを読み込みます。

参考情報

ページング ライブラリについて詳しくは、以下の参考情報をご覧ください。

ドキュメント

コンテンツを表示する