0Pricing
Android Academy · Lesson

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 ImageLoader so caches are shared.
  • Use ContentScale.Crop to 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 via SingletonImageLoader.Factory.
  • Tune per-request behavior with memoryCachePolicy and diskCachePolicy.
  • 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

  1. Loading Images with Coil
  2. Placeholders and Error States
  3. Caching and Performance
  4. Playing Audio and Video
← Back to Android Academy