SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

アプリケーション開発の最新トレンド

Androidのページング処理のライブラリ「Paging 3」の移行と実装のポイント

  • X ポスト
  • このエントリーをはてなブックマークに追加

APIから取得したデータをDBに保存してキャッシュをする場合

RemoteMediatorの実装

 最後に、RemoteMediatorを継承したAnimeMediatorを実装していきます。

 RemoteMediatorは、最初とっかかりにくい部分ですので、細かく区切って説明を進めていきます。

 まず、RemoteMediatorで実装が必要なメソッドは基本的に1つです。

  • load:APIからデータを取得し、DBに保存する関数です。Paging 2等で使用されていたBoundaryCallbackでは、データを読み込むメソッドが2つありましたが、RemoteMediatorでは、1つのloadの中で初回読み込みか追加読み込みかといった識別はload関数の中で引数にわたってくるLoadTypeを用いて行います。

 まず、Load関数の流れは以下になります。

  1. LoadTypeによりloadするPageを決定
  2. LoadTypeがREFRESHつまり初期読み込みまたはpageが0の時データベースのアニメのデータとkeyのデータを削除
  3. APIからデータを取得
  4. APIから取得したデータを元に、データをすべて読み込んだか、nextKeyはいくつかを決定
  5. DBに取得したデータと次回以降使用するnextKeyとsessionIdを持ったKeyを保存
  6. 最後にSuccessとしてMediatorResultを返却
  7. 上記でエラーが返却された際に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)
        }
    }
}

次のページ
APIから取得したデータをDBに保存してキャッシュをする場合

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
アプリケーション開発の最新トレンド連載記事一覧

もっと読む

この記事の著者

長濱 伶(ヤフー株式会社)(ナガハマ レイ)

 2000年沖縄県生まれ。 学生時代に計算機シミュレーションを用いた文化の安定性に関する研究に従事。 2021年3月に沖縄工業高等専門学校のメディア情報工学科卒業後、4月にヤフー株式会社に入社。 2021年7月からPayPayフリマのAndroidアプリ開発に携わる。 Twitter: @Fel1Tech

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/15314 2022/02/17 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング