RemoteMediator и кэширование
Объединяйте постраничную загрузку из сети и базы данных
«RemoteMediator и кэширование» — бесплатный урок Android Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.
Постраничная загрузка с приоритетом офлайн-режима
Обычный сетевой PagingSource работает, но при разрыве соединения список становится пустым. Для надёжного приложения следует использовать базу данных как единственный источник истины, а сеть — для её заполнения в фоновом режиме.
Именно это обеспечивает RemoteMediator: данные постранично загружаются из локальной базы, а когда они заканчиваются, из сети запрашивается следующая страница.
Архитектура
С RemoteMediator данные движутся в одном направлении:
- Интерфейс постранично загружает данные из
PagingSourceRoom - Когда 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 и кэширование
Вы создали конвейер постраничной загрузки с приоритетом офлайн-режима:
- Интерфейс загружает данные из
PagingSourceRoom — единственного источника истины RemoteMediator.load()обрабатываетREFRESH/PREPEND/APPENDи записывает результаты в Room внутри транзакции- Таблица удалённых ключей сохраняет следующие и предыдущие ключи после перезапуска
Pagerполучает иremoteMediator, иpagingSourceFactoryinitialize()управляет актуальностью кэша
Теперь Вы умеете эффективно загружать бесконечные списки онлайн и офлайн. Поздравляем с завершением курса по 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 — локальная установка не требуется.
Все уроки этого курса
- Зачем нужна постраничная загрузка
- PagingSource и Pager
- Постраничная загрузка в списках Compose
- RemoteMediator и кэширование