ページングライブラリの登場オブジェクトのコーディングパターン
ページングライブラリを概観し、その利用の準備ができたところで、実際のコーディング方法を紹介していくことにしましょう。
本連載では、これまで、まずはJavaコードを紹介し、その後Kotlinコードを紹介するという流れでした。一方、ページングライブラリは、図2および表1にFlowオブジェクトが登場することからもわかるように、Kotlinでのコーディングが基本となっています。そのため、これまでとは逆に、まずKotlinコードを紹介し、その後Javaコードを紹介することにします。
RoomによるPagingSourceの生成
まずはPagingSourceオブジェクトです。このオブジェクトはリポジトリで用意することになっています。実際の生成は、Roomを利用する場合、非常に簡単です。リスト2のように、DAOインターフェースのリストデータを取得するメソッドの戻り値の型として、PagingSourceとするだけです。
@Dao interface PhoneDAO { @Query("SELECT * FROM phones ORDER BY id") fun findAll(): PagingSource<Int, Phone> }
その際、ジェネリクスとして2個のデータ型を指定します。ひとつめは、各ページを区別するためのデータのデータ型を指定します。例えば、1ページに50件分のデータを取得する場合、つまり、スクロールしていくとある段階で次の50件を取得するといった場合、どのようなデータをもとに次の50件とするかを判定するためのデータのデータ型です。ここで題材としている電話番号リストの場合は、そのidの値、すなわち、整数値を利用すれば次の50件を取得できます。そこで、リスト2ではIntとしています。
ジェネリクスのふたつめは、リストデータの各アイテムを表すクラスです。Roomを利用する場合は、エンティティクラスそのものですので、リスト2で指定しているPhoneエンティティは、リスト3のコードとなります。
@Entity(tableName = "phones") data class Phone( @PrimaryKey(autoGenerate = true) val id: Long, val phoneNo: String )
このようにして用意したPhoneDAOのfindAll()を、リポジトリでは、リスト4のように実行して、その戻り値をリターンすることで、PagingSourceオブジェクトの用意ができたことになります。
class PhoneRepository(application: Application) { : fun getAllPhoneListPagingSource(): PagingSource<Int, Phone> { val phoneDAO = _db.createPhoneDAO() return phoneDAO.findAll() } }
ViewModelでのFlowの抽出
次に、図2の通り、ViewModelにおいて、リポジトリからPagingSourceオブジェクトを取得した上で、Flowオブジェクトを抽出します。これはリスト5のコードとなります。
private const val ITEMS_PER_PAGE = 50 // (1) class MainViewModel(application: Application) : AndroidViewModel(application) { val phoneListFlow: Flow<PagingData<Phone>> // (2) private val _phoneRepository: PhoneRepository init { _phoneRepository = PhoneRepository(application) val phoneListPageSource = _phoneRepository.getAllPhoneListPagingSource() // (3) val pagingConfig = PagingConfig(ITEMS_PER_PAGE) // (4) val phoneListPager = Pager(pagingConfig) {phoneListPageSource} // (5) phoneListFlow = phoneListPager.flow.cachedIn(viewModelScope) // (6) } }
大まかな流れとしては、図2の通り、リポジトリから取得したPagingSourceオブジェクトとPagingConfigオブジェクトをもとに、Pagerオブジェクトを生成し、そのPagerオブジェクトからFlowオブジェクトを抽出する処理となります。
まず、リスト5の(3)がリポジトリからPagingSourceオブジェクトを取得している処理で、phoneListPageSourceとしています。そして、Pagerオブジェクトを生成する前に、PagingConfigオブジェクトを生成しています。それが(4)のコードです。これは表1の通り、ページ分割に関する設定情報を保持するオブジェクトであり、少なくともインスタンス生成時に、すなわち、コンストラクタの引数として1ページあたりの取得件数を指定する必要があります。リスト5では(1)の定数を指定し、50件としています。
これでPagerオブジェクトを生成する準備ができたので、インスタンスを生成します。その際、第1引数としてPagingConfigオブジェクトを、第2引数としてPagingSourceオブジェクトをリターンするラムダ式を記述します。それが(5)です。Pager(pagingConfig)の()内が第1引数であるPagingConfigオブジェクトのpagingConfigであり、{phoneListPageSource}がPagingSourceオブジェクトであるphoneListPageSourceをリターンするラムダ式です。
このようにして生成したPagerオブジェクトからFlowを抽出します。それが(6)のコードです。Pagerオブジェクトは、そのプロパティであるflowがそのままFlowオブジェクトを表すので、それを取得します。ただし、そのFlowオブジェクトをそのままViewModelのプロパティとしてアクティビティなどで利用するのではなく、cachedIn()メソッド(より正確には拡張関数)を実行した戻り値を利用します。このcachedIn()メソッドは、そのメソッド名の通り、PagingSourceによって生成される各ページデータをキャッシュしながら、アクティビティなどがやり取りできる形で用意してくれる働きがあります。その際、コルーチンスコープを引数として渡す必要があり、そのスコープに連動してページデータのロードが行われるようになります。
cachedIn()メソッドの戻り値もまたFlowオブジェクトなので、(6)ではそれを(2)のプロパティとしています。その(2)のプロパティを見ると、Flowの構成要素がPagingDataとなっています。このPagingDataはまさにページデータを表すオブジェクトであり、ページデータの元となるリストのアイテム、つまりエンティティのデータ型をジェネリクスとして指定する必要があります。(2)ではPhoneとしています。
アダプタとアイテム比較オブジェクトの用意
リスト5のコードで、ページデータのFlowオブジェクトが、ViewModelのプロパティとして用意され、アクティビティやフラグメントからはいつでも利用できるようになりました。そのアクティビティなどでは、FlowデータとRecyclerViewとを連携させる必要があります。その際、本来ならば第5回で紹介した方法で、オブザーバやコレクタから変更処理を通知できるアダプタクラスを作成する必要があります。しかし、ページングライブラリではFlow<PagingData>と連携するための専用のページング用アダプタクラスが用意されており、それを利用することになります。それが、表1のPagingDataAdapterであり、リスト6のコードとなります。
private inner class PhoneListAdapter(diffCallback: DiffUtil.ItemCallback<Phone>) : PagingDataAdapter<Phone, PhoneViewHolder>(diffCallback) { // (1) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhoneViewHolder { // (2) : return PhoneViewHolder(row) } override fun onBindViewHolder(holder: PhoneViewHolder, position: Int) { // (3) val item: Phone? = getItem(position) // (4) holder.bind(item) } }
ページング用アダプタクラスは、リスト6の(1)のように、まさにPagingDataAdapterクラスを継承して作ります。その際、ジェネリクスのひとつめにエンティティを、ふたつめにビューホルダクラスを指定します。そして(2)のようにonCreateViewHolder()メソッドを、(3)のようにonBindViewHolder()メソッドを実装します。これらのメソッドの働きは、通常のアダプタクラスと同様です。
なお、PagingDataAdapterクラスには、ポジションから各アイテムデータを取り出すメソッドとしてgetItem()があるので、(4)のようにonBindViewHolder()ではこのメソッドによって取り出したアイテムデータ、つまりエンティティを利用します。
ただし、PagingDataAdapterクラスは、コンストラクタの引数としてDiffUtil.ItemCallbackオブジェクトを必要とします。そのため、継承した子クラスであるPhoneListAdapterでもコンストラクタの引数として定義しておき、それをそのまま親クラスに渡すコードとします。
そして、アクティビティなどでPhoneListAdapterのインスタンス生成時に、DiffUtil.ItemCallbackインスタンスを渡すようにします。DiffUtil.ItemCallbackクラスは、リスト7のような内容となります。
private class PhoneComparator : DiffUtil.ItemCallback<Phone>() { // (1) override fun areItemsTheSame(oldItem: Phone, newItem: Phone): Boolean { // (2) return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Phone, newItem: Phone): Boolean { // (3) return oldItem == newItem } }
DiffUtil.ItemCallbackクラスそのものは抽象クラスなので、子クラスを作成します。リスト7では、(1)のようにPhoneComparatorとしています。その際、ジェネリクスとしてエンティティを指定します。そして、子クラスではareItemsTheSame()メソッドとareContentsTheSame()メソッドを実装する必要があります。
このクラスは表1の通り、2種のリスト内の各アイテムが同じものかどうかを判定するためのオブジェクト、いわばアイテム比較オブジェクトとなっています。アダプタはこのオブジェクトのareItemsTheSame()メソッドとareContentsTheSame()メソッドを利用して、各アイテムが同じかどうかを判定し、リストの再利用を行うかどうかを判断しています。そのため、(2)のareItemsTheSame()メソッドでは、2種のアイテムの対象が同じかどうかを判定します。これは、通常主キーの値が同じかどうかで判定します。一方、(3)のareContentsTheSame()メソッドは、アイテムに含まれているデータそのものが全て同じかどうかを判定します。
アクティビティなどでのアダプタとFlowの連携
さあ、最後の仕上げです。アダプタオブジェクトをRecyclerViewにセットし、Flowとアダプタを連携させましょう。これは、リスト8のコードとなります。
override fun onCreate(savedInstanceState: Bundle?) { : val adapter = PhoneListAdapter(PhoneComparator()) // (1) _activityMainBinding.rvPhoneList.adapter = adapter // (2) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { _mainViewModel.phoneListFlow.collectLatest { // (3) adapter.submitData(it) // (4) } } } }
リスト8の(1)は、リスト6のPhoneListAdapterインスタンスを生成しているコードであり、その際、リスト7のPhoneComparatorインスタンスを渡しています。これは、前項の解説のとおりです。このようにして生成したアダプタをRecyclerViewにセットしているのが、(2)のコードです。
このアダプタオブジェクト、すなわちPagingDataAdapterオブジェクトには、Flow中のデータ変更に合わせてアダプタ内のデータを自動的に変更するメソッドがあらかじめ備わっています。それが(4)のsubmitData()メソッドです。本来ならば、第5回で紹介したように、このようなメソッドはアダプタクラスに自作する必要がありますが、PagingDataAdapterでは不要です。
あとは、Flowオブジェクトのコレクタ内でこのsubmitData()を実行し、引数として新しいデータを渡すだけです。ただし、ページングライブラリでは、(3)のように、Flowのcollect()メソッドの代わりに、collectLatest()メソッドを実行することが推奨されています。collect()とcollectLatest()との違いは、そのメソッド名の通り、Flow中のデータ更新が行われた場合、もしコレクタ内の処理が途中の場合は、それを破棄して、最新のもののみを採用することになっている点です。そこで、このcollectLatest()を実行し、ラムダ式内でsubmitData()を実行しています。その際、引数として渡しているitがcollectLatest()のラムダ式の引数、つまり、新しいデータを表します。