Реалізація кешування даних в мобільному додатку
Додаток без кешування — це додаток, який при кожному переході на екран робить сітьовий запит. Повільний інтернет, немає інтернету, дорогий мобільний трафік — користувач видить спіннер або пустий екран. Кешування вирішує це, але правильна реалізація складніша, ніж здається: потрібно вирішити що кешувати, як довго, як інвалідувати та що показувати при помилці.
Шари кешування
Гарна архітектура кешу — це кілька рівнів:
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, progressive 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 тижні залежно від кількості типів даних. Вартість розраховується індивідуально.







