APIから取得したデータをDBに保存してキャッシュをする場合
RemoteMediatorの実装
最後に、RemoteMediatorを継承したAnimeMediatorを実装していきます。
RemoteMediatorは、最初とっかかりにくい部分ですので、細かく区切って説明を進めていきます。
まず、RemoteMediatorで実装が必要なメソッドは基本的に1つです。
- load:APIからデータを取得し、DBに保存する関数です。Paging 2等で使用されていたBoundaryCallbackでは、データを読み込むメソッドが2つありましたが、RemoteMediatorでは、1つのloadの中で初回読み込みか追加読み込みかといった識別はload関数の中で引数にわたってくるLoadTypeを用いて行います。
まず、Load関数の流れは以下になります。
- LoadTypeによりloadするPageを決定
- LoadTypeがREFRESHつまり初期読み込みまたはpageが0の時データベースのアニメのデータとkeyのデータを削除
- APIからデータを取得
- APIから取得したデータを元に、データをすべて読み込んだか、nextKeyはいくつかを決定
- DBに取得したデータと次回以降使用するnextKeyとsessionIdを持ったKeyを保存
- 最後にSuccessとしてMediatorResultを返却
- 上記でエラーが返却された際にErrorとしてMediatorResultを返却
上記の処理は、7でエラーを返却するため、全体をtry-catchでかこっており、1~6までの工程はバックグランドスレッドで処理を行うため、withContextを使用してIOスレッドで処理を行っております。イメージとしては以下になります。
return try { withContext(Dispatchers.IO) { 1..6 } } catch(e: Exception) { 7 }
それでは、実装を行っていきましょう。
1. LoadTypeによりloadするPageを決定
まず、LoadTypeにはREFRESH、PREPEND、APPENDがあります。
この3つが呼び出されるタイミングと今回の実装でどのように扱うかを説明します。
- REFRESH:初期読み込みの際に使用されます。今回は使用するAPIの最小のページ数である0をpageとして使用します。
- PREPEND:上方向のページング処理を行う際に呼び出されます。今回は使用しないため、これ以上読み込めるデータはないと示すendOfPaginationReachedにtrueを指定したMediatorResult.Successを返却します。
- APPEND:下方向のページング処理を行う際に呼び出されます。今回は下方向にアニメを読み込んで表示していくため、DBに保存した最新のkeyを取得し、そのnextkeyを使用します。もしkeyが存在しなかった場合は初期読み込みの際に使用する0をpageとして使用します。
上記をコードに落とし込むと以下のようになります。
val page = when (loadType) { LoadType.REFRESH -> { 0 } LoadType.PREPEND -> { return@withContext MediatorResult.Success(true) } LoadType.APPEND -> { val remoteKey = pageKeyDao.getLastKey(sessionId) remoteKey?.nextKey ?: 0 } }
2. LoadTypeがREFRESHつまり初期読み込みまたはpageが0の時データベースのアニメのデータとkeyのデータを削除
この処理は、クラスの引数にわたってくるloadTypeがREFRESHのときかpageが0の時に以前の古いデータが新しいデータの表示などに影響を及ぼさないよう、また古いデータでユーザーのメモリやストレージを圧迫しないように削除します。
実装としては、DataBaseのアニメのデータを全部削除するdeleteAllAnime()とKeyのデータをすべて削除するdeleteRemoteKey()を呼び出す。
上記をコードに落とし込むと以下のようになります。
if (loadType == LoadType.REFRESH || page == 0) { animeDao.deleteAllAnime() pageKeyDao.deleteAllPageKey() }
3. APIからデータを取得
次に、実装したAPIのsearchPagerItemに1で決定したpageと設定として持っているpageSizeを渡して呼び出し、変数に格納します。
この際、関数の引数としてわたってくるPagingStateには指定したPagingConfigの値が格納されているため、そこからpageSizeを指定してあげることで、RemoteMediator内でも固定値を持っておく必要性がなくなります。
コードは以下のようになります。
val response: Response<AnimeSearchResult> = animeService.getAnimes( token = "取得したトークン", page = page, pageSize = state.config.pageSize )
4. APIから取得したデータを元に、データをすべて読み込んだか、nextKeyはいくつかを決定
今回は、3で取得したデータを元に、データの取得に成功していて、かつデータが空だった場合はこれ以上読み込めるデータがないことを示すendOfPaginationReachedをtrueとします。
また、nextKeyはendOfPagenationReachedがtrueのときはnull、それ以外なら次の読み込みのkeyである現在のpage + 1を指定します。
上記をコードに落とし込むと以下になります。
val animeList = response.body?.animeList ?: listOf() val endOfPage = response.isSuccessful && animeList.isEmpty() val nextKey = if(endOfPage) null else page + 1
5. DBに取得したデータと次回以降使用するnextKeyとsessionIdを持ったKeyを保存
今回は、4で定義した変数をDBに保存していきます。
まず、RemoteMediatorに渡しているセッションを識別するsessionIdと次の読み込みに必要なnextKeyを指定してDBに格納します。そして、APIから取得したアニメのリストをinsertAnimesメソッドを用いて格納します。今回は、AnimeにもsessionIdを持たせているため、取得したデータに今回のsessionIdを付与して格納します。
上記をコードに落とし込むと以下になります。
pageKeyDao.insertKey( PageKey( id = 0, sessionId = sessionId, nextKey = nextKey ) ) animeDao.insertAnime(animeList.map { it.copy(sessionId = sessionId) })
6. 最後にSuccessとしてMediatorResultを返却
ここでは、1~5までの工程でエラーが起きずに読み込みに成功したことを示す、MediatorResultを返却します。
endOfPaginationReachedには、4で定義したendOfPageを渡します。今回はwithContextでバックグランドスレッドにて処理を行っているため、return@withContextでデータを返却する必要があります。
上記を実装すると以下のようになります。
return@withContext MediatorResult.Success(endOfPaginationReached = endOfPage)
7. 上記でエラーが返却された際にErrorとしてMediatorResultを返却
ここでは、上記1~6の処理でExceptionが投げられた場合に、MediatorResult.Errorで結果を返却します。
MediatorResult.ErrorにはExceptionを引数に渡す必要があるため、catchで取得したExceptionを渡します。
上記を実装すると以下のようになります。
catch (e: Exception) { MediatorResult.Error(e) }
ここまで実装したRemoteMediatorを継承したAnimeMediatorの全体像は以下のようになります。
@ExperimentalPagingApi class AnimeMediator ( private val sessionId: String, private val animeService: AnimeService, private val animeDao: AnimeDao, private val pageKeyDao: PageKeyDao, ): RemoteMediator<Int, Anime>() { override suspend fun load(loadType: LoadType, state: PagingState<Int, Anime>): MediatorResult { return try { withContext(Dispatchers.IO) { val page = when (loadType) { LoadType.REFRESH -> { 0 } LoadType.PREPEND -> { return@withContext MediatorResult.Success(true) } LoadType.APPEND -> { val remoteKey = pageKeyDao.getLastKey(sessionId) remoteKey?.nextKey ?: 0 } } if (loadType == LoadType.REFRESH || page == 0) { animeDao.deleteAllAnime() pageKeyDao.deleteAllPageKey() } val response: Response<AnimeSearchResult> = animeService.getAnimes( token = "取得したトークン", page = page, pageSize = state.config.pageSize ) val animeList = response.body()?.animeList ?: listOf() val endOfPage = response.isSuccessful && animeList.isEmpty() val nextKey = if(endOfPage) null else page + 1 pageKeyDao.insertKey( PageKey( id = 0, sessionId = sessionId, nextKey = nextKey ) ) animeDao.insertAnime(animeList) return@withContext MediatorResult.Success(endOfPaginationReached = endOfPage) } } catch (e: Exception) { return MediatorResult.Error(e) } } }