1. はじめに
学習内容
- ページング ライブラリの主なコンポーネントについて
- プロジェクトにページング ライブラリを追加する方法
作成するアプリの概要
この Codelab では、すでに記事のリストを表示するようになっているサンプルアプリを使用します。リストは静的で、500 件の記事があり、すべてスマートフォンのメモリに保存されています。
この Codelab を通じて、以下を行います。
- ページネーションをコンセプトとして紹介します。
- ページング ライブラリのコア コンポーネントを紹介します。
- ページング ライブラリを使用してページネーションを実装する方法を説明します。
完了すると、アプリは次のようになります。
- ページネーションが適切に実装される。
- さらにデータが取得されたとき、ユーザーに効果的に通知する。
最終的に UI は次のようになります。
必要なもの
推奨
- 次のアーキテクチャ コンポーネントに精通していること: ViewModel、ビュー バインディング、アプリ アーキテクチャ ガイドで提案されているアーキテクチャ。アーキテクチャ コンポーネントの概要については、Room と View に関する Codelab をご覧ください。
- コルーチンと Kotlin Flow に精通していること。Flow の概要については、Kotlin Flow と LiveData を使用した高度なコルーチンの学習の Codelab をご覧ください。
2. 環境をセットアップする
このステップでは、Codelab 全体のコードをダウンロードし、その後、簡単なサンプルアプリを実行します。
できるだけすぐに開始できるように、たたき台として利用できるスターター プロジェクトを用意しています。
git がインストールされている場合は、以下のコマンドをそのまま実行できます。git がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「git --version
」と入力し、正しく実行されることを確認します。
git clone https://github.com/googlecodelabs/android-paging
git がない場合は、次のボタンをクリックして、この Codelab のすべてのコードをダウンロードできます。
コードは basic
と advanced
という 2 つのフォルダに分けられています。この Codelab では、basic
フォルダのみを使用します。
basic
フォルダにはもう 2 つ、start
と end
というフォルダがあります。まず start
フォルダ内のコードを扱います。Codelab の最後には、start
フォルダ内のコードが end
フォルダ内のコードと同じになります。
- Android Studio で
basic/start
ディレクトリのプロジェクトを開きます。 - デバイスまたはエミュレータで
app
実行構成を実行します。
記事のリストが表示されます。最後までスクロールして、リストが静的であること(つまり、リストの最後に到達したとき、それ以上アイテムが取得されないこと)を確認します。一番上までスクロールして戻り、まだすべてのアイテムがあることを確認します。
3. ページネーションの概要
ユーザーに情報を表示する最も一般的な方法として、リストを使用することが挙げられます。しかしそうしたリストでは、ユーザーが利用できるすべてのコンテンツに対して、ごく小さな範囲しか表示されないことがあります。ユーザーが利用可能な情報をスクロールしていくと、多くの場合、すでに表示されている情報を補足するために、より多くのデータが取得されることが予期されます。データを取得するたびに、効率的かつシームレスに行い、増分読み込みでユーザー エクスペリエンスを損なわないようにする必要があります。また増分読み込みでは、アプリが大量のデータをメモリに一度に保持する必要がないため、パフォーマンス上のメリットも得られます。
情報を段階的に取得するこのプロセスを「ページネーション」といい、各「ページ」は、取得するデータのチャンクに対応します。ページをリクエストするために、ページングされるデータソースに、必要な情報を定義するクエリが必要となることがよくあります。この Codelab の残りの部分では、ページング ライブラリを紹介し、アプリにページネーションを迅速かつ効率的に実装するためにどのように役立つかについて説明します。
ページング ライブラリのコア コンポーネント
ページング ライブラリのコア コンポーネントは次のとおりです。
PagingSource
- 特定のページクエリのデータチャンクを読み込むための基本クラス。これはデータレイヤの一部であり、通常はDataSource
クラスから公開され、その後Repository
によってViewModel
で使用するために公開されます。PagingConfig
- ページング動作を決定するパラメータを定義するクラス。これには、ページサイズや、プレースホルダが有効かどうかなどが含まれます。Pager
-PagingData
ストリームの生成を担うクラス。そのためにPagingSource
に依存します。ViewModel
に作成する必要があります。PagingData
- ページングされたデータのコンテナ。データを更新するたびに、自身のPagingSource
に支えられた、対応する個別のPagingData
出力を行います。PagingDataAdapter
-RecyclerView
でPagingData
を提示するRecyclerView.Adapter
サブクラス。PagingDataAdapter
は、KotlinFlow
、LiveData
、RxJavaFlowable
、RxJavaObservable
、に接続でき、さらに、ファクトリ メソッドを使用して静的リストに接続できます。PagingDataAdapter
は、内部のPagingData
読み込みイベントをリッスンし、ページの読み込みに合わせて UI を効率的に更新します。
以降のセクションでは、上記の各コンポーネントの例を実装します。
4. プロジェクトの概要
現在のアプリでは、記事の静的なリストが表示されます。各記事には、タイトル、説明、作成日があります。静的なリストは、アイテムが少ないうちは適切に機能するものの、データセットが大きくなると適切にスケーリングできなくなります。この問題はページング ライブラリを使用してページネーションを実装することで解決しますが、まずはアプリの既存のコンポーネントを確認しましょう。
このアプリは、アプリ アーキテクチャ ガイドで推奨されているアーキテクチャに沿ったものです。各パッケージの内容を以下に示します。
データレイヤ:
ArticleRepository
: 記事のリストを提供し、メモリに保持します。Article
: データレイヤからの情報の表現である「データモデル」を表すクラス。
UI レイヤ:
Activity
、RecyclerView.Adapter
、RecyclerView.ViewHolder
: UI にリストを表示するクラス。ViewModel
: UI が表示する必要のある状態を作成する状態ホルダー。
リポジトリは、articleStream
フィールドを持つ Flow
ですべての記事を公開します。UI レイヤの ArticleViewModel
によって順番に読み取られ、state
フィールドを持つ ArticleActivity
(StateFlow
)の UI で使用するために準備されます。
記事をリポジトリからの Flow
として公開することで、リポジトリは、時間の経過とともに変化したときに提示する記事を更新できます。たとえば記事のタイトルが変更された場合、その変更を articleStream
のコレクタに簡単に伝えることができます。ViewModel
で UI 状態に StateFlow
を使用すると、UI 状態の収集を停止しても(設定変更の際に Activity
が再作成された場合など)、収集を再開した時点で、中断したところから続けることができます。
前述のように、リポジトリの現在の articleStream
は、当日のニュースのみを提示します。一部のユーザーにとってはこれで十分でも、当日の記事すべてをスクロールした後で過去の記事を確認したいユーザーもいる可能性があります。このような期待から、記事の表示はページネーションの対象として適しています。記事のページングを検討すべき理由としては、他に次のようなものがあります。
ViewModel
は、メモリに読み込んだすべてのアイテムをitems
StateFlow
に保持します。データセットが非常に大きくなった場合、パフォーマンスに影響を与える可能性があるため、この点は重要です。- 変更されたときにリスト内の 1 つまたは複数の記事を更新すると、記事のリストが大きくなるほどコストも高くなります。
ページング ライブラリを使用すると、こうした問題をすべて解決しつつ、アプリでデータを段階的に取得(ページネーション)するための一貫した API を提供できます。
5. データのソースを定義する
ページネーションを実装するときは、次の条件を満たしていることを確認する必要があります。
- UI からのデータに対するリクエストを適切に処理し、同じクエリに対して複数のリクエストが同時にトリガーされないようにする。
- 取得したデータを管理しやすい量でメモリに保持する。
- すでに取得しているデータを補完するために、さらにデータを取得するリクエストをトリガーする。
これはすべて PagingSource
で実現できます。PagingSource
は、増分チャンクでデータを取得する方法を指定することで、データのソースを定義します。PagingData
オブジェクトは、RecyclerView
でのスクロールで生成される読み込みヒントに応答して、PagingSource
からデータを取得します。
PagingSource
は記事を読み込みます。data/Article.kt
で、モデルは次のように定義されます。:
data class Article(
val id: Int,
val title: String,
val description: String,
val created: LocalDateTime,
)
PagingSource
を構築するには、以下を定義する必要があります。
- ページングキーのタイプ - より多くのデータをリクエストするために使用するページクエリのタイプの定義。今回は ID が順番に増加することが確実であるため、特定の記事 ID の前または後の記事を取得します。
- 読み込むデータのタイプ - 各ページは記事の
List
を返すため、タイプはArticle
です。 - データの取得元 - 通常は、データベース、ネットワーク リソース、またはその他のページングされたデータのソースです。ただし、この Codelab ではローカルで生成したデータを使用します。
data
パッケージで、ArticlePagingSource.kt
という新しいファイルに PagingSource
の実装を作成します。
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
TODO("Not yet implemented")
}
}
PagingSource
では、load()
と getRefreshKey()
という 2 つの関数を実装する必要があります。
load()
関数はページング ライブラリによって呼び出され、ユーザーがスクロールしたときに表示される追加のデータを非同期で取得します。LoadParams
オブジェクトは、読み込み操作に関する次のような情報を保持します。
- 読み込むページのキー -
load()
が初めて呼び出された場合、LoadParams.key
はnull
になります。今回は、最初のページキーを定義する必要があります。このプロジェクトでは、記事 ID をキーとして使用します。また、最初のページキーのArticlePagingSource
ファイルの先頭に、STARTING_KEY
定数0
を追加します。 - 読み込みサイズ - 読み込むアイテムのリクエスト数。
load()
関数は LoadResult
を返します。LoadResult
のタイプは次のいずれかです。
LoadResult.Page
: 結果が成功の場合。LoadResult.Error
: エラーの場合。LoadResult.Invalid
: 結果の整合性が保証されなくなったためにPagingSource
を無効にする必要がある場合。
LoadResult.Page
の必須の引数は次の 3 つです。
data
: 取得したアイテムのList
。prevKey
: 現在のページより前のアイテムを取得する必要がある場合にload()
メソッドで使用するキー。nextKey
: 現在のページより後のアイテムを取得する必要がある場合にload()
メソッドで使用するキー。
省略可能な引数は次の 2 つです。
itemsBefore
: 読み込んだデータの前に表示するプレースホルダの数。itemsAfter
: 読み込んだデータの後に表示するプレースホルダの数。
読み込みのキーは Article.id
フィールドです。Article
ID は記事ごとに 1 つずつ増加するため、キーとして使用できます。つまり、記事 ID は連続した単調増加の整数です。
nextKey
または prevKey
は、対応する方向に読み込むデータがない場合、null
になります。この例では、prevKey
の場合、次のようになります。
startKey
がSTARTING_KEY
と同じ場合は、このキーより前のアイテムをさらに読み込むことはできないため、null を返します。- それ以外の場合は、リストの最初のアイテムを取得し、それより前の
LoadParams.loadSize
を読み込み、STARTING_KEY
未満のキーを返さないようにします。そのためには、ensureValidKey()
メソッドを定義します。
ページングキーが有効かどうかを確認する、次の関数を追加します。
class ArticlePagingSource : PagingSource<Int, Article>() {
...
/**
* Makes sure the paging key is never less than [STARTING_KEY]
*/
private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}
nextKey
の場合は次のようになります。
- 無限のアイテムの読み込みをサポートしているため、
range.last + 1
を渡します。
また、各記事には created
フィールドがあるため、その値を生成する必要もあります。ファイルの先頭に次の行を追加します。
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
...
}
こうしたコードをすべて配置したら、load()
関数を実装できます。
import kotlin.math.max
...
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
// Start paging with the STARTING_KEY if this is the first load
val start = params.key ?: STARTING_KEY
// Load as many items as hinted by params.loadSize
val range = start.until(start + params.loadSize)
return LoadResult.Page(
data = range.map { number ->
Article(
// Generate consecutive increasing numbers as the article id
id = number,
title = "Article $number",
description = "This describes article $number",
created = firstArticleCreatedTime.minusDays(number.toLong())
)
},
// Make sure we don't try to load items behind the STARTING_KEY
prevKey = when (start) {
STARTING_KEY -> null
else -> ensureValidKey(key = range.first - params.loadSize)
},
nextKey = range.last + 1
)
}
...
}
次に、getRefreshKey()
を実装する必要があります。このメソッドは、バッキング PagingSource
のデータが変更されたために、ページング ライブラリが UI のアイテムを再読み込みする必要がある場合に呼び出されます。PagingSource
の基となるデータが変更され UI で更新する必要がある状況のことを、「無効化」といいます。無効化されると、ページング ライブラリはデータを再読み込みするために新しい PagingSource
を作成し、新しい PagingData
を出力することで UI に通知します。無効化については後のセクションで詳しく説明します。
新しい PagingSource
から読み込む場合、getRefreshKey()
を呼び出し、新しい PagingSource
が読み込みを開始する必要のあるキーを提供して、更新した後にユーザーがリスト内での現在位置を見失わないようにします。
ページング ライブラリの無効化が発生する理由は、次の 2 つのいずれかです。
PagingAdapter
に対してrefresh()
を呼び出したPagingSource
に対してinvalidate()
を呼び出した
返されたキー(この例では Int
)は、LoadParams
引数を介して、新しい PagingSource
における load()
メソッドの次回の呼び出しに渡されます。無効化の後にアイテムが飛ばないよう、返されたキーが、画面を埋めるために十分な数のアイテムを読み込むようにする必要があります。こうすることで、無効化されたデータ内に存在していたアイテムが、新しいアイテムのセットに含まれる可能性が高まり、現在のスクロール位置を維持できるようになります。アプリの実装を確認しましょう。
// The refresh key is used for the initial load of the next PagingSource, after invalidation
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
// In our case we grab the item closest to the anchor position
// then return its id - (state.config.pageSize / 2) as a buffer
val anchorPosition = state.anchorPosition ?: return null
val article = state.closestItemToPosition(anchorPosition) ?: return null
return ensureValidKey(key = article.id - (state.config.pageSize / 2))
}
上のスニペットでは PagingState.anchorPosition
を使用しています。これが、ページング ライブラリでどのようにして追加のアイテムを取得するのかについて知る手がかりとなります。UI は、PagingData
からアイテムを読み取ろうとするとき、特定のインデックスで読み取ろうとします。データが読み取られた場合、そのデータが UI に表示されます。しかしデータがない場合、ページング ライブラリは、失敗した読み取りリクエストを満たすためにデータを取得する必要があることを認識します。読み取り時にデータを正常に取得した最後のインデックスは anchorPosition
です。
更新するときは、anchorPosition
に最も近い Article
のキーを取得して、読み込みキーとして使用します。こうして、新しい PagingSource
からの読み込みを再開すると、取得したアイテムのセットには読み込み済みのアイテムが含まれるため、スムーズかつ一貫したユーザー エクスペリエンスが実現します。
これで PagingSource
の定義が完了しました。次のステップは UI への接続です。
6. UI 用の PagingData を生成する
現在の実装では、ArticleRepository
の Flow<List<Article>>
を使用して、読み込んだデータを ViewModel
に公開しています。ViewModel
は、UI に公開するために stateIn
演算子を使用して、データの常時利用可能な状態を維持します。
ページング ライブラリでは、代わりに ViewModel
から Flow<PagingData<Article>>
を公開します。PagingData
は読み込んだデータをラップし、ページング ライブラリが追加のデータを取得するタイミングを決定します。また、同じページを 2 回リクエストしないようにします。
PagingData
を作成するために、PagingData
をアプリの他のレイヤに渡すために使用する API に応じて、Pager
クラスのさまざまなビルダー メソッドのうちのいずれかを使用します。
- Kotlin
Flow
-Pager.flow
を使用 LiveData
-Pager.liveData
を使用- RxJava
Flowable
-Pager.flowable
を使用 - RxJava
Observable
-Pager.observable
を使用
すでにアプリで Flow
を使用しているので、このアプローチで続けますが、Flow<List<Article>>
を使用する代わりに Flow<PagingData<Article>>
を使用します。
どの PagingData
ビルダーを使用する場合でも、次のパラメータを渡す必要があります。
PagingConfig
。このクラスは、先読みの量や初期読み込みのサイズ リクエストなど、PagingSource
からコンテンツを読み込む方法のオプションを設定します。定義する必要のある必須のパラメータは、ページサイズ(各ページに読み込む必要のあるアイテムの数)のみです。デフォルトで、ページングは読み込んだページをすべてメモリに保持します。スクロールしたときにメモリが浪費されないようにするには、PagingConfig
でmaxSize
パラメータを設定します。デフォルトでは、ページングが読み込まれていないアイテムを数えることができ、かつenablePlaceholders
設定フラグがtrue
である場合、ページングはまだ読み込まれていないコンテンツのプレースホルダとして null アイテムを返します。このようにして、アダプタにプレースホルダ ビューを表示できます。この Codelab では作業を簡単にするために、enablePlaceholders = false
を渡して、プレースホルダを無効にしましょう。PagingSource
の作成方法を定義する関数。ここではArticlePagingSource
を作成するため、その方法をページング ライブラリに伝える関数が必要です。
では、ArticleRepository
を変更してみましょう。
ArticleRepository
を更新する
articlesStream
フィールドを削除します。- 作成した
ArticlePagingSource
を返すarticlePagingSource()
というメソッドを追加します。
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
ArticleRepository
をクリーンアップする
ページング ライブラリはさまざまな処理を行います。
- メモリ内キャッシュの処理
- リストの最後に近づいたときのデータのリクエスト
つまり、ArticleRepository
の articlePagingSource()
以外はすべて削除できます。ArticleRepository
ファイルは次のようになります。
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
ArticleViewModel
でコンパイル エラーが表示されるはずです。ここでどのような変更が必要か見てみましょう。
7. ViewModel で PagingData をリクエストしてキャッシュする
コンパイル エラーに対処する前に、ViewModel
を確認してみましょう。
class ArticleViewModel(...) : ViewModel() {
val items: StateFlow<List<Article>> = ...
}
ページング ライブラリを ViewModel
に統合するために、items
の戻り値の型を StateFlow<List<Article>>
から Flow<PagingData<Article>>
に変更します。そのためには、まずファイルの先頭に ITEMS_PER_PAGE
というプライベート定数を追加します。
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel {
...
}
次に、Pager
インスタンスの出力結果となるように items
を更新します。これを行うには、Pager
に 2 つのパラメータを渡します。
pageSize
がITEMS_PER_PAGE
であってプレースホルダが無効になっているPagingConfig
- 作成した
ArticlePagingSource
のインスタンスを提供するPagingSourceFactory
class ArticleViewModel(...) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
...
}
次に、設定またはナビゲーションの変更があってもページング状態を維持するために、cachedIn()
メソッドを使用して androidx.lifecycle.viewModelScope
を渡します。
上記の変更が完了すると、ViewModel
は次のようになります。
package com.example.android.codelabs.paging.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel(
private val repository: ArticleRepository,
) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
.cachedIn(viewModelScope)
}
PagingData
についてもう 1 つ注意すべき点は、自己完結型であるということです。RecyclerView
に表示されるデータを更新するための変更可能なストリームが含まれています。PagingData
の出力はそれぞれ完全に独立しており、基となるデータセットが変更されたためにバッキング PagingSource
が無効化されると、1 つのクエリに対して複数の PagingData
インスタンスが出力されることがあります。そのため、PagingData
の Flows
は他の Flows
とは独立して公開する必要があります。
これで、ViewModel
にページング機能を追加できました。
8. Adapter を PagingData と連携させる
PagingData
を RecyclerView
にバインドするには、PagingDataAdapter
を使用します。PagingDataAdapter
は、PagingData
コンテンツが読み込まれるたびに通知を受け、更新するよう RecyclerView
に伝えます。
PagingData
ストリームと連携するように ArticleAdapter
を更新する
- 現在、
ArticleAdapter
はListAdapter
を実装していますが、これをPagingDataAdapter
を実装するようにします。クラス本体の残りは変更しません。
import androidx.paging.PagingDataAdapter
...
class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}
ここまでに多くの変更を加えてきましたが、もう一歩でアプリを実行できるところまで来ました。あとは UI を接続するだけです。
9. UI で PagingData を使用する
現在の実装には、特定の条件が満たされた場合に ViewModel
を呼び出して追加のデータを読み込む、binding.setupScrollListener()
というメソッドがあります。ページング ライブラリはこれをすべて自動的に行うため、このメソッドとその使用方法は削除できます。
次に、ArticleAdapter
が ListAdapter
ではなく PagingDataAdapter
になったため、小さな変更を 2 つ加えます。
Flow
の終了演算子を、ViewModel
からcollect
ではなくcollectLatest
に切り替えます。submitList()
ではなくsubmitData()
を使用して、ArticleAdapter
に変更を通知します。
pagingData
Flow
で collectLatest
を使用して、新しい pagingData
インスタンスが出力されたときに、pagingData
の以前の出力に関する収集がキャンセルされるようにします。
こうした変更を行うと、Activity
は次のようになります。
import kotlinx.coroutines.flow.collectLatest
class ArticleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityArticlesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val viewModel by viewModels<ArticleViewModel>(
factoryProducer = { Injection.provideViewModelFactory(owner = this) }
)
val items = viewModel.items
val articleAdapter = ArticleAdapter()
binding.bindAdapter(articleAdapter = articleAdapter)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
items.collectLatest {
articleAdapter.submitData(it)
}
}
}
}
}
private fun ActivityArticlesBinding.bindAdapter(
articleAdapter: ArticleAdapter
) {
list.adapter = articleAdapter
list.layoutManager = LinearLayoutManager(list.context)
val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
list.addItemDecoration(decoration)
}
アプリがコンパイルされて実行されます。これで、アプリをページング ライブラリに移行できました。
10. 読み込み状態を UI に表示する
ページング ライブラリが追加のアイテムを取得して UI に表示するときは、ユーザーに対し、追加のデータがある旨を通知することをおすすめします。幸い、ページング ライブラリには、CombinedLoadStates
型で読み込み状態にアクセスする便利な方法が用意されています。
CombinedLoadStates
インスタンスは、データを読み込むページング ライブラリ内の、すべてのコンポーネントの読み込み状態を記述します。今回は ArticlePagingSource
の LoadState
だけに着目するため、主に CombinedLoadStates.source
フィールドの LoadStates
型を使用します。CombinedLoadStates
には、PagingDataAdapter.loadStateFlow
を介して PagingDataAdapter
からアクセスできます。
CombinedLoadStates.source
は LoadStates
型であり、3 種類の LoadState
のフィールドがあります。
LoadStates.append
: ユーザーの現在位置より後に取得されるアイテムのLoadState
用。LoadStates.prepend
: ユーザーの現在位置より前に取得されるアイテムのLoadState
用。LoadStates.refresh
: 初期読み込みのLoadState
用。
各 LoadState
自体は次のいずれかです。
LoadState.Loading
: アイテムを読み込んでいます。LoadState.NotLoading
: アイテムを読み込んでいません。LoadState.Error
: 読み込みエラーが発生しました。
今回は、ArticlePagingSource
にエラーケースが含まれていないため、LoadState
が LoadState.Loading
であるかどうかだけを考慮します。
まず、UI の上部と下部に進行状況バーを追加し、いずれかの方向の取得について、読み込み状態を示します。
activity_articles.xml
で、次のように 2 つの LinearProgressIndicator
バーを追加します。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ArticleActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/prepend_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/append_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
次に、PagingDataAdapter
から LoadStatesFlow
を収集して CombinedLoadState
に対応します。ArticleActivity.kt
で状態を収集します。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
articleAdapter.loadStateFlow.collect {
binding.prependProgress.isVisible = it.source.prepend is Loading
binding.appendProgress.isVisible = it.source.append is Loading
}
}
}
lifecycleScope.launch {
...
}
最後に、ArticlePagingSource
に遅延を少し加えて読み込みをシミュレートします。
private const val LOAD_DELAY_MILLIS = 3_000L
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val start = params.key ?: STARTING_KEY
val range = startKey.until(startKey + params.loadSize)
if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
return ...
}
アプリをもう一度実行し、リストの一番下までスクロールします。ページング ライブラリが追加のアイテムを取得している間は下部に進行状況バーが表示され、取得が完了すると消えます。
11. まとめ
今回の内容を振り返りましょう。
- ページネーションの概要と、それが必要な理由を確認しました。
Pager
を作成し、PagingSource
を定義して、PagingData
を出力することで、アプリにページネーションを追加しました。cachedIn
演算子を使用して、ViewModel
でPagingData
をキャッシュに保存しました。PagingDataAdapter
を使用して UI でPagingData
を使用しました。PagingDataAdapter.loadStateFlow
を使用してCombinedLoadStates
に対応しました。
以上です。高度なページングのコンセプトについて詳しくは、高度なページングに関する Codelab をご覧ください。