Реализация кэширования данных в мобильном приложении
Приложение без кэширования — это приложение, которое при каждом переходе на экран делает сетевой запрос. Медленный интернет, нет интернета, дорогой мобильный трафик — пользователь видит спиннер или пустой экран. Кэширование решает это, но реализовать его правильно сложнее, чем кажется: нужно решить что кэшировать, как долго, как инвалидировать и что показывать при ошибке.
Слои кэширования
Хорошая архитектура кэша — это несколько уровней:
Memory cache — в памяти процесса, самый быстрый. Данные живут до перезапуска приложения. Для изображений — NSCache на iOS (автоматически чистится при memory pressure), LruCache на Android.
Disk cache — данные в файлах или локальной БД. Переживают перезапуск. Для API-ответов — Room/SQLite с timestamp, для изображений — DiskLruCache внутри Coil/Glide.
Network — источник правды, к которому обращаемся только когда нужно свежее.
Стратегия cache-first, refresh-in-background (stale-while-revalidate) — самая удобная для пользователя: мгновенно показываем кэш, параллельно обновляем, если изменилось — перерисовываем.
Кэш API-ответов через Room
// Entity с timestamp для инвалидации
@Entity(tableName = "products_cache")
data class ProductCacheEntity(
@PrimaryKey val id: String,
val categoryId: String,
val payload: String, // JSON-строка
val cachedAt: Long, // Unix timestamp
val etag: String? = null
)
// Repository: логика cache-first
class ProductRepository(
private val api: ProductApi,
private val dao: ProductCacheDao,
private val cacheMaxAge: Long = 5 * 60 * 1000L // 5 минут
) {
fun getProductsByCategory(categoryId: String): Flow<List<Product>> = flow {
// 1. Сразу отдаём кэш
val cached = dao.getByCategory(categoryId)
if (cached.isNotEmpty()) {
emit(cached.map { it.toProduct() })
}
// 2. Проверяем свежесть
val oldestEntry = cached.minOfOrNull { it.cachedAt } ?: 0L
val needsRefresh = System.currentTimeMillis() - oldestEntry > cacheMaxAge
if (needsRefresh || cached.isEmpty()) {
try {
val fresh = api.getProducts(categoryId)
val entities = fresh.map { it.toCacheEntity(categoryId) }
dao.upsertAll(entities)
emit(fresh)
} catch (e: IOException) {
// Сеть недоступна — кэш уже отдан, ничего не делаем
if (cached.isEmpty()) throw e // нечего показать — пробрасываем
}
}
}
}
Этот паттерн — основа offline-first. UI подписан на Flow, получает данные дважды: сначала кэш, потом свежие.
ETag и Last-Modified
Вместо time-based инвалидации можно использовать HTTP-кэш заголовки. Сервер отдаёт ETag: "v42", следующий запрос отправляет If-None-Match: "v42" — если данные не изменились, сервер возвращает 304 без тела. Экономит трафик.
OkHttp (базовый HTTP-клиент для Android и React Native) поддерживает HTTP-кэш из коробки:
val cache = Cache(
directory = File(context.cacheDir, "http-cache"),
maxSize = 10L * 1024 * 1024 // 10 МБ
)
val client = OkHttpClient.Builder()
.cache(cache)
.build()
Для iOS URLSession поддерживает URLCache аналогично. Но HTTP-кэш работает только если сервер присылает корректные Cache-Control заголовки — если бэкенд отдаёт Cache-Control: no-store, кэш не сработает вне зависимости от настроек клиента.
Кэширование изображений
Для изображений готовые библиотеки решают задачу лучше любой самоделки:
- Android: Coil 2.x — Kotlin-first, Compose-ready, memory + disk cache, placeholder/error states
- iOS: SDWebImage или Kingfisher — async загрузка, NSCache + disk, прогрессивный JPEG
-
React Native:
react-native-fast-image(обёртка над SDWebImage/Glide)
// Coil в Compose
AsyncImage(
model = ImageRequest.Builder(context)
.data(product.imageUrl)
.memoryCacheKey(product.id)
.diskCacheKey(product.imageUrl)
.crossfade(true)
.build(),
contentDescription = product.title,
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error_image)
)
Инвалидация кэша
Самая сложная часть. Стратегии:
- TTL (Time To Live) — кэш живёт N минут, потом устарел. Просто, предсказуемо.
- Event-based — сервер присылает push-уведомление об изменении данных → инвалидируем конкретный кэш. Точно, но требует серверной поддержки.
-
Version-based — при каждом ответе сервер присылает
dataVersion, клиент сравнивает с сохранённым. - Pull-to-refresh — пользователь явно запрашивает обновление. Всегда нужен как fallback.
Реализация многоуровневого кэша с Room + HTTP-кэш + инвалидацией: 1–2 недели в зависимости от количества типов данных. Стоимость рассчитывается индивидуально.







