初めに
本記事では、Paging3というライブラリを用いたページング処理の実装について自分の理解をまとめておこうと思います。
ページングとは
ページングは大量のデータを少しづつ読み込むことでアプリのパフォーマンスを向上させる仕組みです。
ページングのメリット
ページング導入のメリットとして、UXの向上が挙げられます。APIを叩いてのデータ取得や、ローカルに保存したデータの取得を行う際に、大量のデータを一度にすべて読み込もうとするとユーザーを長時間待たせることになります。しかし、Pagingを用いるとすぐに必要になる(=画面に表示する必要のある)データのみを取得するため、アプリの応答速度を改善することができます。
Paging3に関するクラス
Paging3における主な構成要素となるクラスについて、以下に簡単な概要をまとめておきます。
名前 | 概要・用途 |
---|---|
PagingSource | データソースからのデータの取得方法を定義するクラス |
PagingData | ページングによってロードするデータのコンテナとなるクラス |
Pager | PagingSourceからデータを取得してPagingDataを作成するクラス |
PagingConfig | PagingSourceで定義した方法でデータを取得する際の設定をするクラス |
LazyPagingItems | PagingDataにアクセスし、UIに表示するために使用するクラス |
ページングの実装
今回は、RetrofitでJSONPlaceholderというモックAPIを叩いて情報を取得し、それをUI側で表示するというケースを例にして実装を行います。
JSONPlaceholderの詳細
PagingSourceの実装
class PostPagingSource(
private val apiService: APIService
) : PagingSource<Int, Post>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Post> {
return try {
// 次にロードするページのキー
val page = params.key ?: 1
// 1ページあたりのアイテム数
val limit = params.loadSize
val response = apiService.getPosts(page = page, limit = limit)
LoadResult.Page(
data = response,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Post>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
PagingSourceの拡張クラスにおいては、loadメソッドとgetRefreshKeyメソッドの2つをオーバーライドする必要があります。
loadメソッド
loadメソッドはページングデータのロードを行うためのメソッドです。
引数として、LoadParams
オブジェクトを受け取りますが、これはメンバとしてkey
(現在のページ番号)とloadSize
(1回のリクエストで取得するアイテムの数)を持っています。なお、loadSize
は後述するpagingConfig
によって決定されます。
オーバーライドしたloadメソッド内では、まず次にロードするページのキーと1ページ当たりのアイテム数を取得し、これをもとに非同期にAPIを叩いてデータをロードしています。
なお、今回APIを叩くときのリクエストは以下のようになっています。(例:page=1,limit=10のとき)
https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10
このとき、1ページ目の最初の10件のデータを取得することができます。
なお、ロードした結果はLoadResult
オブジェクトで返されます。これは、データの取得に成功したらLoadResult.Page
を、失敗したらLoadResult.Error
を返します。LoadResult.Page
は以下の3つのメンバが含まれます。
- data:取得したアイテムのリスト
- prevKey:現在のページの1つ前のページをロードするときに使用するキー
- nextKey:現在のページの1つ後のページをロードするときに使用するキー
上記のコードでは、APIを叩いて取得したresponseをdataに格納し、ページが先頭だったときはprevKeyを、ページが最後尾だったときはnextKeyをnullにしています。
getRefreshKeyメソッド
PagingSource
の拡張クラスは、これに加えてgetRefreshKeyメソッドもオーバーライドします。引数として、PagingState
オブジェクトを受け取りますが、これはページングの現在の状態を管理するオブジェクトです。このメソッドでは、PagingState
の持つ情報をもとにしてどのページをリロードするのかを決定します。
getRefreshKeyメソッドを呼び出す必要のある状況としては、以下のようなものが挙げられます。
- ユーザーが手動でリフレッシュ(再読み込み)した場合
- 初回のデータ読み込みのとき
- Roomデータベースが変更された場合(PagingSourceがローカルのDBであるケース)
このような状況では、現在表示されている位置を考慮して適切なページを再取得する必要があります。
上記のコードでは、PagingStateが持つanchorPosition
(現在のスクロール位置)の情報をもとにそれに最も近いページ情報をclosestPagetoPosition
から取得、そしてそのページを基準としてprevKey,nextKeyを更新します。
ViewModelの作成
class PostsViewModel() : ViewModel() {
private val pagingConfig = PagingConfig(
pageSize = 5, // 1回のデータ取得で取得するアイテム数
enablePlaceholders = false, // プレースホルダーを無効にする
initialLoadSize = 10, // 最初の読み込みで取得するデータ数
prefetchDistance = 5 // あらかじめデータを読み込む距離
)
val items: Flow<PagingData<Post>> = Pager(
config = pagingConfig,
pagingSourceFactory = { PostPagingSource(Retrofit.api) }
).flow.cachedIn(viewModelScope) //画面回転などでもデータを保持
}
PagingConfig
では、ページングの動作を制御するための各種設定を行っています。
上記のコードにおいては、pageSize
で1回のデータ取得で取得するアイテム数を、initialloadsize
で初回の読み込みで取得するデータ数を指定します。prefetchDistance
ではユーザーのスクロールで次のページまでのアイテム数が5件になったらあらかじめ次のページをダウンロードするという設定にしています。
また、そのPagingConfig
での設定に従ってPager
でPagingSource
からロードしたデータをitemsに格納しています。このitemsの型はFlow<PagingData<Post>>
となっています。これにより、非同期にデータの更新・取得を行い、UI側に反映することができます。
UI上での表示
@Composable
fun PostsScreen(viewmodel: PostsViewModel) {
val posts = viewmodel.items.collectAsLazyPagingItems()
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
items(posts.itemCount) { index ->
val post = posts[index]
PostCard(
post = post!!
)
}
posts.apply {
when {
loadState.refresh is LoadState.Loading -> {
item { CircularProgressIndicator() }
}
loadState.append is LoadState.Loading -> {
item { CircularProgressIndicator() }
}
loadState.append is LoadState.Error -> {
item {
Text(text = "Error loading data")
}
}
}
}
}
}
@Composable
fun PostCard(
post: Post
) {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Column {
Text(
text = "No.${post.id}",
modifier = Modifier.padding(8.dp),
)
Text(
text = "タイトル\n${post.title}",
modifier = Modifier.padding(8.dp),
)
Text(
text = "内容\n${post.body}",
modifier = Modifier.padding(8.dp),
)
}
}
}
viewmodel.items
は Flow<PagingData<Post>>
なので、これをcollectAsLazyPagingItems()
で UI 側で利用できる形に変換し、LazyColumn
で縦に並べています。
動作例
上記のコードでの動作例は以下の通りです。
数十件のアイテムの情報を取得しているものの、データ取得により発生する待ち時間がほとんどないことが分かります。
最後に
本記事ではPaging3を用いたページングの実装方法についてまとめました。
Android開発初学者ですので、間違い等ありましたら指摘していただけると助かります。