1
/
5

エンジニアブログ「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含むさまざまなエンジニア職種を募集しています。 サービス開発好きな方、ヘルスケア領域に興味のある方、カジュアル面談も行っていますのでぜひお気軽にご連絡ください!

エンジニアブログでは、おいしい健康のエンジニアメンバーが日々どんな課題に向き合っているのかを綴っています。ご興味ある方はぜひこちらも覗いてみてください。

https://oishi-kenko.hatenablog.com/

Invitation from おいしい健康
If this story triggered your interest, have a chat with the team?
おいしい健康's job postings
4 Likes
4 Likes

Weekly ranking

Show other rankings
Like Maasa Noguchi's Story
Let Maasa Noguchi's company know you're interested in their content