おいしい健康 開発者ブログ

株式会社おいしい健康で働くエンジニア・デザイナーが社内の様子をお伝えします。

RemoteMediatorでページングを実装する

こんにちは。おいしい健康Androidエンジニアの小林です。 既存機能の改善や追加機能の開発をしています。

今回はおいしい健康Androidアプリの「人気のテーマリスト」機能の実装で利用した RemoteMediator について書きたいと思います。

※この記事で紹介するコードと実際のアプリのコードは異なります。

こんな機能

さまざまなテーマ別にピックアップされたレシピをリストで見られる機能です。

人気のテーマリスト
人気のテーマリスト

一度開いたテーマのデータは保持したい

テーマごとのレシピリストはAPIでサーバーサイドからデータを取得しています。

テーマ画面を開くたびに毎回APIからデータを取得するとユーザーを待たせがちになるので、取得したデータはアプリのローカルDB に記憶させておいて、再び開くときにはDBから取得するようにします。

RemoteMediatorでAPIとDBからのデータ取得をコーディネートする

RemoteMediatorについては、公式の情報を引用すると下記のように書いてあります。

RemoteMediator は、アプリがキャッシュ データを使い切った際に、ページング ライブラリからのシグナルとして機能します。このシグナルを使用して、追加のデータをネットワークから読み込み、ローカル データベースに保存することができます。 https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja

今回はローカルDBにRoomを利用します。

RemoteMediatorを実装する

class ThemeRecipeMediator (
    private val themeId: Int,
    private val database: Database,
    private val service: ApiService
): RemoteMediator<Int, Recipe>() {
    private val dao = database.themeRecipeDao()

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Recipe>
    ): MediatorResult {
        // ...
    }
}

load() 関数でAPIからのデータ取得やデータをRoomへ保存などの処理を記述します。

    private var page: Int = 1 // APIに渡すページKey

    override suspend fun load(loadType: LoadType, state: PagingState<Int, Recipe>): MediatorResult {
        return try {
            // APIで取得するページのloadKeyを確定します。
            // 今回は page を渡すとそのページのレシピデータを取得できるAPIになるため、ページ番号をLoadTypeステートによって変更します。
            val loadKey = when (loadType) {
                LoadType.REFRESH -> {
                    page++
                    1
                }
                LoadType.PREPEND ->
                    return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    state.lastItemOrNull() ?: return MediatorResult.Success(endOfPaginationReached = true)
                    page++
                }
            }

            // APIからデータ取得します
            val response = service.getThemesRecipe(themeId, geometries.toQueryMap(), loadKey)
            val recipes = response.body()?.recipes
            database.withTransaction {
                // 初めから再取得時にはデータを一度クリアにします
                if (loadType == LoadType.REFRESH) {
                    dao.deleteThemeRecipes(themeId)
                }

                // Roomへデータを保存します
                if (!recipes.isNullOrEmpty()) dao.insertThemeRecipes(recipes)
            }

            MediatorResult.Success(endOfPaginationReached = recipes?.isEmpty() ?: true)
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }

Pagerの引数にRemoteMediatorを渡す

実装したRemoteMediatorをPagerの引数に渡します。

val pager: Flow<PagingData<Recipe>> = Pager(
            config = PagingConfig(pageSize = 10, initialLoadSize = 10),
            remoteMediator = ThemeRecipeMediator(id, database, service)
        ) {
            dao.selectThemeRecipes(id) // テーマごとのレシピをリストで返します
        }.flow.cachedIn(lifecycleScope)

PagingDataAdapterにPagingDataを渡す

RecyclerViewに渡すPagingDataAdapterにPagingDataを渡します。

class ThemeRecipesAdapter() : PagingDataAdapter<Recipe, ThemeRecipesAdapter.ViewHolder>(DIFF_CALLBACK) {
     // ...
}
private val adapter =  ThemeRecipesAdapter()

viewModel.pager.collectLatest { pagingData ->
    adapter.submitData(pagingData)
}

これでRoomに保存したデータがあるときにはローカルデータを利用するので、表示が早くなりユーザーを待たせる時間を減らすことができます。

最後に

おいしい健康ではAndroidエンジニアの募集をしています。 サービス開発好きな方、ヘルスケア領域に興味のある方、ぜひお気軽にご連絡ください