On this page
"There are only two hard things in computer science: cache invalidation and naming things." On Android, caching is also essential: users on slow networks, data caps and flaky connections notice every unnecessary request. The goal is a deliberate policy per data type, not caching everything.
Cache layers on Android
Core strategies
| Strategy | Behaviour | Use for |
|---|---|---|
| Cache-first | Use cache if present; network only when missing | Static or rarely changing data (country lists, config) |
| Network-first | Try network; fall back to cache on failure | Data that must be fresh but should degrade offline (account balance with "last updated") |
| Stale-while-revalidate | Show cache immediately, refresh in background, update UI | Feeds, lists, profiles: most content |
| Network-only | Never cache | Payments, OTPs, highly sensitive or one-time data |
Stale-while-revalidate with Room
class ProfileRepository(
private val api: ProfileApi,
private val dao: ProfileDao,
private val clock: Clock,
) {
private val maxAge = Duration.ofMinutes(10)
/** Emits cached profile immediately; refreshes in the background if stale. */
fun observeProfile(userId: String): Flow<Profile?> = dao.observe(userId)
.onStart { refreshIfStale(userId) }
.map { it?.toDomain() }
private suspend fun refreshIfStale(userId: String) {
val cached = dao.get(userId)
val stale = cached == null ||
Duration.between(Instant.ofEpochMilli(cached.fetchedAt), clock.instant()) > maxAge
if (stale) {
runCatching { api.profile(userId) }
.onSuccess { dao.upsert(it.toEntity(fetchedAt = clock.millis())) }
// On failure, keep serving the cached copy; the UI can show "last updated".
}
}
}Running the refresh in onStart delays the first emission until the refresh attempt finishes. To show the cache instantly, launch the refresh in a separate coroutine from the ViewModel instead.
HTTP caching with OkHttp
If your server sends proper Cache-Control and ETag headers, OkHttp's disk cache handles freshness and conditional requests (304 Not Modified) automatically:
val client = OkHttpClient.Builder()
.cache(Cache(directory = File(context.cacheDir, "http_cache"), maxSize = 50L * 1024 * 1024))
.build()
// Offline fallback: accept stale cached responses when there is no connectivity
class OfflineCacheInterceptor(private val isOnline: () -> Boolean) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isOnline()) {
request = request.newBuilder()
.cacheControl(CacheControl.Builder().onlyIfCached().maxStale(7, TimeUnit.DAYS).build())
.build()
}
return chain.proceed(request)
}
}HTTP caching is great for GET responses you display as-is. When you need to query, combine or modify data locally, Room is the better cache.
In-memory caches
class ExchangeRateCache(maxEntries: Int = 50) {
private val cache = LruCache<String, Pair<Double, Long>>(maxEntries)
fun get(pair: String, maxAgeMs: Long, now: Long): Double? =
cache.get(pair)?.takeIf { now - it.second < maxAgeMs }?.first
fun put(pair: String, rate: Double, now: Long) { cache.put(pair, rate to now) }
}Memory caches are fast but vanish with the process, so use them for hot, recomputable data. Respond to onTrimMemory by clearing large ones.
Images
Image libraries such as Coil and Glide provide memory and disk caching, downsampling and request deduplication. Don't build your own. Make sure image URLs change when content changes (versioned URLs), or use cache headers correctly.
Invalidation rules
- Time-based (TTL): simple and predictable. Choose per data type.
- Event-based: after a user edits their profile, update or invalidate the cache immediately.
- Server-driven: ETags and versions, or push messages that trigger a refresh.
- On logout: clear all user-specific caches, including the HTTP cache if it contains personal data.
Policy per data type: an example
| Data | Strategy | TTL / trigger |
|---|---|---|
| Product catalogue | Stale-while-revalidate (Room) | 1 hour; pull-to-refresh |
| Prices and stock | Network-first | Always refresh on product open |
| User profile | Stale-while-revalidate | 10 minutes; invalidate on edit |
| App config | Cache-first | Refresh on app start in background |
| Payment status | Network-only | - |
Common mistakes
- Caching without any invalidation, so users see outdated data indefinitely.
- Different screens caching the same data separately, so they disagree.
- Personal data left in caches after logout.
- Unbounded caches that grow until storage or memory runs out.
Frequently asked questions
Room cache or HTTP cache?
HTTP cache for responses you show directly and that the server controls with headers. Room when you need queries, joins, offline edits or a single source of truth across screens.
Where should cache logic live?
In repositories or data sources, so every caller gets consistent freshness.
How do I show staleness to users?
Store fetchedAt with cached data, and display "Updated 5 min ago" when it matters, for example for balances and prices.
Conclusion
Effective caching is per data type: choose cache-first, network-first, stale-while-revalidate or network-only; pick the right layer (memory, Room, HTTP); and define clear invalidation, including on logout.
Next step: secure the credentials behind those requests with Secure Token Storage, or move on to quality with Unit Testing in Android with JUnit.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.