はじめに
Paging3はAndroidJetpackの一部で、ページングや無限スクロールを簡単に実装できるライブラリです。
キャッシュやリモートとローカルのデータ取得に対応しています。
基本的には公式のドキュメントが詳しいです。
公式のCodelabもおすすめです。
基本コースの内容は高度コースにほぼ含まれているので高度コースができれば基本コースはする必要はなかったと思います。
高度コースにはDBキャッシュのためのRemoteMediatorについての内容が含まれてます。
一応サンプルのプロジェクトをgithubにあげておきました
基本的な実装
基本的には次のような流れです。
- 普通にRecyclerViewを実装
- PagingSourceを追加
- RepositoryにPagingSourceを生成する関数を追加
- ViewModelでPagerを使ってPagingSourceからPagingDataをキャッシュする
- Adapterの親クラスをListAdapterからPagingDataAdapterに変更する
- ViewModelのPagingDataをFragmentなどで購読し、Adapterにsubmitする
PagingSourceの記述とPagerのConfigに気を付ければあまり問題ないと思います。
Placeholderの表示
CodelabではPlaceholderについては触れていませんがどうやって表示するかというと、
まず、enablePlaceholdersをtrue(デフォルト)にします。
次にListPagingSourceのLoadResult.PageにitemsBeforeとitemsAfterを指定してあげるとPlaceholderが表示されます。
itemsBefore | offsetまたはif (prevKey == null) 0 else limit |
itemsAfter | if(nextKey == null) 0 else limit |
と指定するのがシンプルな解決法です。
プレースホルダを表示する必要がない場合は0を指定します。
itemsAfterについて、最後のロード時のアイテムがlimitより少ない数だった場合、
Placeholderを取り除くようなアニメーションが入るため
Apiのレスポンスからアイテムのトータルなどが取得できる場合は利用したほうが良いです。
Placeholderの表示内容については
ViewHolderでgetItemがnullだった場合の表示を追加してあげればよいです。
データのリフレッシュ
PagingDataはViewModelでキャッシュしているので、
画面を再表示したときに更新した場合などは明示的にrefreshする必要があります。
PagingDataをrefreshする方法は複数ありますが、
UIレイヤーではPagingAdapterのrefreshを使うようです。
ただ、このrefreshを呼び出すタイミングが重要です。
純粋にonViewCreatedやsubmitDataの直後にrefreshした場合は、
PagingAdapterとPagingDataが連携していない場合があるのでrefreshされません。
いくつかのタイミングを試したのですが、onDestroyViewでrefreshするとうまくいきました。
onDestroyViewでrefreshする場合は
bindingかadapterを保持することになると思うのでメモリーリークに気を付けてください。
また、Placeholderの設定をしていない場合、
データのrefreshの際にスクロール位置がPagingの途中であった場合、スクロール位置が変更されます。
10個ずつのリストだった場合こんな感じです。
Refresh前 | 8 | 9 | 10 | 11 | 12 |
Placeholder 無し | 6 | 7 | 8 | 9 | 10 |
Placeholder 有り | 8 | 9 | 10 | null | null |
ProgressBarの表示
高度コースのCodelabにもありますが、
PagingAdapterのwithLoadStateHeaderAndFooterを使用すればプログレスバーを簡単に表示できます。
ただSwipeRefreshLayoutを使用した場合など少しカスタマイズしたい場合は工夫が必要です。
withLoadStateHeaderAndFooterの中身を見ると、
addLoadStateListenerとConcatAdapterを使用しているだけのようです。
なのでカスタムしたい場合はこれらを自前で書けばよいです。
PagingAdapterはLoadingStateをflowとして公開しているので、addLoadStateListenerの代わりに使えます。
一例を置いておきます。
pagingAdapter.loadStateFlow.onEach {when (it.refresh) {is LoadState.Loading -> onRefreshLoading(binding, pagingAdapter)is LoadState.NotLoading -> onRefreshSuccess(binding, pagingAdapter)is LoadState.Error -> onRefreshError(binding, pagingAdapter)}if(it.prepend is LoadState.Error || it.append is LoadState.Error) {headerAdapter.loadState = LoadState.NotLoading(false)footerAdapter.loadState = LoadState.NotLoading(false)return@onEach showErrorToast(binding)}headerAdapter.loadState = it.prependfooterAdapter.loadState = it.append}.launchIn(viewLifecycleOwner.lifecycleScope)
これはよくわからないのですが、HeaderAdapterを追加した状態でrefreshすると、Placeholderを追加していてもアイテム一つ分くらい上にスクロールされます。