0Pricing
Android Academy · Lesson

RemoteMediator and Caching

Combine network and database paging.

RemoteMediator and Caching is a free Android Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Offline-First Paging

A plain network PagingSource works, but if the connection drops the list is empty. For a robust app you want the database as the single source of truth and the network filling it in the background.

That is exactly what RemoteMediator enables: page from the local database, and fetch more from the network when the database runs out.

The Architecture

With RemoteMediator the data flows in one direction:

  • The UI pages from a Room PagingSource
  • When Room nears its end, RemoteMediator fetches the next network page
  • The network result is written into Room
  • Room emits the new rows, and the UI updates

The user always sees cached data instantly, even offline.

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

Room PagingSource for Free

Room can generate a PagingSource automatically. Just declare a query that returns PagingSource<Int, Entity> and Room implements it.

This is the source the UI actually pages from.

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>
}

Subclassing RemoteMediator

RemoteMediator<Key, Value> has one required method, load(), plus an optional initialize().

load() receives a LoadType (REFRESH, PREPEND or APPEND) and the current PagingState, then returns a 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
}

Handling LoadType

In load() you decide which page to fetch based on the LoadType:

  • REFRESH - load the first page (or around the anchor)
  • PREPEND - usually nothing to do; return endOfPaginationReached = true
  • APPEND - load the page after the last one we have
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 Table

Unlike a PagingSource, a RemoteMediator cannot return next/prev keys directly - they must be persisted. Store them in a small remote keys table alongside your data.

This lets you resume APPEND/PREPEND across app restarts.

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?
)

Writing Network Results to Room

After fetching a page, write the items and their keys to Room inside a transaction. On REFRESH, clear the old tables first so the cache stays consistent.

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() })
}

Returning MediatorResult

Finish load() by returning a MediatorResult:

  • Success(endOfPaginationReached = true) when there are no more pages
  • Success(endOfPaginationReached = false) when more may exist
  • Error(e) on failure - surfaced as a LoadState.Error
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)
}

Wiring Pager with remoteMediator

Build the Pager with both the remoteMediator and the Room pagingSourceFactory. The mediator fills Room; the UI pages from Room.

Because it uses an experimental API, opt in with @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

Looking Up Keys for APPEND

For an APPEND load you need the nextKey of the last loaded item. Use the PagingState anchor to find the right remote key from your keys table.

Helper functions keep load() readable across REFRESH, APPEND and PREPEND.

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)

Caching and Freshness

Because data lives in Room, the app loads instantly and works offline. To avoid stale content, override initialize() to decide whether to refresh on launch based on how old the cache is.

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
    }
}

Quick Check

In a RemoteMediator setup, what is the single source of truth that the UI pages from?

Recap: RemoteMediator and Caching

You built an offline-first paging pipeline:

  • The UI pages from a Room PagingSource - the single source of truth
  • RemoteMediator.load() handles REFRESH/PREPEND/APPEND and writes results to Room in a transaction
  • A remote keys table persists next/prev keys across restarts
  • The Pager takes both a remoteMediator and a pagingSourceFactory
  • initialize() controls cache freshness

You can now load endless lists efficiently, online or offline. Congratulations on completing the Paging 3 course.

Frequently asked questions

Is the “RemoteMediator and Caching” lesson free?

Yes — the full text of “RemoteMediator and Caching” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “RemoteMediator and Caching”?

Combine network and database paging. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “RemoteMediator and Caching” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Why Paging
  2. PagingSource and Pager
  3. Paging in Compose Lists
  4. RemoteMediator and Caching
← Back to Android Academy