Unit 3 Pathway 2 Activity 2: Add a scrollable list¶

Before you begin¶

  • In this codelab, you learn how to make a scrollable list in your app using Jetpack Compose.

  • You will be working with the Affirmations app, which displays a list of affirmations paired with beautiful images to bring positivity to your day!

  • The data is already there, all you need to do is take that data and display it in the UI.

Prerequisites¶

  • Familiarity with lists

  • Experience building layouts

  • Experience running apps on a device or emulator

What you’ll learn¶

  • How to create a material design card using Jetpack Compose

  • How to create a scrollable list using Jetpack Compose

What you’ll build¶

  • You will take an existing application and add a scrollable list to the UI

  • The finished product will look like this:

    ../_images/unit3-pathway2-activity2-section1-286f5132aa155fa6_1440.png

What you’ll need¶

  • A computer with internet access, a web browser, and Android Studio

  • GitHub access

Download the starter code¶

Create a list item data class¶

Create a data class for an Affirmation¶

  • In Android apps, lists are made up of list items. For single pieces of data, this could be something simple like a string or an integer. For list items that have multiple pieces of data, like an image and text, you will need a class that contains all of these properties.

  • Data class: a class that only contains properties, and possibly some utility methods to work with those properties.

  • Create a new package under com.example.affirmations

    ../_images/unit3-pathway2-activity2-section2-89c8d8485c685fac_1440.png
  • Packages are folders that contain classes and other folders.

  • Name the new package model. The model package will contain the data model that will be represented by a data class. The data class will contain 2 properties that are related to an “Affirmation”: a string resource, and an image resource.

    ../_images/unit3-pathway2-activity2-section2-b54fb6bf57de44c8_1440.png
  • Create a new class in the com.example.affirmations.model package.

    ../_images/unit3-pathway2-activity2-section2-58510a651bd49100_1440.png
  • Name the new class Affirmation and make it a Data class.

    ../_images/unit3-pathway2-activity2-section2-7f94b65ee3d8407f_1440.png
  • An Affirmation consists of one image and one string. In Affirmation.kt:

    data class Affirmation(
        @DrawableRes val imageResourceId: Int
        @StringRes val stringResourceId: Int,
    )
    

    imports

    import androidx.annotation.DrawableRes
    import androidx.annotation.StringRes
    
    • imageResourceId : ID of the affirmation image stored in a drawable resource

    • stringResourceId : ID of the affirmaiton text stored in a string resource

  • In the com.example.affirmations.data package, open Datasource.kt and uncomment the two import statements and the contents of the Datasource class. In Datasource.kt:

    import com.example.affirmations.R
    import com.example.affirmations.model.Affirmation
    
    class Datasource() {
        fun loadAffirmations(): List<Affirmation> {
            return listOf<Affirmation>(
                Affirmation(R.string.affirmation1, R.drawable.image1),
                Affirmation(R.string.affirmation2, R.drawable.image2),
                Affirmation(R.string.affirmation3, R.drawable.image3),
                Affirmation(R.string.affirmation4, R.drawable.image4),
                Affirmation(R.string.affirmation5, R.drawable.image5),
                Affirmation(R.string.affirmation6, R.drawable.image6),
                Affirmation(R.string.affirmation7, R.drawable.image7),
                Affirmation(R.string.affirmation8, R.drawable.image8),
                Affirmation(R.string.affirmation9, R.drawable.image9),
                Affirmation(R.string.affirmation10, R.drawable.image10))
        }
    }
    
  • loadAffirmations() gathers all the affirmations provided in the starter code, and returns them as a list. This will be used later to build the scrollable list.

Add a list to your app¶

Create a list item card¶

  • This app is meant to display a list of affirmations. The first step is to create a list item for each affirmation.

  • The item will be comprised of a Card composable, containing an Image and a Text composable. A Card displays content and actions. The Affirmation card will look like this in the preview:

    ../_images/unit3-pathway2-activity2-section3-4f657540712a069f_1440.png
  • The card shows an image with some text beneath it. This vertical layout can be achieved using a Column composable wrapped in a Card composable. Follow the steps below to achieve this.

  • Open MainActivity.kt.

  • Create two new composables, one for the app, and one for the card.

  • Under AffirmationsApp(), create a new AffirmationCard() composable. It is a best practice to pass a modifier to every composable and set it to a default value like Modifier.

    @Composable
    fun AffirmationsApp() {
    }
    
    @Composable
    fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
    
    }
    

    imports

    import com.example.affirmations.model.Affirmation
    
  • Inside AffirmationCard(), call Card(), and pass modifier to it, since Card is the root element.

    @Composable
    fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
        Card(modifier = modifier) {
    
        }
    }
    

    imports

    import androidx.compose.material3.Card
    
  • Inside Card, add a Column. This makes the affirmation image and text to be stacked vertically.

    @Composable
    fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
        Card(modifier = modifier) {
            Column {
    
            }
        }
    }
    

    imports

    import androidx.compose.foundation.layout.Column
    
  • Inside Column, add an Image.

    @Composable
    fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
        Card(modifier = modifier) {
            Column {
                Image(
                    painter = painterResource(affirmation.imageResourceId),
                    contentDescription = stringResource(affirmation.stringResourceId),
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(194.dp),
                    contentScale = ContentScale.Crop
                )
            }
        }
    }
    

    imports

    import androidx.compose.foundation.Image
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.height
    import androidx.compose.ui.layout.ContentScale
    import androidx.compose.ui.res.painterResource
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    
    • fillMaxWidth(): makes the Image occupy the full width of its container, in this case stretching it horizontally to fill the Column.

    • height(194.dp): sets a fixed height for the Image to 194 density-independent pixels (dp). Ensures all images in the affirmation cards have a consistent height, regardless of their original sizes.

    • contentScale = ContentScale.Crop: scales the image to cover the entire target area. If the aspect ratio doesn’t match the target area, may result in parts of the image being cropped.

  • After Image, add a Text composable.

    @Composable
    fun AffirmationCard(affirmation: Affirmation, modifier: Modifier = Modifier) {
        Card(modifier = modifier) {
            Column {
                Image(
                    painter = painterResource(affirmation.imageResourceId),
                    contentDescription = stringResource(affirmation.stringResourceId),
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(194.dp),
                    contentScale = ContentScale.Crop
                )
                Text(
                    text = LocalContext.current.getString(affirmation.stringResourceId),
                    modifier = Modifier.padding(16.dp),
                    style = MaterialTheme.typography.headlineSmall
                )
            }
        }
    }
    

    imports

    import androidx.compose.material3.Text
    import androidx.compose.foundation.layout.padding
    import androidx.compose.ui.platform.LocalContext
    
    • LocalContext.current.getString(affirmation.stringResourceId): retrieves the string with the ID affirmation.stringResourceId

    • Modifier.padding(16.dp): adds padding around the text

    • MaterialTheme.typography.headlineSmall: sets the text style to a smaller headline

Preview the AffirmationCard composable¶

  • In MainActivity.kt, create a private method called AffirmationCardPreview():

    import androidx.compose.ui.tooling.preview.Preview
    
    @Preview
    @Composable
    private fun AffirmationCardPreview() {
        AffirmationCard(Affirmation(R.string.affirmation1, R.drawable.image1))
    }
    
  • Open the Split tab and you will see a preview of the AffirmationCard. If necessary, click Build & Refresh in the Design pane to display the preview.

    ../_images/unit3-pathway2-activity2-section3-924a4df2c1db236c_1440.png

Create the list¶

  • Create list items first, then use them to make the list component.

  • Create a function called AffirmationList():

    @Composable
    fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
    
    }
    
  • A scrollable list can be made using the LazyColumn composable. The difference between a LazyColumn and a Column is that a Column loads all the items at once, and should be used when you have only a small number of items to display. A Column can only hold a predefined, or fixed, number of composables.

  • A LazyColumn can add content on demand, which makes it good for long lists and particularly when the length of the list is unknown. It’s suitable for apps like Instagram. A LazyColumn also provides scrolling by default, without additional code.

  • Declare a LazyColumn composable inside AffirmationList(). Since LazyColumn is the root element, pass modifier to it.

    @Composable
    fun AffirmationList(affirmationList: List<Affirmation>, modifier: Modifier = Modifier) {
        LazyColumn(modifier = modifier) {
            items(affirmationList) { affirmation ->
                AffirmationCard(
                    affirmation = affirmation,
                    modifier = Modifier.padding(8.dp)
                )
            }
        }
    }
    

    imports

    import androidx.compose.foundation.lazy.items
    import androidx.compose.foundation.lazy.LazyColumn
    
    • The items() method adds items to the LazyColumn. This method is somewhat unique to this composable, not a common practice for most composables.

    • items() takes a lambda function as an argument. The function takes one list item (an affirmation) as input, and calls AffirmationCard() to display that affirmation.

Display the list¶

  • The layout direction defines whether items should be laid out from left to right or right to left. Each user has their own setting, which can be retrieved using LocalLayoutDirection.current.

  • In AffirmationsApp, save the current layout direction in a variable. This will be used to configure the padding later.

    import com.example.affirmations.data.Datasource
    
    @Composable
    fun AffirmationsApp() {
        val layoutDirection = LocalLayoutDirection.current
    }
    
  • Now add the rest of the code:

    @Composable
    fun AffirmationsApp() {
        val layoutDirection = LocalLayoutDirection.current
        Surface(
            Modifier = Modifier
            .fillMaxSize()
            .statusBarsPadding()
            .padding(
                start = WindowInsets.safeDrawing.asPaddingValues()
                        .calculateStartPadding(layoutDirection),
                end = WindowInsets.safeDrawing.asPaddingValues()
                        .calculateEndPadding(layoutDirection),
            ),
        ) {
            AffirmationList(
                affirmationList = Datasource().loadAffirmations(),
            )
        }
    }
    

    imports

    import com.example.affirmations.data.Datasource
    
    • fillMaxSize(): makes the Surface fill the max width and height of its parent

    • statusBarsPadding(): sets a padding to avoid overlapping with the status bar

    • padding(): sets the start and end padding to suit the layoutDirection.

  • Run the Affirmations app on a device or emulator and see the finished product!

    ../_images/unit3-pathway2-activity2-section3-286f5132aa155fa6_1440.png

Get the solution code¶

Conclusion¶

  • You now know how to create cards, list items, and scrollable lists using Jetpack Compose! Keep in mind that these are just basic tools for creating a list. You can let your creativity roam and customize list items however you like!

  • Use Card composables to create list items.

  • Modify the UI contained within a Card composable.

  • Create a scrollable list using the LazyColumn composable.

  • Build a list using custom list items.