0Pricing
Android Academy · レッスン

PagingSourceとPager

ページの読み込み方法を定義します。

「PagingSourceとPager」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

ページの読み込み方法を定義する

ネットワークAPIをページングするには、PagingSourceを作成します。Paging 3に対して、次の2つの問いに答える役割を担います。

  • 指定されたキーのページをどのように読み込むか
  • ユーザーが更新したとき、どこから再開するか

次に、それをPagerでラップして、Flow<PagingData>に変換します。

PagingSource の型パラメーター

PagingSource<Key, Value>は2つの型パラメーターを受け取ります。

  • Key - ページを識別します。ページ番号を使うAPIではInt、カーソルAPIではStringトークンです。
  • Value - 項目の型です。たとえばArticleです。
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

load() の実装

load()はsuspend関数です。params.key(取得するページ)を受け取り、LoadResultを返します。

成功した場合は、項目と前後のキーを含むLoadResult.Pageを返します。キーがnullの場合、その方向にはページがないことを表します。

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

prevKey と nextKey が重要な理由

Pagingは、ユーザーが下へスクロールするときにnextKeyを使って次のページを読み込み、上へスクロールするときにprevKeyを使って前のページを読み込みます(リストの途中から開始する場合に便利です)。

nextKeyにnullを返すと、Pagingはこれ以上ページがないと判断し、リクエストを停止します。これを忘れると、空のリクエストが無限に発生する可能性があります。

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

getRefreshKey() の実装

リストが更新されたとき(プル・トゥ・リフレッシュや無効化など)、ユーザーがほぼ同じ位置に留まれるように、Pagingは再読み込みするページを知る必要があります。

getRefreshKey()は現在のanchorPosition(ビューポートに最も近い項目の位置)を使って、適切なキーを選びます。

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

エラーを適切に処理する

ネットワーク呼び出しをtry/catchで囲み、失敗時にはLoadResult.Error(e)を返します。PagingはこれをUIのLoadState.Errorとして公開するため、再試行ボタンを表示できます。

例外をload()の外へそのまま伝播させないでください。捕捉してLoadResult.Errorに変換します。

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

Pager の作成

Pagerは、PagingConfigと新しいPagingSourceを構築するファクトリを結び付けます。その.flowプロパティは、UIが収集するFlow<PagingData>です。

Pagingは更新時にソースを無効化して再作成するため、ファクトリラムダは呼び出されるたびに新しいソースを作成する必要があります。

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

ViewModel での cachedIn

PagingDataの収集は1回限りの操作です。再度収集すると読み込みが再開されます。構成変更をまたいで状態を維持し、複数のコレクターでデータを共有するには、cachedInを使ってviewModelScope内にFlowをキャッシュします。

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize と pageSize

load()が読み取るのは、設定したpageSizeではなくparams.loadSizeであることに注意してください。

最初の読み込みでは、Pagingがより大きな初期チャンクをリクエストする場合があります(PagingConfigのinitialLoadSizeで制御され、デフォルトではページサイズの3倍です)。Pagingが返すことを想定しているサイズとリクエストを一致させるため、必ずparams.loadSizeをAPIに渡してください。

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

カーソルベースのAPI

すべてのAPIがページ番号を使うわけではありません。次のページを指すカーソルやトークンを返すAPIもあります。パターンは同じで、Keyの型をStringに変更し、レスポンスから取得したトークンを使うだけです。

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

全体を組み合わせる

これでデータレイヤー全体が完成しました。1ページを読み込むPagingSource、ページをストリーミングするPager、そしてFlowをキャッシュするViewModelです。

UIレイヤーではviewModel.articlesを収集するだけです。次のレッスンで、ComposeのLazyColumnを使って表示します。

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

確認問題

ページ番号を使うPagingSourceで、load()からnextKey = nullを返すと何を示しますか?

まとめ:PagingSource と Pager

ページングのデータレイヤーを構築しました。

  • PagingSource<Key, Value>はload()とgetRefreshKey()を実装する
  • load()はprevKey/nextKeyを含むLoadResult.Page、またはLoadResult.Errorを返す
  • キーがnullになると、その方向のページングが停止する
  • Pager(config, factory).flowはFlow<PagingData>を生成する
  • cachedIn(viewModelScope)により、構成変更後もデータを維持できる

次は、このFlowをComposeのリストに表示します。

よくある質問

「PagingSourceとPager」レッスンは無料ですか?

はい。「PagingSourceとPager」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「PagingSourceとPager」で何を学びますか?

ページの読み込み方法を定義します。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「PagingSourceとPager」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Pagingが必要な理由
  2. PagingSourceとPager
  3. ComposeのリストでPagingを使う
  4. RemoteMediatorとキャッシュ
← Android Academyに戻る