Android Academy · レッスン

RemoteMediatorとキャッシュ

ネットワークとデータベースのページングを組み合わせます。

レッスン 4/413 ステップ

「RemoteMediatorとキャッシュ」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

オフラインファーストのページング

通常のネットワーク用PagingSourceでも動作しますが、接続が切れるとリストが空になります。堅牢なアプリにするには、データベースを唯一の信頼できる情報源とし、ネットワークからバックグラウンドでデータを補充する構成にします。

これを実現するのがRemoteMediatorです。ローカルデータベースからページングし、データベースにデータがなくなったらネットワークから追加取得します。

アーキテクチャ

RemoteMediatorを使用すると、データは一方向に流れます。

  • UIはRoomのPagingSourceからページングします
  • Roomの終端が近づくと、RemoteMediatorがネットワークから次のページを取得します
  • ネットワークの結果がRoomに書き込まれます
  • Roomが新しい行を発行し、UIが更新されます

ユーザーはオフラインでも、常にキャッシュ済みのデータをすぐに確認できます。

// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directly

RoomのPagingSourceを自動生成

RoomはPagingSourceを自動的に生成できます。PagingSource<Int, Entity>を返すクエリを宣言するだけで、Roomが実装してくれます。

これが、UIが実際にページングするデータソースです。

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY position ASC")
    fun pagingSource(): PagingSource<Int, ArticleEntity>
}

RemoteMediatorのサブクラス化

RemoteMediator<Key, Value>には、必須メソッドのload()と、オプションのinitialize()があります。

load()はLoadType(REFRESH、PREPEND、APPENDのいずれか)と現在のPagingStateを受け取り、MediatorResultを返します。

import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
    private val api: ArticleApi,
    private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
    // override suspend fun load(loadType, state): MediatorResult
}

LoadTypeの処理

load()では、LoadTypeに基づいて取得するページを決定します。

  • REFRESH — 先頭のページ(またはアンカー付近)を読み込みます
  • PREPEND — 通常は処理することがないため、endOfPaginationReached = trueを返します
  • APPEND — 現在取得済みの最後のページの次を読み込みます
val page: Int = when (loadType) {
    LoadType.REFRESH -> 1
    LoadType.PREPEND ->
        return MediatorResult.Success(endOfPaginationReached = true)
    LoadType.APPEND -> {
        val lastKey = db.remoteKeyDao().last()?.nextKey
            ?: return MediatorResult.Success(endOfPaginationReached = true)
        lastKey
    }
}

Remote Keysテーブル

PagingSourceとは異なり、RemoteMediatorは次や前のキーを直接返せません。キーは永続化する必要があります。データと一緒に、小さなremote keysテーブルへ保存します。

これにより、アプリを再起動してもAPPENDやPREPENDを再開できます。

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "remote_keys")
data class RemoteKey(
    @PrimaryKey val articleId: Int,
    val prevKey: Int?,
    val nextKey: Int?
)

ネットワーク結果をRoomに書き込む

ページを取得したら、アイテムとそのキーをトランザクション内でRoomに書き込みます。REFRESHでは、キャッシュの整合性を保つため、先に古いテーブルをクリアします。

db.withTransaction {
    if (loadType == LoadType.REFRESH) {
        db.remoteKeyDao().clearAll()
        db.articleDao().clearAll()
    }
    val nextKey = if (response.items.isEmpty()) null else page + 1
    db.remoteKeyDao().insertAll(
        response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
    )
    db.articleDao().insertAll(response.items.map { it.toEntity() })
}

MediatorResultを返す

load()の最後にMediatorResultを返します。

  • ページがこれ以上ない場合は、Success(endOfPaginationReached = true)
  • さらに存在する可能性がある場合は、Success(endOfPaginationReached = false)
  • 失敗した場合はLoadState.Errorとして通知されるError(e)
return try {
    val response = api.getArticles(page = page, size = state.config.pageSize)
    db.withTransaction { /* write items + keys */ }
    MediatorResult.Success(
        endOfPaginationReached = response.items.isEmpty()
    )
} catch (e: IOException) {
    MediatorResult.Error(e)
} catch (e: HttpException) {
    MediatorResult.Error(e)
}

remoteMediatorをPagerに組み込む

remoteMediatorとRoomのpagingSourceFactoryの両方を指定してPagerを構築します。mediatorがRoomにデータを補充し、UIはRoomからページングします。

実験的なAPIを使用するため、@OptIn(ExperimentalPagingApi::class)でオプトインします。

@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
    config = PagingConfig(pageSize = 20),
    remoteMediator = ArticleRemoteMediator(api, db),
    pagingSourceFactory = { db.articleDao().pagingSource() }
).flow

APPEND用のキーを検索する

APPENDの読み込みでは、最後に読み込んだアイテムのnextKeyが必要です。PagingStateのアンカーを使って、キーのテーブルから適切なremote keyを検索します。

ヘルパー関数を使うと、REFRESH、APPEND、PREPENDに対応するload()を読みやすく保てます。

private suspend fun lastRemoteKey(
    state: PagingState<Int, ArticleEntity>
): RemoteKey? {
    return state.pages.lastOrNull { it.data.isNotEmpty() }
        ?.data?.lastOrNull()
        ?.let { db.remoteKeyDao().keyFor(it.id) }
}

// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
    ?: return MediatorResult.Success(endOfPaginationReached = true)

キャッシュと鮮度

データはRoomに保存されるため、アプリはすぐに起動し、オフラインでも動作します。古いコンテンツを表示しないようにするには、initialize()をオーバーライドし、キャッシュの経過時間に基づいて起動時に更新するかどうかを決定します。

override suspend fun initialize(): InitializeAction {
    val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
    val cacheTimeout = TimeUnit.HOURS.toMillis(1)
    return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
        InitializeAction.LAUNCH_INITIAL_REFRESH
    } else {
        InitializeAction.SKIP_INITIAL_REFRESH
    }
}

確認問題

RemoteMediatorを使う構成で、UIがページングする唯一の信頼できる情報源は何ですか?

まとめ:RemoteMediatorとキャッシュ

オフラインファーストのページングパイプラインを構築しました。

  • UIはRoomのPagingSource(唯一の信頼できる情報源)からページングします
  • RemoteMediator.load()がREFRESH、PREPEND、APPENDを処理し、結果をトランザクション内でRoomに書き込みます
  • remote keysテーブルが次や前のキーを永続化し、再起動後も利用できるようにします
  • PagerにはremoteMediatorとpagingSourceFactoryの両方を指定します
  • initialize()がキャッシュの鮮度を制御します

これで、オンラインでもオフラインでも、無限に続くリストを効率よく読み込めるようになりました。Paging 3コースの修了、おめでとうございます。

無料で開始

AI チューターと学ぶ Kotlin — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
36
レッスン
152

よくある質問

「RemoteMediatorとキャッシュ」レッスンは無料ですか?

はい。「RemoteMediatorとキャッシュ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「RemoteMediatorとキャッシュ」で何を学びますか?

ネットワークとデータベースのページングを組み合わせます。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「RemoteMediatorとキャッシュ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Pagingが必要な理由
  2. PagingSourceとPager
  3. ComposeのリストでPagingを使う
  4. RemoteMediatorとキャッシュ
← Android Academyに戻る