CPS251 Android Development by Scott Shaper

Image Loading

Imagine you're building a photo gallery app. You need to show hundreds of pictures, but if you try to load them all at once, your app will be slow and might crash. Just like a smart photo album that only shows the photos you're looking at, image loading libraries help your app display images efficiently. They handle downloading images from the internet, storing them temporarily, and showing them only when needed. This makes your app faster and uses less memory, which is especially important on mobile devices.

In this lesson, we'll learn how to use Coil (Coroutine Image Loader) to load and display images in your Android app. Coil is a popular library that makes it easy to load images from the internet, local storage, or resources, with automatic caching and memory management.

When to Use Image Loading

Key Image Loading Concepts

Concept What It Does When to Use It
AsyncImage Loads and displays images asynchronously When you need to display images from URLs or resources
rememberAsyncImagePainter Creates an image painter that loads images in the background When you need more control over image loading states
ImageRequest Defines how and where to load an image When you need to customize image loading (size, transformations)
ContentScale Defines how the image should be scaled to fit its container When you need to control how images fill their space
placeholder Shows an image while loading When you want to show something while the image downloads
error Shows an image when loading fails When you want to handle image loading errors gracefully
crossfade Creates a smooth fade transition when image loads When you want smooth visual transitions for better UX

Project Setup: Image Gallery App

Let's start by setting up our ImageGallery project. This will be a simple app that demonstrates how to load and display images using Coil.

Step 1: Adding Dependencies

First, we need to add the Coil dependency for image loading. We'll update the gradle/libs.versions.toml file:

[versions]
...
coil = "2.6.0"  # Add this line

[libraries]
...
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }  # Add this line

Then update the app/build.gradle.kts file to include the Coil dependency:

dependencies {
    ...
    implementation(libs.coil.compose)  // Add this line
    
}

Step 2: Adding Internet Permission

Since our app loads images from the internet, we need to add the internet permission to AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permission for internet access to load images -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        <!--manifest code here-->
    </application>
</manifest>

Step 3: Understanding the App Structure

Our ImageGallery app will have a simple structure with:

Step 4: Creating the Data Model

First, let's create our data model. Create a new file called ImageItem.kt:

package com.example.imagegallery

// Data class representing an image in our gallery
data class ImageItem(
    val id: String,
    val title: String,
    val imageUrl: String,
    val thumbnailUrl: String
)

What this does:

Step 5: Creating Sample Data

Now let's create sample data for our app. Create SampleImageData.kt:

package com.example.imagegallery

// Sample image data using Picsum Photos (a free image service)
object SampleImageData {
    
    fun getSampleImages(): List {
        return listOf(
            ImageItem(
                id = "1",
                title = "Sample Image 1",
                imageUrl = "https://picsum.photos/id/1/800/600",
                thumbnailUrl = "https://picsum.photos/id/1/200/200"
            ),
            ImageItem(
                id = "2",
                title = "Sample Image 2",
                imageUrl = "https://picsum.photos/id/2/800/600",
                thumbnailUrl = "https://picsum.photos/id/2/200/200"
            ),
            ImageItem(
                id = "3",
                title = "Sample Image 3",
                imageUrl = "https://picsum.photos/id/3/800/600",
                thumbnailUrl = "https://picsum.photos/id/3/200/200"
            ),
            ImageItem(
                id = "4",
                title = "Sample Image 4",
                imageUrl = "https://picsum.photos/id/4/800/600",
                thumbnailUrl = "https://picsum.photos/id/4/200/200"
            ),
            ImageItem(
                id = "5",
                title = "Sample Image 5",
                imageUrl = "https://picsum.photos/id/5/800/600",
                thumbnailUrl = "https://picsum.photos/id/5/200/200"
            ),
            ImageItem(
                id = "6",
                title = "Sample Image 6",
                imageUrl = "https://picsum.photos/id/6/800/600",
                thumbnailUrl = "https://picsum.photos/id/6/200/200"
            )
        )
    }
}

What this does:

Step 6: Creating a Simple Image Card

Now let's create a composable that displays an image card. Create ImageCard.kt:

package com.example.imagegallery

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun ImageCard(
    imageItem: ImageItem,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable(onClick = onClick),
        shape = RoundedCornerShape(8.dp)
    ) {
        Column {
            // Image
            AsyncImage(
                model = imageItem.thumbnailUrl,
                contentDescription = imageItem.title,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp),
                contentScale = ContentScale.Crop
            )
            
            // Image title
            Text(
                text = imageItem.title,
                style = MaterialTheme.typography.titleMedium,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

What this code does:

Why we use AsyncImage directly:

AsyncImage is the simplest and most reliable way to load images. It automatically handles all the complexity of loading, caching, and error handling for you. While you can use rememberAsyncImagePainter for more control over loading states, AsyncImage is perfect for most use cases and is what we use in this example.

Step 7: Creating the Image Gallery Screen

Now let's create the main screen that displays our image gallery. Create ImageGalleryScreen.kt:

package com.example.imagegallery

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ImageGalleryScreen(
    images: List,
    onImageClick: (ImageItem) -> Unit
) {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 50.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
    ) {
        items(images) { image ->
            ImageCard(
                imageItem = image,
                onClick = { onImageClick(image) }
            )
        }
    }
}

What this code does:

Why we use LazyColumn:

LazyColumn is essential for displaying lists efficiently. Unlike a regular Column, which would try to create all items at once (even if they're off-screen), LazyColumn only creates the items that are visible. This means:

Step 8: Creating a Full-Size Image Viewer

Let's create a screen that shows the full-size image when clicked. Create FullSizeImageScreen.kt:

package com.example.imagegallery

import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FullSizeImageScreen(
    imageItem: ImageItem,
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(imageItem.title) },
                navigationIcon = {
                    IconButton(onClick = onBackClick) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "Back"
                        )
                    }
                }
            )
        }
    ) { paddingValues ->
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(paddingValues),
            contentAlignment = Alignment.Center
        ) {
            AsyncImage(
                model = imageItem.imageUrl,
                contentDescription = imageItem.title,
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Fit
            )
        }
    }
}

What this code does:

Key difference from ImageCard:

Step 9: Updating MainActivity

Finally, let's update MainActivity.kt to use our image gallery:

package com.example.imagegallery

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.example.imagegallery.ui.theme.ImageGalleryTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ImageGalleryTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    ImageGalleryApp(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun ImageGalleryApp(modifier: Modifier = Modifier) {
    var selectedImage by remember { mutableStateOf(null) }
    val images = remember { SampleImageData.getSampleImages() }
    
    if (selectedImage == null) {
        ImageGalleryScreen(
            images = images,
            onImageClick = { image -> selectedImage = image }
        )
    } else {
        FullSizeImageScreen(
            imageItem = selectedImage!!,
            onBackClick = { selectedImage = null }
        )
    }
}

Understanding Each Part of the Implementation

How the App Flow Works:

  1. App Starts - ImageGalleryApp creates a list of images and shows ImageGalleryScreen
  2. Displaying the List - ImageGalleryScreen uses a LazyColumn to efficiently display all images as cards
  3. Image Cards - Each ImageCard shows a thumbnail (200x200) using AsyncImage with ContentScale.Crop
  4. User Clicks - When a user taps an image card, the onClick callback updates selectedImage state
  5. Showing Full Image - When selectedImage is not null, the app shows FullSizeImageScreen with the full-size image (800x600)
  6. Going Back - When the user clicks the back button, selectedImage is set to null, returning to the list

Image Loading with Coil:

Content Scale Options:

State Management:

How this example renders

Above is just a snippet of the code to view the full code, you need to go to my GitHub page and look at the chapter15 ImageGallery app.

image gallery single image

Tips for Success

Common Mistakes to Avoid

Best Practices