Image Loading (Coil & Glide) Explained
DATA › Networking
Loading images off the network is a top Android interview topic because almost every app does it, and it's easy to get wrong: block the UI thread, decode a full-resolution bitmap into a tiny thumbnail, or forget to cancel a request when its ImageView scrolls off screen. Two libraries dominate this space. **Coil** (Coroutine Image Loader) is Kotlin-first, built on coroutines and Okio, and ships Compose composables like AsyncImage. **Glide** is the older, more mature library, Java-based and View or RecyclerView-oriented, with deep lifecycle integration and a BitmapPool that aggressively reuses bitmap allocations.
Both solve the same three problems: load asynchronously off the main thread, cache aggressively so scrolling doesn't refetch, and cancel or replace requests tied to a specific view. The rest of this lesson works through those three problems one at a time.
In Compose, the simplest way to show a remote image is the AsyncImage composable:
AsyncImage(
model = "https://example.com/photo.jpg",
contentDescription = "Photo"
)
Under the hood it launches the load on a coroutine scoped to the composition, so it automatically cancels when the composable leaves composition, no manual cleanup needed. If you need a raw Painter to hand to Icon or draw yourself, use rememberAsyncImagePainter instead. And when you need genuinely different composables for the loading, success, and error states, not just placeholder or error drawables, reach for SubcomposeAsyncImage. It uses subcomposition to let each state render its own content, at the cost of being more expensive and a poor fit for sizing items inside a LazyColumn.
A bitmap's memory cost is roughly width x height x bytes-per-pixel, decoded at full resolution regardless of how small the view showing it is. Decode a 4000x3000 photo into a 64dp thumbnail without downsampling and you've allocated tens of megabytes for a handful of visible pixels, exactly the kind of thing that adds up into an OutOfMemoryError when a list has dozens of images in flight.
Both Coil and Glide handle this automatically: they downsample during decode (an inSampleSize-style trick) to roughly the target view or requested size, so you never pay for pixels you can't see.
imageView.load(url) {
size(128, 128) // decode only what the view needs
}
This is why you should almost never call BitmapFactory.decodeFile yourself for a thumbnail, you'd lose this optimization entirely.
Both libraries use a **two-tier cache**. A fast in-memory cache holds already-decoded bitmaps; a slower on-disk cache holds the original or transformed bytes. A lookup checks memory first, then disk, then finally hits the network, each tier is progressively more expensive.
The memory cache key matters: it's not just the URL, it also includes the requested size and any transformations. Load the same URL once plain and once with a CircleCropTransformation, and you get **two separate cache entries**, not one shared bitmap:
imageView.load(url) // key: (url, size)
imageView.load(url) { transformations(CircleCropTransformation()) } // different key
That's deliberate: a circle-cropped bitmap and the original are different pixels and can't share a cache slot.
In a RecyclerView, rows get reused as you scroll: view holder 3's ImageView might get rebound to a different item before its old image request finishes. If that stale request finishes late and just sets the bitmap, you get the classic **wrong-image bug**, a photo for row 3 appearing under row 7's text.
Both libraries prevent this the same way: the request is tied to the target ImageView itself, so starting a new load on that view cancels whatever was previously in flight for it.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val url = items[position].thumbnailUrl
holder.imageView.load(url) // cancels any stale request on this view
}
You don't need to manually track or cancel anything, binding a new URL to the view is the cancellation mechanism.
Glide adds a layer Coil doesn't: **lifecycle awareness at the call site**. Glide.with(fragment) or Glide.with(activity) binds the whole request to that component's lifecycle, pausing it when the host stops and clearing it when the host is destroyed, on top of the per-view cancellation you just saw.
Glide.with(fragment) // binds to the Fragment's lifecycle
.load(imageUrl)
.into(imageView)
Glide also exposes DiskCacheStrategy to control what lands on disk: NONE, DATA (original bytes), RESOURCE (decoded or transformed), ALL (both), or the default AUTOMATIC, which picks intelligently, typically caching original bytes for remote sources so the same source can still be re-transformed differently later.
One nuance that trips people up: on supported API levels, Coil decodes into Bitmap.Config.HARDWARE by default. A hardware bitmap's pixels live in graphics memory rather than the Java heap, which eases both heap pressure and GC churn, especially useful for scrolling lists full of images.
The catch: **you cannot read a hardware bitmap's pixels on the CPU**. Anything that needs pixel access, a custom software transformation, getPixel, saving raw bytes, forces a software config instead:
imageView.load(url) {
bitmapConfig(Bitmap.Config.ARGB_8888) // needed for CPU pixel access
}
Most apps never need to touch this, AsyncImage and load just work, but knowing why a getPixel call crashes on an otherwise-normal load is a good "I actually understand this" signal in an interview.
Zooming out: image loading is async loading plus a two-tier cache plus per-view cancellation, and both libraries implement all three. Coil leans coroutine-native and Compose-first, with a shared ImageLoader singleton (configure once via ImageLoaderFactory, since every ImageLoader owns its own caches). Glide leans View or lifecycle-first, with BitmapPool-based bitmap reuse and explicit DiskCacheStrategy control.
In an interview, the strongest answer isn't reciting API names, it's connecting them to the failure mode each one prevents: downsampling prevents OutOfMemoryError, request-per-view binding prevents the wrong-image bug in lists, and the memory-then-disk-then-network order is just cost-ordered caching. If you can explain *why* a mechanism exists, not just *that* it exists, that's what interviewers are actually listening for.