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/

おいしい健康's job postings
4 Likes
4 Likes

Weekly ranking

Show other rankings
If this story triggered your interest, go ahead and visit them to learn more