Kotlin版RemoteMediatorのコーディングとその利用方法
準備ができたところで、早速RemoteMediatorを作成していきましょう。
RemoteMediatorのコードパターン
RemoteMediatorのKotlin版のコード例は、リスト2のようになります。
@OptIn(ExperimentalPagingApi::class) // (1)
class PhoneRemoteMediator(
private val _db: AppDatabase // (2)
) : RemoteMediator<Int, Phone>() { // (3)
override suspend fun load(loadType: LoadType, state: PagingState<Int, Phone>): MediatorResult { // (4)
//戻り値変数を用意。
var returnVal: MediatorResult // (5)
val phoneDAO = _db.createPhoneDAO()
try {
//loadTypeで処理を分岐。
when(loadType) { // (6)
//データクリアの場合。
LoadType.REFRESH -> { // (7)
phoneDAO.deleteAll()
returnVal = MediatorResult.Success(false)
}
//データの末尾を検知した場合。
LoadType.PREPEND -> { // (8)
returnVal = MediatorResult.Success(true)
}
//データの先頭を検知した場合。
LoadType.APPEND -> { // (9)
//データベース内のidの最大ちからstartKeyとendKeyを決定。
val lastId = phoneDAO.findLastId() // (10)
val startKey = lastId + 1
val endKey = startKey + state.config.pageSize - 1 // (11)
//ネット上から追加のリストを取得しデータベースに格納。
val fetchedPhoneList = fetchPhoneList(startKey, endKey) // (12)
phoneDAO.insertPhoneList(fetchedPhoneList) // (13)
//ネット上からリストサイズを取得してリスに続きがあるかどうかを判定。
val listSize = fetchPhoneListSize() // (14)
if(endKey < listSize) {
returnVal = MediatorResult.Success(false) // (15)
}
else {
returnVal = MediatorResult.Success(true) // (16)
}
}
}
}
catch(ex: Exception) {
returnVal = MediatorResult.Error(ex) // (17)
}
return returnVal // (18)
}
}
RemoteMediatorを作成する場合の大きなルールをまとめると、次のようになります。
RemoteMediatorクラスを継承したクラスとする
リスト2の(3)が該当します。その際、PagingSourceのジェネリクスとして記述していたもの、すなわち、ジェネリクスのひとつめは、各ページを区別するためのデータのデータ型を、ふたつめはリストデータの各アイテムを表すクラスを指定します。
クラスに@OptInアノテーションを付与する
リスト2の(1)が該当します。この記述がないとコンパイルエラーとなります。これは、実験的なAPIには付与しなければならないアノテーションです。この例であれば、Pagingライブラリで導入されている新しい機能を試験的に利用できるようになります。
AppDatabaseをコンストラクタの引数として定義する
リスト2の(2)が該当します。先述のとおり、RemoteMediatorではRoomのDAOクラスを利用してデータベースへのアクセスが必要です。そのためにはAppDatabaseをあらかじめ取得しておく必要があります。
load()メソッドをオーバーライドする
リスト2の(4)が該当します。RemoteMediatorクラスは抽象クラスであり、抽象メソッドであるload()をオーバーライドする必要があります。そして、このメソッド内にネットからデータを取得し、データベースにキャッシュするコードを記述します。
load()メソッドの構文
そのload()メソッドの構文を分解すると、表1のようになります。
| 第1引数 | loadType: LoadType | データベース内のデータに対してどのような処理を行えばよいのかを表す値 |
| 第2引数 | state: PagingState |
ページングの現在の状態に関するデータを格納したオブジェクト |
| 戻り値 | MediatorResult | リストデータの取得結果を表すオブジェクト |
load()メソッドの戻り値
表1の内容のうち、まず、戻り値から説明することにします。これは表1およびリスト2の(4)の通り、MediatorResultです。
これは、表2の3種のインスタンスを生成してリターンします。
| インスタンス生成コード | インスタンスを生成するパターン |
|---|---|
| MediatorResult.Success(false) | データ取得に成功し、次ページデータが存在する場合 |
| MediatorResult.Success(true) | データ取得に成功し、次ページデータが存在しない、つまりリストデータの最後に到達した場合 |
|
MediatorResult.Error(例外インスタンス) |
データ取得が失敗した場合 |
Success()によるインスタンス生成時の引数のbool値に関しては、その引数名がendOfPaginationReachedと定義されていることから、リストデータの最後に到達しているかを表す引数です。そのことを踏まえると、trueなのかfalseなのかがわかりやすいと思います。
リスト2では(5)で戻り値としてリターンするMediatorResult型の変数returnValを用意し、最終的に(18)でリターンするコードパターンとしています。その途中では、try-catch構文を利用し、tryブロック中にデータ取得、およびデータベースへの格納コードを記述し、その結果に応じて、Success(false)かSuccess(true)をreturnValに格納しています。
一方、(17)のcatchブロック内でreturnValにError(ex)を格納しています。このコードパターンを採用したのは、前回同様、ネット上からデータを取得する処理では必須のコードパターンだからです。
データ処理コードは引数loadTypeで分岐
では、tryブロックの中、すなわちデータ処理コードを見ていくことにしましょう。
その際、load()メソッドの第1引数loadTypeの値で分岐を行います。このloadTypeはEnumのLoadTypeであり、その値は表3の3個のどれかです。
| 値 | 内容 |
|---|---|
| REFRESH | PagingSourceが破棄されるなど、キャッシュをクリアしてデータを再取得する必要がある場合 |
| APPEND | データベース内のデータの末尾を検知し、その続きのデータを取得する必要がある場合 |
| PREPEND | データベース内のデータの先頭を検知し、その続きのデータを取得する必要がある場合 |
値が3個なので、リスト2では(6)でloadTypeの値を元にしたwhen構文を利用して分岐処理を行っています。
そのうち、(7)がREFRESHの場合です。この場合は、表3の通り、データのクリアが必要なので、リスト1でPhoneDAOに用意した全削除メソッドのdeleteAll()を実行し、データを削除します。その後、戻り値をSuccess(false)としています。falseとしているのは、データベース内のデータが空なので、最初からのデータが必要だからです。
(8)は、PREPENDの場合です。これは表3の通り、データベース内のデータの先頭を検出した場合です。先頭よりさらにその前にデータを追加する必要がある場合に利用します。例えば、ネット上のリストデータが1000件あるとして、初期リスト画面が1から始まるのではなく、リストの末尾の980~1000件や、途中の500~520件を表示する場合、リストをさかのぼるスクロールが発生します。この場合は、PREPENDが渡されるので、それに合わせて980以前、あるいは500以前のデータを取得してデータベースに格納する処理を行います。ただし、今回はリストを最初から表示させる仕様のアプリなので、(8)ではSuccess(true)を戻り値とし、前のデータが存在しないとしています。
そして(9)が、APPENDの場合であり、今回のメインの処理です。その処理内容は(10)で、リスト1でPhoneDAOに追加したfindLastId()メソッドを使って、データベース内のidの最大値を取得し、その値に1足して開始キーとしています。
次に、終了キーがわかれば、ネットからリストデータを取得できます。その際、1ページ分のアイテム数データが必要で、その値はload()の第2引数PagingStateから取得できます。このオブジェクトのconfigプロパティは、PagingConfigオブジェクトです。そのpageSizeプロパティに1ページ分のアイテム数が格納されているので、(11)ではこれを基に取得するリストデータの終了キーを計算しています。
これらの開始キーと終了キーを元に、ネットからリストデータを取得しているのが(12)です。このコードパターンは、前回のリスト2のPagingSourceクラスのload()メソッド内のコードパターンと同じです。
異なるのは(13)でネットから取得したリストデータをデータベースに格納している点です。その際、リスト1でPhoneDAOに追記したinsertPhoneList()を利用します。
最終的に、データの取得と格納処理が終了したら、戻り値を確定する必要があります。そこで(14)でネット上のリストのサイズを取得し、終了キーとそのリストサイズを比較し、リストサイズの方が大きい場合、続きのリストデータが存在するとして(15)のように戻り値Success(false)とします。
逆に、リストサイズの方が小さい場合は続きのデータがないとして(16)のように戻り値をSuccess(true)とします。
リポジトリでのRemoteMediatorの利用
これで、RemoteMediatorクラスが完成しました。今度は、リポジトリでのPagerの生成時に、RemoteMediatorを利用するコードパターンを紹介します。
これは、リスト3のようになります。
@OptIn(ExperimentalPagingApi::class) // (1)
fun getAllPhoneListPager(): Pager<Int, Phone> {
val phoneDAO = _db.createPhoneDAO()
val pagingConfig = PagingConfig(ITEMS_PER_PAGE)
val phoneListPager = Pager(pagingConfig, null, PhoneRemoteMediator(_db)) { phoneDAO.findAll() } // (2)
return phoneListPager
}
リスト3のポイントは、Pagerインスタンスを生成する(2)のコードです。
Pagerインスタンスを生成する際の第1引数であるPagingConfigインスタンス、ラムダ式内でPagingSourceオブジェクトをリターンするコード記述は、これまで同様です。リスト3ではRoomを利用するために、ラムダ式内はPhoneDAOのfindAll()メソッドの実行コードを記述しています。
変わるのは第2引数と第3引数です。その第3引数として、自作したRemoteMediatorのインスタンスを渡します。なお、第2引数は、initialKeyという引数であり、初期キー値を渡します。ただし、これは省略可能であり、その場合はリスト3の(2)のようにnullを渡します。
さらに、リスト2の(1)でRemoteMediatorクラスに付記したアノテーションがリポジトリメソッドにも必要であり、それが(1)です。こちらも忘れるとコンパルエラーとなるので注意してください。
