Paging3で簡単にページング処理
業務でPaging3を使ったらとても簡単にページング処理を書けたので紹介します。
公式(https://developer.android.com/topic/libraries/architecture/paging/v3-overview)
Retrofitの通信(Service)
@GET("/users/{user}/sample")
suspend fun samplePaging(params: String, page: Int): Response<Model>
ここは特に特殊な事をやっていないので、普段通りに書いてください
Repositoryの代わりとなるクラスを用意するので、Repositoryは不要です。
PagingSource(Repositoryの代わり)
class SamplePagingSource(private val apiParams: String) : PagingSource<Int, String>(), KoinComponent {
private val api: GithubService by inject()
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
val page = params.key ?: 1
val response = api.samplePaging(apiParams, page).datas
return if (response != null) {
LoadResult.Page(
data = response,
nextKey = page + 1, // 本当はAPIからの戻り値を元に次のページの有無確認など必要
prevKey = page - 1
)
} else {
LoadResult.Page(
data = emptyList(),
prevKey = null,
nextKey = null
)
}
}
}
コンストラクタの引数でAPIのGetを呼ぶのに必要なパラメータを渡します。
private val api: GithubService by inject()
の部分でKoinを使って通信(Service)
クラスをinjectしています。
親クラスのPagingSourceは<ページングに使うキーの型, データの型>
を指定します。
今回はページ情報を数字で表しているのでInt、データの型はStringとしたので、
PagingSource<Int, String>
を指定しました。
APIからのデータ取得完了時はLoadResult.Page
を返却します。
LoadResult.Page.data
にはPagingSourceで指定した型のListを渡します。
LoadResult.Page.prevKey
には前のページのキー
LoadResult.Page.nextKey
には次のページのキー
をそれぞれ指定します。
※(前/次)のページが無い場合はnullを渡します。
また、エラーとしたい場合にはLoadResult.Error(e)
を返却できます。
ViewModel
val samplePagingFlow: Flow<PagingData<String>> = Pager(
PagingConfig(pageSize = 10, initialLoadSize = 10)
) {
SamplePagingSource("params")
}.flow.cachedIn(viewModelScope)
ここでPagingSourceをFlowとして読めるようにします。
Adapter
class SampleAdapter : PagingDataAdapter<String, SampleAdapter.ViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.title = getItem(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ViewSampleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
class ViewHolder(val binding: ViewSampleBinding) : RecyclerView.ViewHolder(binding.root)
companion object {
val diffCallback = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
// 本当はユニークなidなどで比較する
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}
}
通常のReyclerView.Adapterとの違いは
- DiffUtil.ItemCallbackを渡す必要がある
- アイテムを取得するgetItemメソッドが生えている
くらいになります。
Fragment
viewLifecycleOwner.lifecycleScope.launch {
viewModel.samplePagingFlow.collectLatest { pagingData ->
sampleAdapter.submitData(pagingData)
}
}
ViewModelでFlow化したPagingSourceに変更があった時にAdapter#submitDataします。
※いつも通りRecyclerView.Adapter = sampleAdapterやLayoutManagerの指定はしてください
これだけでPaging処理が行われます。
おまけ(ロード中にプログレスを表示したい場合)
Layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
プログレスバーのみを設置したレイアウトを用意します。
LoadStateAdapter
class SampleLoadStateAdapter : LoadStateAdapter<ViewHolder>() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
val progress = holder.itemView.findViewById<ProgressBar>(R.id.progress)
progress.visibility = if (loadState is LoadState.Loading) View.VISIBLE else View.GONE
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState) =
LoadStateViewHolder(parent)
class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.view_sample_load_state, parent, false)
)
}
LoadStateAdapterを用意します(中身はRecyclerView.Adapterと同一)
progress.visibility = if (loadState is LoadState.Loading) View.VISIBLE else View.GONE
でプログレスの表示/非表示を切り替えています。
Fragment
recyclerView.adapter = sampleAdapter.withLoadStateFooter(LoadStateAdapter())
FragmentでwithLoadStateFooterを指定するだけでロード中かどうかを判定できるようになり、
LoadStateAdapterで指定したロード中の処理が行われます。
エラー時のリトライなども可能で、公式のサンプルアプリでも紹介されています。
※執筆時点で見ようとしたら404になってしまっていた。。。
Discussion