Caching and Performance
Memory and disk caching basics.
Caching and Performance is a free Android Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Caching Matters
Downloading and decoding an image is expensive: it uses bandwidth, battery, and CPU. If you refetch the same image every time a list scrolls, your app feels slow and burns data.
Caching stores decoded images and downloaded bytes so the next request is nearly instant. Coil ships with two caches by default: an in-memory cache (fast, decoded bitmaps) and a disk cache (persistent, raw bytes).
This lesson explains how they work and how to tune them.
Two Layers: Memory and Disk
Coil checks caches in order:
- Memory cache: holds recently used decoded bitmaps in RAM. A hit here is instant, no decoding needed.
- Disk cache: stores the original downloaded bytes on the device. A hit here skips the network but still decodes.
- Network: the slow path, only used when both caches miss.
Memory is fast but small and cleared when the app dies; disk is larger and survives restarts.
Configuring the ImageLoader
Caches live on the ImageLoader, the engine behind every request. You usually build one custom loader and reuse it. Here we size the memory cache to a percentage of available RAM and the disk cache to a fixed byte budget.
A singleton loader avoids duplicate caches and wasted memory.
import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import okio.Path.Companion.toOkioPath
fun newImageLoader(context: Context): ImageLoader =
ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.25) // 25% of app RAM
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache").toOkioPath())
.maxSizeBytes(50L * 1024 * 1024) // 50 MB
.build()
}
.build()Providing a Singleton Loader
Coil 3 lets you set a single application-wide loader by implementing SingletonImageLoader.Factory on your Application class. Then every AsyncImage uses it automatically, no need to pass a loader each time.
import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
class MyApp : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: PlatformContext): ImageLoader =
ImageLoader.Builder(context)
.crossfade(true)
.build()
}Controlling Cache Policies
Each request can opt in or out of caching with memoryCachePolicy and diskCachePolicy. Policies are ENABLED, READ_ONLY, WRITE_ONLY, or DISABLED.
For example, disable the memory cache for huge one-off images you don't want to keep in RAM, or disable disk for sensitive content.
import android.content.Context
import coil3.request.CachePolicy
import coil3.request.ImageRequest
fun noMemoryCacheRequest(context: Context, url: String) =
ImageRequest.Builder(context)
.data(url)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build()Resize: Don't Decode Huge Bitmaps
The single biggest performance mistake is decoding a 4000x3000 photo into a 100dp thumbnail. That wastes memory and time. By default Coil sizes the bitmap to the composable's measured bounds, so a small AsyncImage decodes a small bitmap.
You can force an explicit target size with size() on the request when you know the dimensions.
import android.content.Context
import coil3.request.ImageRequest
import coil3.size.Size
fun thumbnailRequest(context: Context, url: String) =
ImageRequest.Builder(context)
.data(url)
.size(Size(200, 200)) // decode at most 200x200
.build()Stable Cache Keys
Coil derives a cache key from the request data. If your URLs contain changing query parameters (like a signed token), identical images get different keys and never hit the cache.
Set an explicit memoryCacheKey and diskCacheKey based on the stable part (such as the image id) so caching works.
import android.content.Context
import coil3.request.ImageRequest
fun stableKeyRequest(context: Context, id: String, signedUrl: String) =
ImageRequest.Builder(context)
.data(signedUrl)
.memoryCacheKey("image_$id")
.diskCacheKey("image_$id")
.build()Preloading Images
For a smoother experience you can warm the cache before an image is shown, for example prefetch the next page of a feed. Use the loader's enqueue with a request that has no target.
When the user reaches the image, it's already cached and appears instantly.
import android.content.Context
import coil3.ImageLoader
import coil3.request.ImageRequest
fun preload(context: Context, loader: ImageLoader, urls: List<String>) {
urls.forEach { url ->
val request = ImageRequest.Builder(context)
.data(url)
.build()
loader.enqueue(request)
}
}Clearing the Cache
Occasionally you need to free space or force a refresh, for example after the user updates their avatar. You can clear the memory cache, the disk cache, or remove a single key.
Clearing everything is heavy, prefer removing specific keys when you can.
import coil3.ImageLoader
import coil3.memory.MemoryCache
fun refreshAvatar(loader: ImageLoader, key: String) {
// Remove one entry so the next load refetches
loader.memoryCache?.remove(MemoryCache.Key(key))
loader.diskCache?.remove(key)
}
fun clearAll(loader: ImageLoader) {
loader.memoryCache?.clear()
// Disk clearing is heavier; do it sparingly
}Crossfade vs Cache Hits
Crossfade looks great for network loads, but it can feel sluggish when an image comes straight from the memory cache and could appear instantly. Coil is smart here: by default it skips the crossfade animation for memory-cache hits and only animates genuinely loaded images.
So you get smooth fades on first load and instant display on cached scrolls, no extra work required.
import android.content.Context
import coil3.request.ImageRequest
import coil3.request.crossfade
fun feedRequest(context: Context, url: String) =
ImageRequest.Builder(context)
.data(url)
// Crossfade plays on real loads; memory-cache hits show instantly
.crossfade(true)
.build()Performance Checklist for Lists
When showing many images in a LazyColumn or grid, follow these rules for smooth scrolling:
- Give each image a fixed size so Coil decodes a small bitmap.
- Reuse one singleton
ImageLoaderso caches are shared. - Use
ContentScale.Cropto avoid layout shifts. - Keep cache keys stable across reloads.
- Let Coil cancel off-screen requests automatically, don't fight it.
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
@Composable
fun PhotoGrid(urls: List<String>) {
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
items(urls) { url ->
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.aspectRatio(1f)
)
}
}
}Quick Check
Coil checks its caches before hitting the network. Which cache, when hit, is fastest because it skips decoding entirely?
Recap
You learned how Coil keeps images fast:
- Two caches: memory (decoded, fast, small) and disk (raw bytes, persistent, larger).
- Configure sizes on a custom
ImageLoader, and expose one viaSingletonImageLoader.Factory. - Tune per-request behavior with
memoryCachePolicyanddiskCachePolicy. - Always decode at the display size; set explicit
size()when needed. - Use stable cache keys, preload upcoming images, and clear specific keys to refresh.
Next: stepping beyond images to play audio and video.
Frequently asked questions
Is the “Caching and Performance” lesson free?
Yes — the full text of “Caching and Performance” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.
What will I learn in “Caching and Performance”?
Memory and disk caching basics. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Android Academy?
No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Caching and Performance” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Android Academy lesson?
Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Loading Images with Coil
- Placeholders and Error States
- Caching and Performance
- Playing Audio and Video