PagingSourceクラスの自作が必要(1)
本連載は、Android Jetpackを紹介しています。今回は、大量のデータを効率よく扱えるページングライブラリを利用して、ネット上にあるリストデータをページングする方法を解説します。
なお、今回のサンプルデータは、GitHubから参照できます。ただし、サンプル内のコードでは実際にネットにアクセスしてデータを取得するコードは記載せず、ダミーでデータを生成するコードとなっている点はあらかじめご了承ください。
PagingSourceクラスはPagingSourceを継承
前回紹介したのは、大量のリストデータをページに分割し、その制御を行うPagingSourceオブジェクトを、Roomによって自動生成してもらうといった内容でした。大量のリストデータがデータベース内に格納されているならば、この方法が最適といえます。
一方、データがネット上にある場合、当然ですがRoomによる自動生成は使えず、PagingSourceクラスを自作し、その中でネットからデータを取得するコードを記述する必要があります。その骨格は、例えば、リスト1のコードになります。このコードは、前回の図1同様に、電話番号とその主キーを表示するアプリを想定しています。そのため、クラス名をPhonePagingSourceとしています。
class PhonePagingSource : PagingSource<Int, Phone>() { // (1) override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Phone> { // (2) : } override fun getRefreshKey(state: PagingState<Int, Phone>): Int? { // (3) : } }
PagingSourceクラス作成のポイントは、(1)にあるように、PagingSourceクラスを継承することです。その際、ジェネリクスとして、RoomのDAOインターフェースのリストデータを取得するメソッドの戻り値のPagingSourceに定義したジェネリクスと同様のものを指定します。すなわち、ジェネリクスのひとつめは、各ページを区別するためのデータのデータ型を、ふたつめはリストデータの各アイテムを表すクラスを指定します。
このPagingSourceクラスは抽象クラスとなっているので、抽象メソッドをオーバーライドしておく必要があります。それが、リスト1の(2)のload()と(3)のgetRefreshKey()です。
load()メソッドの実装方法
オーバーライドが必要な2個の抽象メソッドのうち、load()メソッドはページングの肝となるメソッドです。その引数であるLoadParamsには、表1の3個のプロパティが含まれています。なお、表中のデータ型Keyはジェネリクスで指定したデータ型です。
プロパティ名 | データ型 | 内容 |
key | Key? | 次に読み込むべきリストのキーとなる値。初回時はnull |
loadSize | Int | 1ページ分のアイテム数 |
placeholdersEnabled | Boolean | PagingConfig.enablePlaceholdersで設定した値 |
これらの値を元に、ネットなどから必要なリストデータを取得し、そのリストデータを元にページデータを生成してリターンします。その戻り値としては、無事リストデータが用意できた場合はLoadResult.Pageを、失敗した場合はLoadResult.Errorとします。
これは、例えば、リスト2のコードとなります。なお、(2)のfetchPhoneListSize()は、ネット上にあるリストデータの総件数を取得するsuspend関数です。同様に、fetchPhoneList()は、ネット上からリストデータを取得するsuspend関数です。引数としては切り出すリストの開始idと終了idを受け取るようにしています。
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Phone> { var returnVal: LoadResult<Int, Phone> // (1) try { val phoneListSize = fetchPhoneListSize() // (2) val startKey = params.key ?: 1 // (3) var endKey = startKey + params.loadSize - 1 // (4) if(endKey > phoneListSize) { // (4) endKey = phoneListSize } val fetchedPhoneList = fetchPhoneList(startKey, endKey) // (5) val prevKey = if(startKey - params.loadSize <= 0) { // (6) null } else { startKey - params.loadSize } val nextKey = if(endKey + 1 >= phoneListSize) { // (7) null } else { endKey + 1 } returnVal = LoadResult.Page(fetchedPhoneList, prevKey, nextKey) // (8) } catch(ex: Exception) { returnVal = LoadResult.Error(ex) // (9) } return returnVal }
リスト2の大きな流れとしては、(1)で戻り値変数としてLoadResult<Int, Phone>型のreturnValを用意し、その後、処理全体をtryブロックで囲み、例外処理を行っています。ネット上からデータを取得するような処理では必須のコードパターンといえます。
その例外処理のcatchブロックが処理失敗と言えるので、(9)でLoadResult.Errorインスタンスを生成し、戻り値のreturnValとしています。その際、引数として例外インスタンスを渡しています。
一方、tryブロックの最終行である(8)が処理が成功した場合ですので、そこでLoadResult.Pageインスタンスを生成しています。その際、引数として表2の3個の値を渡す必要があり、これらを事前にtryブロック内で用意しています。なお、表中のデータ型KeyとValueはジェネリクスで指定したデータ型です。
引数名 | データ型 | 内容 | |
1 | data | List<Value> | 現在のページのリストデータ |
2 | prevKey | Key? | 前ページの開始キーの値。前ページがない場合はnull |
3 | nextKey | Key? | 次ページの開始キーの値。次ページがない場合はnull |
まず、(3)~(5)でネット上からリストデータを取得しています。(3)が開始キー、すなわち、ネット上にあるリストデータから切り出すリストデータの開始idを、変数startKeyとして生成しています。その際、引数paramsのkeyプロパティの値を利用し、表1の通り、この値がnullの場合は1としています。
(4)は、終了キーをendKeyとして生成しています。その際、表1の引数paramsのloadSizeプロパティを利用しています。さらに(2)で取得した全リストサイズを元にendKeyがリストサイズを超えている場合は、endKeyをリストサイズとしています。
このようにして生成したstartKeyとendKeyを元にネット上からリストデータを取得しているのが(5)です。そして、これがそのまま(8)でLoadResult.Pageインスタンスを生成する際の第1引数となっています。
次に、第2引数のprevKeyと第3引数のnextKeyを生成しておく必要があります。それが(6)と(7)です。これらのコードは、単純にstartKeyやendKeyとparams.loadSizeを元に算出する処理に過ぎません。ただし、表2の通り、prevKeyの場合は、算出した結果が0以下の場合は前ページがないのでnullとする必要があります。同様に、nextKeyは全リストサイズより大きければnullとする必要があります。