Android Academy · Урок

RemoteMediator и кэширование

Объединяйте постраничную загрузку из сети и базы данных

Урок 4 из 413 шагов

«RemoteMediator и кэширование» — бесплатный урок Android Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.

Постраничная загрузка с приоритетом офлайн-режима

Обычный сетевой PagingSource работает, но при разрыве соединения список становится пустым. Для надёжного приложения следует использовать базу данных как единственный источник истины, а сеть — для её заполнения в фоновом режиме.

Именно это обеспечивает RemoteMediator: данные постранично загружаются из локальной базы, а когда они заканчиваются, из сети запрашивается следующая страница.

Архитектура

С RemoteMediator данные движутся в одном направлении:

  • Интерфейс постранично загружает данные из PagingSource Room
  • Когда Room приближается к концу данных, RemoteMediator загружает из сети следующую страницу
  • Результат сетевого запроса записывается в Room
  • Room передаёт новые строки, и интерфейс обновляется

Пользователь всегда мгновенно видит кэшированные данные, даже без подключения к интернету.

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

Бесплатный PagingSource для Room

Room может автоматически создавать PagingSource. Достаточно объявить запрос, возвращающий PagingSource<Int, Entity>, и Room реализует его.

Именно из этого источника интерфейс фактически загружает данные постранично.

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

Таблица удалённых ключей

В отличие от PagingSource, RemoteMediator не может напрямую возвращать следующие и предыдущие ключи — их необходимо сохранять. Храните их в небольшой таблице удалённых ключей рядом с данными.

Это позволяет продолжать операции 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) — когда могут существовать дополнительные страницы
  • Error(e) — при ошибке; она передаётся как 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)
}

Подключение Pager к remoteMediator

Создайте Pager, указав в нём и remoteMediator, и pagingSourceFactory Room. Медиатор заполняет Room, а интерфейс постранично загружает данные из 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, чтобы найти нужный удалённый ключ в таблице ключей.

Вспомогательные функции делают load() понятнее при обработке REFRESH, APPEND и 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)

Кэширование и актуальность данных

Поскольку данные хранятся в 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 что является единственным источником истины, из которого интерфейс загружает данные постранично?

Итоги: RemoteMediator и кэширование

Вы создали конвейер постраничной загрузки с приоритетом офлайн-режима:

  • Интерфейс загружает данные из PagingSource Room — единственного источника истины
  • RemoteMediator.load() обрабатывает REFRESH/PREPEND/APPEND и записывает результаты в Room внутри транзакции
  • Таблица удалённых ключей сохраняет следующие и предыдущие ключи после перезапуска
  • Pager получает и remoteMediator, и pagingSourceFactory
  • initialize() управляет актуальностью кэша

Теперь Вы умеете эффективно загружать бесконечные списки онлайн и офлайн. Поздравляем с завершением курса по Paging 3.

Можно начать бесплатно

Изучай Kotlin с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
36
Уроки
152

Часто задаваемые вопросы

Урок «RemoteMediator и кэширование» бесплатный?

Да — полный текст урока «RemoteMediator и кэширование» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 4 уроков всего.

Чему я научусь в уроке «RemoteMediator и кэширование»?

Объединяйте постраничную загрузку из сети и базы данных Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Android Academy?

Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «RemoteMediator и кэширование»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Android Academy?

Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Зачем нужна постраничная загрузка
  2. PagingSource и Pager
  3. Постраничная загрузка в списках Compose
  4. RemoteMediator и кэширование
← Назад к Android Academy