Paging3で写真ギャラリーをサクッと表示する
Paging 3
言わずと知れた、Jetpackのページングライブラリがかなり使いやすくなっていました。
時間が経ち、余計なものもたくさん入ってしまっていますが、関連リポジトリはこちら↓↓↓
依存関係
def paging_version = "3.0.0-alpha02"
implementation "androidx.paging:paging-runtime:$paging_version"
Repository(PagingSource)
PagingSourceを継承し、独自のDataSourceを作成します。引数はアプリに合わせて設定してください。今回は日付(timestamp)をキーにしたいのでLong
です。
class GalleryPagingSource(private val context: Context) : PagingSource<Long, Media>()
MediaStoreの設定は以下のようによしなに...以下は最低限のUri取得用のIDとページングのキーにする日付をprojectionに指定しています。
const val PHOTO_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
const val PHOTO_ID = MediaStore.Images.ImageColumns._ID
const val PHOTO_DATE_ADDED = MediaStore.Images.ImageColumns.DATE_ADDED
val PHOTO_PROJECTION = arrayOf(
PHOTO_ID,
PHOTO_DATE_ADDED
)
const val PHOTO_SORT_ORDER = "$PHOTO_DATE_ADDED DESC, $PHOTO_ID ASC"
const val PHOTO_FROM_DATE_ADDED = "$PHOTO_DATE_ADDED <=?"
PagingSource.load
ここがポイントです。
以前だとここはloadInitial
, loadBefore
, loadAfter
3つのコールバックに分かれていましたが、1つに集約されています。
LoadResult.Page
に次のページのキーを渡してあげます。(初回はnullなので現在時刻)
pagedKeyDate == lastItemDate
の部分は、終了時にキーが繰り返す事への対応です。キーの繰り返しを許可するためには、以下 keyReuseSupported
を指定すればいいのですが、当然キーがループするので暫定でこの処理を入れています。もっといい対応あるかもなのであれば知りたい...
override val keyReuseSupported :Boolean = true
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, Photo> {
return try {
val pagedKeyDate = params.key ?: System.currentTimeMillis()
val result = fetch(pagedKeyDate)
val lastItemDate = result.last().detail.date
if (pagedKeyDate == lastItemDate) {
return LoadResult.Error(/**例外**/)
}
LoadResult.Page(
data = result,
prevKey = null,
nextKey = lastItemDate
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
データの取得はContentProviderからよしなに。上記で、nextkey
を最後の写真の日付にしているのでここからPageを繰り返していくだけです。
suspend fun fetch(pageKey: Long) = withContext(Dispatchers.IO) {
val photosList = mutableListOf<Photo>()
context.contentResolver.query(
PHOTO_URI,
PHOTO_PROJECTION,
PHOTO_FROM_DATE_ADDED,
arrayOf(pageKey.toString()),
PHOTO_SORT_ORDER
)?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(PHOTO_ID)
val dateAddedIndex = cursor.getColumnIndexOrThrow(PHOTO_DATE_ADDED)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
val dateAdded = cursor.getLong(dateAddedIndex)
val uri = ContentUris.withAppendedId(PHOTO_URI, id)
photosList.add(
Photo(uri,dateAdded)
)
}
cursor.close()
}
photosList
}
ViewModel
ViewModelでは取ったデータをFlow
として受け取ります。
LiveData
やObservable
にも変換できますし、cachedInでコルーチンスコープやLifecycle
の間でキャッシュしてくれるのでかなり柔軟性もあります。
class GalleryViewModel(app: Application) : AndroidViewModel(app) {
companion object {
const val PAGE_SIZE = 20
}
val galleryDataFlow = Pager(
PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE)
) {
GalleryPagingSource(getApplication())
}.flow.cachedIn(viewModelScope)
}
UI(Adapter, Activity)
collectLatestは、FlowのAPIで最新以外をキャンセルしながら受け取ってくれる感じのやつです。PagingDataAdapter
を継承したAdapter
にsubmitしてあげましょう。ここは今までのPagingと同じくReccyclerView.Adapter
のシンプルな拡張になっていて、DiffUtill
を渡してあげるだけです。
viewModel.galleryDataFlow.collectLatest {
galleryAdapter.submitData(it)
}
まとめ
とても簡単になっていました。
Discussion