こんにちは。おいしい健康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エンジニアの募集をしています。 サービス開発好きな方、ヘルスケア領域に興味のある方、ぜひお気軽にご連絡ください