📝

Paging 3 の使い方を勉強した時のメモ

2024/03/30に公開

Paging 3

概要

Android Jetpack の一部で、アプリ内でスクロール可能なリストやグリッドのデータを効率的に読み込んで表示するためのライブラリ。

大量のデータセットから少量ずつデータを読み込むことでメモリ消費を抑え、応答性の高い UI を維持する。

特徴

  • データの非同期読み込み:バックグラウンドスレッドでデータを読み込むことで、UIスレッドをブロックせずにデータを読み込む。
  • リストの無限スクロール:ユーザーがリストの末尾に到達すると自動的に次のデータセットを読み込む。
  • エラーハンドリングと再試行:ネットワークエラーやデータの読み込みエラーが発生した場合に、エラーを表示し、再試行のオプションを提供する。

実装例

Paging 3 の基本的な構成要素

  • PagingSource:データソースからページ単位でデータを取得する方法を定義
  • Pager:PagingSource を使用して PagingData ストリームを作成
  • PagingData:ページングされたデータのコンテナで、UI に表示するデータセットを表す
  • LazyPagingItems:PagingData を Jetpack Compose で表示するために使用されるアダプターの役割を果たす

サンプルコード

Ktor で API を叩き、取得したデータのリストを Jetpack Compose で表示する時のサンプル。

依存関係

アプリレベルの build.gradle に以下を追加。

dependencies {
    // 略
    
    // 2つ追加
    implementation("androidx.paging:paging-runtime-ktx:3.2.1")
    implementation("androidx.paging:paging-compose:3.3.0-alpha05")
}

1.PagingSource の定義

import androidx.paging.PagingSource
import androidx.paging.PagingState
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

class MyPagingSource(
    private val httpClient: HttpClient // Ktor のクライアント
) : PagingSource<Int, MyData>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> {
        return try {
            val page = params.key ?: 1
            val response = withContext(Dispatchers.IO) {
                httpClient.get("https://myapi.com/data?page=$page") // レスポンスを取得する
            }
            val data = Json.decodeFromString<List<MyData>>(response.bodyAsText()) // Json をデシリアライズ

            LoadResult.Page(
                data = data, // ページのデータ
                prevKey = if (page == 1) null else page - 1,  // 前のページ番号
                nextKey = if (data.isEmpty()) null else page + 1 // 後ろのページ番号
            )
        } catch (e: Exception) {
            LoadResult.Error(e) 
        }
    }

    override fun getRefreshKey(state: PagingState<Int, MyData>): Int? {
        // ここにリフレッシュ時のキーを決定するロジックを書く
        return state.anchorPosition
    }
}

loadメソッド

ページングデータのロードを行うための主要なメソッド。

非同期でデータをロードし、その結果を LoadResult オブジェクトとして返す。

必要な要素

  • params:LoadParams 型のパラメータで、ページング要求の情報を含んでいる。主に、key(現在のページ位置またはページ番号)と loadSize(ロードしたいデータの量)が含まれる。
  • 戻り値:LoadResult.Page または LoadResult.Error。正常にデータをロードできたかどうかによって戻り値が変わる。
    コード例
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> {
        try {
            val currentPage = params.key ?: 1 // 最初のページの場合は、デフォルト値として1を使用
            val response = // APIからデータをロードするロジック
            val responseData = // レスポンスからデータを抽出するロジック
    
            return LoadResult.Page(
                data = responseData,
                prevKey = if (currentPage == 1) null else currentPage - 1, // 最初のページの場合は前のページなし
                nextKey = if (responseData.isEmpty()) null else currentPage + 1 // データがない場合は次のページなし
            )
        } catch (exception: Exception) {
            return LoadResult.Error(exception)
        }
    }
    
    

getRefreshKeyメソッド

getRefreshKeyメソッドは、Pagingライブラリがリフレッシュ操作(下に引っ張って更新など)を行う際に、どのページからロードを再開するかを決定するために使用する。

必要な要素

  • state:PagingState 型の状態情報で、ページングの現在の状態を含んでいる。
    コード例
    ユーザーがリストのどこにいるのか(anchorPosition) に基づいて、最も近いページのキーを計算し、そのページの前後のページを判断して適切なリフレッシュ位置を返す例。

    override fun getRefreshKey(state: PagingState<Int, MyData>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }
    
    

2.Pager のセットアップと ViewModel 内での使用

import androidx.lifecycle.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import io.ktor.client.*
import kotlinx.coroutines.flow.Flow

class MyViewModel(
    private val httpClient: HttpClient // Ktor のクライアント
) : ViewModel() {

    val myDataFlow: Flow<PagingData<MyData>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = false
        ),
        pagingSourceFactory = { MyPagingSource(httpClient) }
    ).flow
}

Pagerクラス

ページングデータを非同期にロードし、PagingData のストリームとして提供する。

主要な要素

  • PagingConfig:ページングの設定を指定するオブジェクト。ページサイズ、プレフェッチ距離、メモリ内キャッシュのサイズ、プレースホルダーの使用有無などを制御する。
  • PagingSource:データソースからページングデータをロードするためのオブジェクト。ローカルデータベースやネットワークAPIからデータを取得するロジックを実装する。
  • Pager.flow:ページングデータの Flow<PagingData<T>> を提供する。
    コード例
    class MyViewModel : ViewModel() {
        // PagingConfigの設定
        private val pagingConfig = PagingConfig(
            pageSize = 20, // 一度にロードするデータの数
            enablePlaceholders = false, // プレースホルダーを使用するかどうか
            prefetchDistance = 5 // ユーザーがスクロールしているときに先読みするデータの数
        )
    
        // Pagerオブジェクトの生成と、PagingDataのFlowを提供
        val pagingDataFlow: Flow<PagingData<MyData>> = Pager(
            config = pagingConfig,
            pagingSourceFactory = { MyPagingSource() } // MyPagingSourceはPagingSourceを継承したクラス
        ).flow
            .cachedIn(viewModelScope) // ViewModelのスコープ内でPagingDataをキャッシュする
    }
    
    

3.Jetpack Compose でのデータ表示

@Composable
fun MyDataList(myDataFlow: Flow<PagingData<MyData>>) {
    val lazyPagingItems = myDataFlow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems.itemCount) { index ->
            val item = lazyPagingItems[index]
            if (item != null) {
                Text(text = item.toString()) // 実際にはカスタムデザインを適用
            }
        }
    }
}

おわり

近いうちに RemoteMediator の使い方も学びたい

Discussion