Techbase Academy
Android · Lesson 31

Load and Display Images from the Internet with Coil

11 phasesRemote GalleryAndroid Studio
Phase 1 of 11
Lesson Introduction

Text is easy. Pictures are where networking gets interesting: a photo can be a few megabytes, it must be downloaded without freezing the screen, shrunk to fit a small card, kept in a cache so the next visit is instant, and replaced by a friendly placeholder when the network fails. Writing all that yourself takes weeks. A library called Coil does it in one composable.

In this lesson you will use Coil's AsyncImage to show remote images, handle loading and error states, understand caching and memory, and build the two apps that close Pathway 2 of Unit 5: the Amphibians practice app and the Bookshelf project.

This lesson covers three items from the official Android Basics with Compose course: Load and display images from the internet, Practice: Build amphibians app and Project: Create a Bookshelf app.

By the end you will be able to:

  • Add Coil 3 to a project and explain why it needs a separate network artifact.
  • Display a remote image with AsyncImage, with a placeholder, an error image and a content description.
  • Use ImageRequest for crossfade and other options.
  • Explain memory and disk caching, and image sizing.
  • Build a grid of remote images with loading and error states.
  • Plan a Bookshelf app from a public book-search API, taking care with API keys, terms and data.

Big idea: an image on screen is a URL in your data, plus a library that turns it into pixels at the right size.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts
  • Retrofit, @Serializable, sealed UI state (lesson 29).
  • Repositories and the app container (lesson 30).
  • Lazy grids (lesson 21).
  • contentDescription and ContentScale (lesson 21).
  • The INTERNET permission (lesson 29). Coil downloads images, so you still need it.
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: What an Image Loader Does

When you ask Coil to display https://example.com/photo.jpg in a 120 dp card, it:

  1. Checks the memory cache: has this exact image already been decoded at this size? If yes, show it immediately.
  2. Otherwise checks the disk cache: has the file been downloaded before? If yes, decode it.
  3. Otherwise downloads it over the network, on a background thread.
  4. Decodes it to a bitmap, scaled down to the size of the target, not the full resolution.
  5. Caches it and shows it.
  6. If you leave the screen while it loads, the work is cancelled.
text
AsyncImage(url)
    |
    v
[memory cache?] --hit--> show
    | miss
[disk cache?]   --hit--> decode --> show
    | miss
[network download] --> decode (downsampled) --> cache --> show
    | fails
show error image

Why size matters

A 4,000 x 3,000 pixel photo uses about 48 MB of memory when decoded (4,000 x 3,000 x 4 bytes). Ten of them in a list would crash a budget phone with little RAM. Coil decodes images at roughly the size they will be shown, so a 120 dp card gets a small bitmap. That is a major reason to use a library instead of loading bitmaps by hand.

Coil 3 package changes

Coil 3 is a rewrite that supports Kotlin Multiplatform. If you read older tutorials you will see Coil 2 code: be aware of the differences.

Coil 2 Coil 3
Group ID io.coil-kt io.coil-kt.coil3
Compose artifact io.coil-kt:coil-compose io.coil-kt.coil3:coil-compose
Package for AsyncImage coil.compose coil3.compose
Network loading Built in Separate artifact, for example coil-network-okhttp

The last row is the one that trips people up: in Coil 3, the base library no longer includes networking, so a URL will fail to load unless you add a network artifact.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: What an Image Loader Does. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: Add Coil

Look up the latest stable Coil 3 version on the Coil releases page (https://github.com/coil-kt/coil/releases) and add:

libs.versions.toml
[versions]
coil = "<latest stable 3.x>"
[libraries]
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
build.gradle.kts
dependencies {
 implementation(libs.coil.compose)
 implementation(libs.coil.network.okhttp)
}

Sync, and make sure the INTERNET permission is in your manifest. The two artifacts must use the same version.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Add Coil. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: Your First `AsyncImage`
kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.unit.dp
import coil3.compose.AsyncImage
@Composable
fun RemotePhoto(url: String, caption: String) {
 Column {
 AsyncImage(
 model = url,
 contentDescription = caption,
 contentScale = ContentScale.Crop,
 modifier = Modifier
 .fillMaxWidth()
 .height(200.dp)
 )
 Text(caption)
 }
}
  • model is what to load. A String URL is the simplest. It can also be a Uri, a File, a drawable resource ID or an ImageRequest.
  • contentDescription is read by screen readers; describe the picture meaningfully (lesson 47).
  • contentScale works like ImageView.scaleType: Crop fills and trims, Fit shows the whole picture with gaps.
  • Always give the image a size (a fixed height or aspectRatio). Without one, the layout jumps when the image arrives.

Placeholders and error images

While loading, show something; if it fails, show something else. Pass painters:

kotlin
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage
AsyncImage(
 model = url,
 contentDescription = caption,
 placeholder = painterResource(R.drawable.ic_loading),
 error = painterResource(R.drawable.ic_broken_image),
 fallback = painterResource(R.drawable.ic_no_image),
 contentScale = ContentScale.Crop,
 modifier = Modifier.height(200.dp)
)
Parameter When it is shown
placeholder While the request is loading
error When the request fails (no network, 404, undecodable)
fallback When model is null

Create ic_loading, ic_broken_image and ic_no_image as vector assets (File > New > Vector Asset, lesson 21), for example using the built-in "image" and "broken image" icons.

Controlling the request

For more options, build an ImageRequest:

kotlin
import androidx.compose.ui.platform.LocalContext
import coil3.request.ImageRequest
import coil3.request.crossfade
AsyncImage(
 model = ImageRequest.Builder(LocalContext.current)
 .data(url)
 .crossfade(true)
 .build(),
 contentDescription = caption,
 modifier = Modifier.height(200.dp)
)

crossfade(true) fades the image in. In Coil 3 the crossfade builder function is an extension in coil3.request, so it needs its own import; if the compiler cannot find a builder method you expect, check the current Coil documentation, because the library is actively developed and some names have moved between 2.x and 3.x.

Observing loading state

AsyncImage also accepts onLoading, onSuccess and onError callbacks, which is useful for logging or for showing a progress indicator:

kotlin
AsyncImage(
 model = url,
 contentDescription = caption,
 onError = { error -> Log.e("Coil", "Failed: ${error.result.throwable}") }
)

For fully custom loading UI there is also SubcomposeAsyncImage with loading and error slots that accept composables. It is more flexible but heavier, because it composes the slots separately, so avoid it in big grids; simple painters are cheaper.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Your First `AsyncImage`. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: The Amphibians App

The official Practice: Build amphibians app asks you to build an app that fetches a list of amphibians (name, type, description, image URL) from a service and shows them in a scrolling list of cards. We will build the same architecture using the pieces from lessons 29 and 30. The data shape:

json
[
 {
 "name": "Great Basin Spadefoot",
 "type": "Toad",
 "description": "This toad spends most of its life underground.",
 "img_src": "https://example.com/images/spadefoot.png"
 }
]

The Android Developers course uses a small sample server for this. Servers for training courses can change or disappear, so open the practice page in the official pathway for the current address, or point the app at any JSON service of your own with the same shape (even a static amphibians.json file hosted on GitHub Pages works, and costs nothing).

Model

Amphibian.kt
package com.example.amphibians.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Amphibian(
 val name: String,
 val type: String,
 val description: String,
 @SerialName("img_src") val imgSrc: String
)

Service, repository, container

These follow lesson 30 exactly:

kotlin
interface AmphibiansApiService {
 @GET("amphibians")
 suspend fun getAmphibians(): List<Amphibian>
}
interface AmphibiansRepository {
 suspend fun getAmphibians(): List<Amphibian>
}
class NetworkAmphibiansRepository(
 private val api: AmphibiansApiService
) : AmphibiansRepository {
 override suspend fun getAmphibians(): List<Amphibian> = api.getAmphibians()
}

with retrofit2.http.GET imported and a container that builds Retrofit with the base URL you chose (ending in /).

ViewModel and state

kotlin
sealed interface AmphibiansUiState {
 data class Success(val amphibians: List<Amphibian>) : AmphibiansUiState
 data object Error : AmphibiansUiState
 data object Loading : AmphibiansUiState
}

The ViewModel is the same as PostsViewModel, with the new repository type and factory.

The screen

AmphibiansScreen.kt
package com.example.amphibians.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.example.amphibians.R
import com.example.amphibians.model.Amphibian
@Composable
fun AmphibianCard(amphibian: Amphibian, modifier: Modifier = Modifier) {
 Card(modifier = modifier) {
 Column {
 Text(
 text = "${amphibian.name} (${amphibian.type})",
 style = MaterialTheme.typography.titleLarge,
 modifier = Modifier.padding(16.dp)
 )
 AsyncImage(
 model = amphibian.imgSrc,
 contentDescription = amphibian.name,
 placeholder = painterResource(R.drawable.ic_loading),
 error = painterResource(R.drawable.ic_broken_image),
 contentScale = ContentScale.Crop,
 modifier = Modifier.fillMaxWidth().height(194.dp)
 )
 Text(
 text = amphibian.description,
 style = MaterialTheme.typography.bodyMedium,
 modifier = Modifier.padding(16.dp)
 )
 }
 }
}
@Composable
fun AmphibiansList(amphibians: List<Amphibian>, modifier: Modifier = Modifier) {
 LazyColumn(
 modifier = modifier,
 contentPadding = PaddingValues(16.dp),
 verticalArrangement = Arrangement.spacedBy(16.dp)
 ) {
 items(amphibians, key = { it.name }) { AmphibianCard(it) }
 }
}

The when over the sealed state, the retry button and the ViewModel factory are the same as in lessons 29 and 30. Resist copy-pasting without thinking: re-type the ViewModel yourself and notice each line you recognise.

Notice that key = { it.name } assumes names are unique. If the service could return two items with the same name, the app would crash on scroll. Choose a key you know is unique, or omit the key.

Run it with the device online, then in airplane mode: the first run shows images; the second shows the error state for the list (if the JSON cannot load) or the broken-image icon for images that fail (if the JSON came from cache but the pictures did not).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: The Amphibians App. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Caching, Memory and Data Use

By default Coil keeps a memory cache (a fraction of the app's available memory) and a disk cache (a limited size in the app's cache directory), and respects the server's HTTP caching headers. That means:

  • Scrolling back to a card you have seen is instant.
  • A second launch of the app reuses downloaded files, saving the user's mobile data, which is a real cost for many users.
  • The system may delete cache files when storage is low. A cache is a convenience, never a place for important data.

You can customise this with a global ImageLoader, for example to set the disk cache size or add logging, by creating the loader in your Application class and passing it to Coil's singleton setup. The defaults are good for most apps, so start there. Check the Coil documentation for the current API, as setup differs between Coil 2 and 3.

Memory tips:

  • Give AsyncImage a bounded size so Coil can downsample.
  • Avoid putting huge full-resolution pictures in LazyColumn items if the server offers thumbnails. Many APIs let you request a smaller version, for example via a size parameter in the URL.
  • Test on a low-RAM device or emulator profile before shipping.
✏️ Your Task
Practise what you just learned about Caching, Memory and Data Use. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Project Planning: The Bookshelf App

The unit's project is Bookshelf. The brief: a user searches for books by keyword and sees a grid of cover images; tapping one shows details (title, authors, description, link). The data comes from a public book-search API.

Plan it properly before coding:

Decision Guidance
Which API? Choose a public API with a free tier whose terms allow your use. The course uses the Google Books API. Read its documentation and terms yourself before you start, and never guess endpoints or field names.
Authentication Some endpoints need an API key. Do not commit keys to a public repository, and do not assume a key inside an APK is secret. For a learning project use a key restricted to your app, store it in local.properties and read it into BuildConfig, and understand that a determined user can still extract it.
Architecture Service, repository, container, ViewModel, sealed UI state, exactly as lesson 30.
Screens Search screen with a text field and a LazyVerticalGrid of covers; a detail screen (navigation from lesson 26).
Covers over HTTPS Some APIs return http:// image links; they will not load, because cleartext traffic is blocked. Convert to https:// where the host supports it.
Empty and error states No results, no network, rate limited (HTTP 429). Handle each.
Tests Fake repository, ViewModel tests, one UI test.

Treat cover images and book text as third-party content: show attribution where the API requires it.

✏️ Your Task
Practise what you just learned about Project Planning: The Bookshelf App. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Grid with shimmer-free placeholders

Change the Amphibians list to a two-column LazyVerticalGrid of square cards showing only the image and the name. Use AsyncImage with ContentScale.Crop and aspectRatio(1f). Provide a placeholder and an error painter.

kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
@Composable
fun AmphibiansGrid(amphibians: List<Amphibian>, modifier: Modifier = Modifier) {
 LazyVerticalGrid(
 columns = GridCells.Fixed(2),
 modifier = modifier,
 contentPadding = PaddingValues(8.dp),
 horizontalArrangement = Arrangement.spacedBy(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(amphibians) { a ->
 Card {
 Box {
 AsyncImage(
 model = a.imgSrc,
 contentDescription = a.name,
 placeholder = painterResource(R.drawable.ic_loading),
 error = painterResource(R.drawable.ic_broken_image),
 contentScale = ContentScale.Crop,
 modifier = Modifier.fillMaxWidth().aspectRatio(1f)
 )
 Text(
 a.name,
 modifier = Modifier.align(Alignment.BottomStart).padding(8.dp)
 )
 }
 }
 }
 }
}

aspectRatio(1f) fixes the height from the width so the grid does not jump as images load. Text laid directly over a photo can be hard to read; a real design adds a scrim (a semi-transparent background) behind the text for contrast (lesson 46).

🎯 Your Challenge

Exercise 2: Handle HTTP image links

A service returns image URLs starting with http://. Images silently fail to load. Explain why and write a small helper that upgrades the link, then discuss the risk.

Android blocks cleartext (unencrypted) HTTP traffic by default for modern apps, so the request fails and the error painter appears. A helper can rewrite the scheme:

kotlin
fun String.toHttps(): String =
 if (startsWith("http://")) replaceFirst("http://", "https://") else this

and use model = amphibian.imgSrc.toHttps(). The risk: not every host serves the same resource over HTTPS, so some links will still fail and you should show the error painter. Do not disable the cleartext protection for the whole app to work around one server; that weakens security for every request (anyone on the network can read or alter the traffic).

✏️ Your Task

Turn on airplane mode after your app has loaded the list once, force-stop and reopen it, then scroll. Which images still show, and why? Write down what you learned about the memory and disk caches.

Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Coil 2 imports with Coil 3 artifacts. Symptom: Unresolved reference: coil.compose. The fix: Coil 3 uses coil3.compose.AsyncImage; make sure the dependency group is io.coil-kt.coil3.

Mistake #2: No network artifact. Symptom: images stay on the placeholder or error forever and Logcat mentions no fetcher for the URL. The fix: add coil-network-okhttp with the same version as coil-compose.

Mistake #3: Missing INTERNET permission. Symptom: every image fails. The fix: add the permission (lesson 29).

Mistake #4: Layout jumps. What happens: images pop in and push content down. The fix: give AsyncImage a fixed height or aspectRatio.

Mistake #5: Empty or missing contentDescription. What happens: screen-reader users get nothing. The fix: a meaningful description, or null only for purely decorative images.

Mistake #6: Duplicate keys in items. Symptom: a crash on scroll saying a key was already used. The fix: a unique key, or none.

Mistake #7: http:// URLs. The fix: use https (Exercise 2), never disable cleartext protection globally.

Mistake #8: Using SubcomposeAsyncImage everywhere. What happens: slow scrolling in big grids. The fix: use AsyncImage with painters.

Mistake #9: Committing API keys. What happens: a key in a public repo is found by bots in minutes. The fix: keep it out of source control, restrict it, and prefer a server for anything truly secret.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

Question 1. What does an image-loading library do that you would not want to write yourself?

Background downloading, decoding at the target size, memory and disk caching, cancelling requests when the UI leaves the screen, and showing placeholders and error states.

Question 2. Why does Coil 3 need two dependencies to load a URL?

The Compose artifact displays images but networking was split into separate artifacts; coil-network-okhttp supplies the HTTP fetcher. Without it, URLs cannot be loaded.

Question 3. When are placeholder, error and fallback used?

placeholder while loading, error when loading fails, and fallback when the model is null.

Question 4. Why give AsyncImage a fixed size or aspect ratio?

So the layout does not shift when the image finishes loading, and so Coil can decode at the right size, saving memory.

Question 5. Why should an API key in an app not be considered secret?

Anything inside the APK can be extracted by a determined person. Real secrets belong on a server; keys that must be in the app should be restricted and kept out of source control.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Remote Gallery

Project goal: A photo gallery that loads image URLs from a JSON file you host yourself (for example on GitHub Pages) and shows them in an adaptive grid, with loading, error and empty states.

Requirements:

  1. A JSON file with at least eight entries: title, imageUrl, credit. Use images you own or that are licensed for reuse.
  2. Retrofit service, repository, container and ViewModel as in lessons 29 and 30.
  3. A LazyVerticalGrid with GridCells.Adaptive(minSize = 150.dp); each cell is an AsyncImage with placeholder and error painters and a meaningful description.
  4. A sealed UI state with loading, success, empty and error; a retry button.
  5. Tapping an image opens a full-screen view (a dialog or a second destination) showing the credit.
  6. A fake repository and a ViewModel test for the empty and error states.

Stretch goal: show Image placeholders with a pulsing alpha using rememberInfiniteTransition (lesson 41).

GalleryScreen.kt
package com.example.gallery.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.example.gallery.R
import com.example.gallery.model.GalleryItem
sealed interface GalleryUiState {
 data object Loading : GalleryUiState
 data object Empty : GalleryUiState
 data class Success(val items: List<GalleryItem>) : GalleryUiState
 data class Error(val message: String) : GalleryUiState
}
@Composable
fun GalleryScreen(state: GalleryUiState, onRetry: () -> Unit, modifier: Modifier = Modifier) {
 when (state) {
 GalleryUiState.Loading -> Column(
 modifier.fillMaxSize(),
 verticalArrangement = Arrangement.Center,
 horizontalAlignment = Alignment.CenterHorizontally
 ) { CircularProgressIndicator() }
 GalleryUiState.Empty -> Column(
 modifier.fillMaxSize().padding(24.dp),
 verticalArrangement = Arrangement.Center,
 horizontalAlignment = Alignment.CenterHorizontally
 ) { Text("No photos yet.") }
 is GalleryUiState.Error -> Column(
 modifier.fillMaxSize().padding(24.dp),
 verticalArrangement = Arrangement.Center,
 horizontalAlignment = Alignment.CenterHorizontally
 ) {
 Text(state.message)
 Button(onClick = onRetry, modifier = Modifier.padding(top = 12.dp)) { Text("Retry") }
 }
 is GalleryUiState.Success -> GalleryGrid(state.items, modifier)
 }
}
@Composable
fun GalleryGrid(items: List<GalleryItem>, modifier: Modifier = Modifier) {
 var opened by rememberSaveable { mutableStateOf<String?>(null) } // image URL
 val openedItem = items.firstOrNull { it.imageUrl == opened }
 LazyVerticalGrid(
 columns = GridCells.Adaptive(minSize = 150.dp),
 modifier = modifier,
 contentPadding = PaddingValues(8.dp),
 horizontalArrangement = Arrangement.spacedBy(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(items, key = { it.imageUrl }) { item ->
 AsyncImage(
 model = item.imageUrl,
 contentDescription = item.title,
 placeholder = painterResource(R.drawable.ic_loading),
 error = painterResource(R.drawable.ic_broken_image),
 contentScale = ContentScale.Crop,
 modifier = Modifier
 .fillMaxWidth()
 .aspectRatio(1f)
 .clickable { opened = item.imageUrl }
 )
 }
 }
 if (openedItem != null) {
 AlertDialog(
 onDismissRequest = { opened = null },
 confirmButton = { TextButton(onClick = { opened = null }) { Text("Close") } },
 title = { Text(openedItem.title) },
 text = {
 Column {
 AsyncImage(
 model = openedItem.imageUrl,
 contentDescription = openedItem.title,
 modifier = Modifier.fillMaxWidth().aspectRatio(1f)
 )
 Text("Credit: ${openedItem.credit}", modifier = Modifier.padding(top = 8.dp))
 }
 }
 )
 }
}

Notes: the ViewModel maps an empty successful response to Empty and failures to Error, with two ViewModel tests using the fake repository: emptyList() gives Empty; a thrown IOException gives Error. The selected image is stored by URL in rememberSaveable (a String fits in a Bundle), which survives rotation. The dialog uses AsyncImage again, so the image is served from Coil's cache and appears instantly.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 31 project”.

3

On github.com click New repository, name it android-lesson-31-load-and-display-images-from-the-internet, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 31 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: What an Image Loader Does
  • Step-by-Step Build 1: Add Coil
  • Step-by-Step Build 2: Your First `AsyncImage`
  • Step-by-Step Build 3: The Amphibians App
  • Caching, Memory and Data Use
  • Project Planning: The Bookshelf App
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close