0Pricing
Android Academy · Lesson

Loading Images with Coil

Async image loading in Compose.

Loading Images with Coil is a free Android Academy lesson on CoddyKit — lesson 1 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 You Need an Image Loader

Most modern apps show images from the internet: profile photos, product pictures, article thumbnails. Downloading and decoding those images correctly is harder than it looks.

You have to fetch bytes over the network on a background thread, decode them into a bitmap, cache them so you don't refetch, and cancel the request if the user scrolls away. Doing this by hand is error-prone.

Coil (Coroutine Image Loader) handles all of this for you with a tiny, Compose-friendly API. In this lesson you'll load your first remote image.

Adding Coil to Your Project

Coil ships as a Gradle dependency. For Jetpack Compose you want the coil-compose artifact, which gives you the AsyncImage composable.

Add it to your module's build.gradle.kts. Coil 3 is the current major version and works across platforms; the Compose integration is what we use here.

// build.gradle.kts (module level)
dependencies {
    implementation("io.coil-kt.coil3:coil-compose:3.0.4")
    implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")
}

Internet Permission

To download images from a URL your app needs the INTERNET permission. This is a normal (install-time) permission, so you only declare it in the manifest. There is no runtime prompt.

Without it, every network request fails with a security exception.

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>
</manifest>

Your First AsyncImage

The simplest way to show a remote image in Compose is AsyncImage. Give it a model (usually a URL string) and a contentDescription, and Coil does the rest: background fetch, decode, and display.

It automatically uses coroutines tied to the composition, so the request is cancelled if the composable leaves the screen.

import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage

@Composable
fun Avatar() {
    AsyncImage(
        model = "https://example.com/avatar.png",
        contentDescription = "User avatar",
        modifier = Modifier.size(96.dp)
    )
}

contentScale: How the Image Fits

Images rarely match the exact size of your layout slot. ContentScale controls how the bitmap is scaled inside its bounds.

  • ContentScale.Crop fills the box and clips overflow (great for avatars and thumbnails).
  • ContentScale.Fit keeps the whole image visible, possibly leaving empty space.
  • ContentScale.FillBounds stretches to fill (can distort).

Crop is the most common choice for fixed-size slots.

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage

@Composable
fun CircleAvatar(url: String) {
    AsyncImage(
        model = url,
        contentDescription = "Avatar",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .size(80.dp)
            .clip(CircleShape)
    )
}

Building a Request with ImageRequest

Passing a URL string is the quick path. For more control, build an ImageRequest. This lets you set options like crossfade, headers, transformations, and cache keys.

You need a Context, which Compose provides via LocalContext.current.

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade

@Composable
fun ProductImage(url: String) {
    val context = LocalContext.current
    val request = ImageRequest.Builder(context)
        .data(url)
        .crossfade(true)
        .build()

    AsyncImage(
        model = request,
        contentDescription = "Product photo"
    )
}

Crossfade for Smooth Loads

By default an image pops in instantly when it finishes loading, which can feel abrupt. crossfade(true) fades the image in over a short animation.

You can also pass a duration in milliseconds for a slower or faster fade. This small touch makes lists feel much more polished.

import coil3.request.ImageRequest
import coil3.request.crossfade
import android.content.Context

fun smoothRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .crossfade(durationMillis = 300)
        .build()

Loading from Different Sources

The model in AsyncImage is flexible. Coil can load from many source types, not just remote URLs:

  • A String or Uri for network or local files.
  • A drawable resource id (R.drawable.logo).
  • A File from the device.
  • A content Uri from the photo picker.

This means the same composable handles remote and local images seamlessly.

import android.net.Uri
import androidx.compose.runtime.Composable
import coil3.compose.AsyncImage

@Composable
fun FlexibleImage(source: Any) {
    // source can be a URL String, a Uri, a File, or a resource id
    AsyncImage(
        model = source,
        contentDescription = null
    )
}

// Examples:
// FlexibleImage("https://example.com/pic.jpg")
// FlexibleImage(Uri.parse("content://media/external/images/1"))

Reacting to Load State

Sometimes you want to know whether the image is loading, succeeded, or failed so you can show different UI. AsyncImagePainter exposes that state, but the simplest approach is SubcomposeAsyncImage, which lets you provide content per state.

We'll go deeper into placeholders next lesson; here is the shape of state-aware loading.

import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import coil3.compose.SubcomposeAsyncImage

@Composable
fun StatefulImage(url: String) {
    SubcomposeAsyncImage(
        model = url,
        contentDescription = "Photo",
        loading = { CircularProgressIndicator() }
    )
}

Cancellation Comes for Free

One of Coil's biggest wins is automatic cancellation. Because AsyncImage launches its request inside the composition, when the composable leaves the screen (for example a row scrolls out of a LazyColumn), the in-flight request is cancelled.

This saves bandwidth and CPU and prevents wasted work. You don't write any cancellation code yourself, which is why Coil scales so well in long lists.

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage

@Composable
fun PhotoFeed(urls: List<String>) {
    LazyColumn {
        items(urls) { url ->
            AsyncImage(
                model = url,
                contentDescription = null,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp)
            )
        }
    }
}

A Complete Image Card

Let's combine what we've learned: a card with a remote image that fills the top, uses crossfade, and crops to a fixed height. This is the bread-and-butter pattern you'll reuse constantly.

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade

@Composable
fun ImageCard(title: String, imageUrl: String) {
    val context = LocalContext.current
    Card(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Column {
            AsyncImage(
                model = ImageRequest.Builder(context)
                    .data(imageUrl)
                    .crossfade(true)
                    .build(),
                contentDescription = title,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxWidth().height(180.dp)
            )
            Text(text = title, modifier = Modifier.padding(16.dp))
        }
    }
}

Quick Check

Which composable from coil-compose is the simplest way to display a remote image by URL in Jetpack Compose?

Recap

You loaded your first remote images with Coil. Key takeaways:

  • Add coil-compose and an OkHttp network dependency, plus the INTERNET permission.
  • AsyncImage(model = url, contentDescription = ...) is the quick path.
  • Use ContentScale.Crop with a fixed size for avatars and thumbnails.
  • Build an ImageRequest for crossfade and more control.
  • Coil cancels in-flight requests automatically when composables leave the screen.

Next: making loading graceful with placeholders and error states.

Frequently asked questions

Is the “Loading Images with Coil” lesson free?

Yes — the full text of “Loading Images with Coil” 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 “Loading Images with Coil”?

Async image loading in Compose. 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 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Loading Images with Coil” 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