AndroidのPaging3を理解する
初めに
ポートでAndroid開発をしている @shxun6934 です。
業務内で、Paging3を導入し、ページングでのリスト表示を実装しました。
本記事では、Paging3について紹介しつつ、理解していこうと思います。
Google公式のAndroidドキュメントは豊富でわかりやすいので、そちらも参考にしてください。
また、Codelab
でコードを書きながら学ぶこともできます。
- 基本のCodelab: https://developer.android.com/codelabs/android-paging-basics?hl=ja#0
- 高度なCodelab: https://developer.android.com/codelabs/android-paging?hl=ja#0
Paging3とは
ページングを実装する際に使用するAndroidのライブラリーです。
APIから取得するデータやDBに保存しているデータを、RecyclerViewやComposeのLazyListのようなリストで、ページネーションとして表示するために使用します。
(他の言語・フレームワークでは、railsのkaminari
やreactのreact-paginate
のようなライブラリーと似ています。)
FlowやLiveDataなどのObserveをサポートしているので状態を監視することができたり、更新処理や再試行処理などを簡単に行うことができます。
androidx.paging
のライブラリーで、バージョンを3.x.x
にすると使用できます。
Paging3に関するクラス
コードを見ていく前に、Paging3でよく使用するクラスと簡単な概要をまとめておきます。
(公式ドキュメントや実際のライブラリーのコードを読んで、自分なりに解釈しました。間違ってたらすいません。。。)
クラス | 用途 |
---|---|
PagingSource |
表示するデータのソース(取得先)を定義するクラス。ソースからの取得方法や次のデータリストを取得するためのkeyを設定する。 |
RemoteMediator |
リモートから取得したデータをローカルデータに入れ込みながら、データをページングするために使用するクラス。例えば、APIから取得したデータをDBにキャッシュして、データを表示したい時に使用する。 |
PagingData |
ページングによってロードされたデータのコンテナクラス。このクラスをFlowやLiveDataでラップすることでデータを監視することができる。 |
PagingConfig |
PagingSourceで設定した取得方法でデータをロードする際の設定をするクラス。初期取得件数やページサイズなどを設定できる。 |
Pager |
PagingDataのリアクティブストリームを生成するためのクラス。PagingSourceで設定した取得方法とPagingConfigで設定した取得設定でロードし、PagingDataを生成する。 |
LazyPagingItems |
FlowでラップされたPagingDataへのアクセスを担当するクラス。このクラスのインスタンスをLazyListScope.itemsで使用できる。(Composeのみ。) |
PagingDataAdapter |
RecyclerViewでPagingDataを表示するためのAdapterクラス。RecyclerViewで表示するデータの設定を行う。(xml、data-bindingのみ。) |
LoadState |
ページングのロードの状態を表すクラス。NotLoading、Loading、Errorの3種類が存在する。 |
LoadStates |
LoadStateのコレクションクラス。refresh、prepend、appendごとのLoadStateを保持している。 |
LoadState
LoadState
は以下の3種類の状態を持っています。
状態 | 説明 |
---|---|
NotLoading |
ロードもされておらず、エラーもない状態。endOfPaginationReached プロパティを持っていて、このプロパティで最後のデータまで達したかどうかがわかる。 |
Loading |
ロードしている状態。 |
Error |
ロードした結果、エラーになった状態。error プロパティを持っていて、このプロパティでなんのエラーが発生したかがわかる。 |
LoadStates
LoadStates
は以下の3種類のプロパティを持っています。
これらのプロパティは、LodeType
の3種類に該当します。
種類 | 説明 |
---|---|
REFRESH |
コンテンツの更新。ただし、初期ロードやPagingSourceの無効化も含む。 |
PREPEND |
現在のPagingDataの先頭にデータを追加する。 |
APPEND |
現在のPagingDataの末尾にデータを追加する。 |
Paging3の実装
クラスについて簡単に理解したところで、実際にコードを書きながら、実装方法をみていこうと思います。
前提
「ユーザー一覧を表示する」機能を作成すると仮定して、実装していきます。
その際、キャッシュデータを使用しない方針で進めていこうと思うので、RemoteMediator
を使用せず、PagingSource
のみの実装になります。
また、UIの描画は、Composeでの実装になります。
データソースの定義
まず、データのソースの定義をします。
今回は、以下のような要件があるとします。
- WebのREST APIでデータを取得できる。
- IDと名前のデータを持つユーザーのデータリストが取得できる。エンドポイントは、
/users
。 - APIにもページネーションが実装されていて、ページングに関する情報はURLクエリで渡す。
- 整数で次のページングができる。初期値は
1
。ページングがnull
の場合、ページングできるデータがないことを指す。 - 取得できるページサイズも指定できる。
Userモデル
「IDと名前のデータを持つユーザーのデータ」の受け皿を用意するために、id
とname
をプロパティに持つUser
データクラスを作成しておきます。
@Serializable
data class User(
val id: Int,
val name: String
)
GET Users
Retrofit
のようなAPIClientでは、以下のようにRESTのGETメソッドを呼ぶことができると思います。(詳しい実装は省きます。)
// APIレスポンス用のクラス
data class Response<T: Any>(
val data: T,
val nextPage: Int
)
interface UserApi {
@GET("/users")
suspend fun getUsers(@Query("page") page: Int, @Query("pageSize") pageSize: Int): Response<List<User>>
}
UserPagingSource
ユーザー一覧を取得するソースと取得方法を定義するため、UserPagingSource
を作成します。
UserPagingSource
に、UserApi
を依存させ、PagingSourceを継承させます。
PagingSourceのジェネリクスは、<ページングに使用するkeyの型, ページングに使用するデータの型> を定義します。
今回は、Int
とUser
を指定します。
loadメソッド と getRefreshKeyメソッド はabstruct
なので、必ず宣言します。
load
メソッドでは、APIからデータを取得し、成功した場合はLoadResult.Page
を、失敗した場合はLoadResult.Error
を返すようにします。
getRefreshKey
メソッドでは、データを破棄する前のkeyに最も近いkeyを返すようにします。(公式ドキュメントより。)
class UserPagingSource(
private val api: UserApi
) : PagingSource<Int, User>() {
// データをソースからロードした結果を返す。
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
try {
val response = api.getUsers(page = params.key ?: 1, pageSize = params.loadSize)
return LoadResult.Page(
data = response.data,
prevKey = params.key,
nextKey = response.nextPage
)
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
// データを破棄した後、再取得する(再びloadを呼び出す)際に使用するkeyを設定する。
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
PagingDataの設定
PagingSourceの設定ができたので、PagingSourceをloadした結果をUIに渡すメソッドを作成します。
Pager
にPagingConfig
とUserPagingSource
のインスタンスを渡してPager
のインスタンスを作成し、.flow
によってPagingData
をFlowでラップし、その値を返すようにします。
PagingConfig
は、初期取得数 と ページサイズ を設定できるので、10件に設定しておきます。
object UserList {
fun getUsers(api: UserApi): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(
initialLoadSize = 10, // 初期取得数、ページサイズを10件に設定。
pageSize = 10
)
) {
UserPagingSource(api)
}.flow
}
}
ページングを表示
UIへFlow<PagingData<User>>
を渡すようにできたので、これをLazyPagingItems
に変換し、LazyListScope
コンポーネントで表示します。
LazyPagingItems
のインスタンスはLoadStates
を参照できるので、LoadStates
の各プロパティの状態によって画面を出し分けるように制御します。
ローディング中はローディング画面を、エラーの場合は空の画面を、取得成功の場合はリスト画面を表示するようにします。
UserListViewModel
ユーザー一覧画面用のViewModel、UserListViewModel
を作成します。
ViewModelにUIで使用するデータを持たせるようにします。
class UserListViewModel : ViewModel() {
private val retrofit = Retrofit.Builder().~~~~.build()
private val api: UserApi = retrofit.create(UserApi::class.java)
val paging: Flow<PagingData<User>> = UserList.getUsers(api).cachedIn(viewModelScope)
}
UserListActivity
ユーザー一覧画面用のActivity、UserListActivity
を作成します。
Composeでの使用を想定しているので、以下のようなコンポーネントを作成します。
-
UserListScreen
: ユーザー一覧画面の親コンポーネント。このコンポーネント内で、ページングの状態によってコンポーネントの表示制御を行う。 -
LoadingScreenView
: 全画面ローディング。データの取得中に表示する。 -
EmptyView
: 空の画面。データの中身が空の場合やエラーの場合に表示する。 -
UserColumnList
: ユーザーリスト。データを正常に取得できた場合に表示する。 -
LoadingIndicator
: ローディングインディケータ。
UserListViewModel
のpaging
をcollectAsLazyPagingItems
メソッドを使用して、LazyPagingItems
に変換します。
LazyPagingItems
のインスタンスから、REFRESH
・PREPEND
・APPEND
のそれぞれの状態を保持し、それぞれの状態をみて、コンポーネントの表示を切り替えるようにします。
class UserListActivity : ComponentActivity() {
private val viewModel: UserListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
UserListScreen()
}
}
}
@Composable
private fun UserListScreen() {
val lazyPagingItems = viewModel.paging.collectAsLazyPagingItems() // LazyPagingItemsのインスタンスに変換
val pagingLoadStates = lazyPagingItems.loadState.source
val refreshLoadState = pagingLoadStates.refresh // 更新時
val prependLoadState = pagingLoadStates.prepend // 前データを読み込む時
val appendLoadState = pagingLoadStates.append // 後データを読み込む時
when {
refreshLoadState is LoadState.Error || prependLoadState is LoadState.Error || appendLoadState is LoadState.Error -> {
// 何かしらのエラーが発生した
EmptyView()
}
refreshLoadState is LoadState.Loading -> {
// 初期ローディング中または再更新でのローディング中
LoadingScreenView()
}
refreshLoadState is LoadState.NotLoading -> {
// データを取得できた
LazyColumn {
if (prependLoadState is LoadState.Loading) {
// 前データを読み込み中
item {
LoadingIndicator()
}
}
UserColumnList(lazyPagingItems)
if (appendLoadState is LoadState.Loading) {
// 後データを読み込み中
item {
LoadingIndicator()
}
}
}
}
}
}
}
これでページングのリスト表示ができると思います。
終わりに
Paging3の実装(PagingSourceのみ)を書きながら、みてきました。
操作するクラス・オブジェクトが多く、クラス・オブジェクトの意味や用途をしっかり理解していないと、正しい挙動にならず、理解するまでは苦戦すると思います。
(自分は結構理解するまで苦戦しました。特にREFRESH
・PREPEND
・APPEND
の部分。)
ですが、理解した時はページングの実装がより簡単にできるものでもあると思うので、細かいクラス・オブジェクトの仕様やメソッドの挙動をもう少し深く理解できたら、難しいページングも実装できるのではないかなと思います。
おまけ
REFRESH
・PREPEND
・APPEND
の状態の変化のログ。(後データのみ取得していく機能の場合)
Discussion