Unit 5 Pathway 2 Activity 3: Load and display images from the internet¶

Before you begin¶

Introduction¶

  • In previous codelabs, you learned how to get data from a web service using a repository pattern and parse the response into a Kotlin object. In this codelab, you build on that knowledge to load and display photos from a web URL. You also revisit how to build a LazyVerticalGrid and use it to display a grid of images on the overview page.

Prerequisites¶

  • Knowledge of how to retrieve JSON from a REST web service and the parsing of that data into Kotlin objects

  • Knowledge of a REST web service

  • Familiarity with Android architecture components, such as a data layer and repository

  • Knowledge of dependency injection

  • Knowledge of ViewModel and ViewModelProvider.Factory

  • Knowledge of coroutine implementation for your app

  • Knowledge of the repository pattern

What you’ll learn¶

  • How to use the Coil library to load and display an image from a web URL.

  • How to use a LazyVerticalGrid to display a grid of images.

  • How to handle potential errors as the images download and display.

What you’ll build¶

  • Modify the Mars Photos app to get the image URL from the Mars data, and use Coil to load and display that image.

  • Add a loading animation and error icon to the app.

  • Add status and error handling to the app.

What you’ll need¶

  • A computer with a modern web browser, such as the latest version of Chrome

  • Starter code for the Mars Photos app with REST web services

App overview¶

  • In this codelab, you continue working with the Mars Photos app from a previous codelab. The Mars Photos app connects to a web service to retrieve and display the number of Kotlin objects retrieved. These Kotlin objects contain the URLs of real-life photos from the Mars surface captured from NASA’s Mars Rovers.

    ../_images/unit5-pathway2-activity3-section2-a59e55909b6e9213_1440.png
  • In this codelab, the app will display Mars photos in a grid of images. The images are retrieved from the web service. The Coil library loads and displays the images. A LazyVerticalGrid creates the grid layout for the images. The app also handles network errors gracefully by displaying an error message.

    ../_images/unit5-pathway2-activity3-section2-68f4ff12cc1e2d81_1440.png

Solution code¶

Display a downloaded image¶

  • Displaying a photo from a web URL might sound straightforward, but there is quite a bit of engineering to make it work well. The image has to be downloaded, cached, and decoded from its compressed format to an image that Android can use. You can cache the image to an in-memory cache, a storage-based cache, or both. All this has to happen in low-priority background threads, so the UI remains responsive. Also, for the best network and CPU performance, it’s best to fetch and decode more than one image at once.

  • A community-developed library called Coil simplifies this process. Without the use of Coil, much more work has to be done.

  • Coil needs two things:

    • The URL of the image you want to load and display.

    • An AsyncImage composable to actually display that image.

  • Displaying a single image from the Mars web service looks like this:

    ../_images/unit5-pathway2-activity3-section3-1b670f284109bbf5_1440.png

Coil dependency¶

  • In build.gradle.kts (Module :app), in the dependencies section, this line is added for Coil:

    // Coil
    implementation("io.coil-kt:coil-compose:2.4.0")
    

The AsyncImage composable¶

  • The AsyncImage composable loads and displays a single photo. AsyncImage is a composable that executes an image request asynchronously, and renders the result. Example:

    AsyncImage(
        model = "https://android.com/sample_image.jpg",
        contentDescription = null
    )
    
  • The model argument is either an ImageRequest object, or its ImageRequest.data. In the preceding example, the ImageRequest.data value is used, which is the image URL https://android.com/sample_image.jpg. The following example code shows how to assign an ImageRequest to model

    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data("https://example.com/image.jpg")
            .crossfade(true)
            .build(),
        placeholder = painterResource(R.drawable.placeholder),
        contentDescription = stringResource(R.string.description),
        contentScale = ContentScale.Crop,
        modifier = Modifier.clip(CircleShape)
    )
    
  • AsyncImage supports the same arguments as the standard Image composable. Additionally, it supports setting placeholder / error / fallback painters and onLoading / onSuccess / onError callbacks. The preceding example code loads the image with a circle crop and crossfade and sets a placeholder.

  • contentDescription sets the text used by accessibility services to describe what this image represents.

  • In ui/screens/HomeScreen.kt, the composable function called MarsPhotoCard() uses AsyncImage() and displays only a single image:

    @Composable
    fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
        Card(
            modifier = modifier,
            shape = MaterialTheme.shapes.medium,
            elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
        ) {
            AsyncImage(
                model = ImageRequest.Builder(context = LocalContext.current).data(photo.imgSrc)
                    .crossfade(true).build(),
                error = painterResource(R.drawable.ic_broken_image),
                placeholder = painterResource(R.drawable.loading_img),
                contentDescription = stringResource(R.string.mars_photo),
                contentScale = ContentScale.Crop, // Fill the available space on screen
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
    
    • crossfade(true) enables a crossfade animation when the request completes successfully.

Loading and error images¶

  • To improve the user experience, show a placeholder image while loading the image. You can also display an error image if the loading fails due to an issue, such as a missing or corrupt image file.

  • Open res/drawable/ic_broken_image.xml and click the Design or Split tab on the right. It’s a broken-image icon that’s available in the built-in icon library. This can be used for the error image. This vector drawable uses the android:tint attribute to color the icon gray.

    ../_images/unit5-pathway2-activity3-section3-70e008c63a2a1139_1440.png
  • Open res/drawable/loading_img.xml. This drawable is an animation that rotates an image drawable, loading_img.xml, around the center point. (You don’t see the animation in the preview.)

    ../_images/unit5-pathway2-activity3-section3-92a448fa23b6d1df_1440.png
  • Error and placeholder attributes can be added to AsyncImage as shown:

    // ...
    AsyncImage(
        // ...
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        // ...
    )
    
  • This code sets the placeholder loading image to use while loading (the loading_img drawable). It also sets the image to use if image loading fails (the ic_broken_image drawable).

Display a grid of images with a LazyVerticalGrid¶

  • MarsPhotoCard displays a single image. To display a grid of images, use a LazyVerticalGrid with a Grid layout manager.

Lazy grids¶

  • The LazyVerticalGrid and LazyHorizontalGrid composables provide support to display items in a grid. A lazy vertical grid displays its items in a vertically scrollable container, spanned across multiple columns, while a lazy horizontal grid has the same behavior on the horizontal axis.

    ../_images/unit5-pathway2-activity3-section4-27680e208333ed5_1440.png
  • From a design perspective, Grid Layout is best for displaying Mars photos as icons or images.

  • The columns parameter in LazyVerticalGrid and rows parameter in LazyHorizontalGrid control how cells are formed into columns or rows. The following example code displays items in a grid, using GridCells.Adaptive to set each column to be at least 128.dp wide:

    // Sample code, not for the app
    
    @Composable
    fun PhotoGrid(photos: List<Photo>) {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = 150.dp)
        ) {
            items(photos) { photo ->
                PhotoItem(photo)
            }
        }
    }
    
  • LazyVerticalGrid lets you specify a width for items, and the grid then fits as many columns as possible. After calculating the number of columns, the grid distributes any remaining width equally among the columns. This adaptive way of sizing is especially useful for displaying sets of items across different screen sizes.

    Note

    If you know the exact amount of columns to be used, you can instead provide an instance of GridCells.Fixed containing the number of required columns.

  • In this codelab, to display Mars photos, you use the LazyVerticalGrid composable with GridCells.Adaptive, with each column set to 150.dp wide.

Use LazyVerticalGrid¶

  • In HomeScreen.kt the PhotosGridScreen() composable takes a list of MarsPhoto and a modifier as arguments, and displays a grid.

    @Composable
    fun PhotosGridScreen(
        photos: List<MarsPhoto>,
        modifier: Modifier = Modifier,
        contentPadding: PaddingValues = PaddingValues(0.dp),
    ) {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(150.dp),
            modifier = modifier.padding(horizontal = 4.dp),
            contentPadding = contentPadding,
        ) {
            items(items = photos, key = { photo -> photo.id }) { photo ->
                MarsPhotoCard(
                    photo,
                    modifier = Modifier
                        .padding(4.dp)
                        .fillMaxWidth()
                        .aspectRatio(1.5f)
                )
            }
        }
    }
    
    • Inside the LazyVerticalGrid lambda, items() takes in the list of MarsPhoto and an item key as photo.id.

    • When the user scrolls through the grid (a LazyRow within a LazyColumn), the list item position changes. However, due to an orientation change or if the items are added or removed, the user can lose the scroll position within the row. Item keys help you maintain the scroll position based on the key.

    • By providing keys, you help Compose handle reorderings correctly. For example, if your item contains a remembered state, setting keys allows Compose to move this state together with the item when its position changes.

Conclusion¶

  • Congratulations on completing the Mars Photos app! It’s time to show off your app with real life Mars pictures to your family and friends.