こんにちは。おいしい健康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含むさまざまなエンジニア職種を募集しています。 サービス開発好きな方、ヘルスケア領域に興味のある方、カジュアル面談も行っていますのでぜひお気軽にご連絡ください!
エンジニアブログでは、おいしい健康のエンジニアメンバーが日々どんな課題に向き合っているのかを綴っています。ご興味ある方はぜひこちらも覗いてみてください。