Jetpack Compose Tutorial for Android: Getting Started

In this Jetpack Compose tutorial, you’ll learn to use the new declarative UI framework being developed by the Android team by creating a cookbook app. By Joey deVilla.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

Listing Ingredients

Each Recipe instance has a property called ingredients, which is a list of strings. You can display the ingredients by adding the following below the Text for the recipe’s title:

for (ingredient in recipe.ingredients) {
  Text(ingredient)
}

One of the awesome elements of Jetpack Compose is you can use ordinary Kotlin code to express slightly more complex UI details. In the code above, you use a for loop to list each item in the recipe’s ingredients property using a Text() composable. If you rebuild the UI you’ll see all the ingredients of this delicious ramen meal listed below the title. Not only is that cool, but you were also spared from having to define a RecyclerView.Adapter or any ViewHolders.

Refresh the preview. You should see this:

A preview of the recipe card, which displays the image of the recipe, the title, and the ingredients.

You’ll need one more piece of information to add to the card: the description.

Adding the Description

The right side of the recipe card below the photo has plenty of space, making it a good place to put the description. To do this, you’ll need another layout composable: Column’s counterpart, Row. Like Column, Row acts like a LinearLayout, but with its orientation set to horizontal.

It’s often helpful to work from a diagram as your Jetpack Compose layouts get more complex. Here’s a diagram showing how you’ll use Column and Row layout composables to arrange the UI elements on the recipe card:

Diagram showing how Column and Row composables are used to lay out elements on the recipe card.

You can see the body of the recipe card is still contained inside a Column, but now there’s a Row beneath the photo. The Row contains two elements: A Column containing the recipe title and the list of ingredients on the left, and the description on the right.

Implement this new layout by updating RecipeCard() as shown below:

@Composable
fun RecipeCard(recipe: Recipe) {
  Column(modifier = Modifier.fillMaxWidth()) {
    Image(
      painterResource(recipe.imageResource),
      contentDescription = recipe.title,
      contentScale = ContentScale.Crop,
      modifier = Modifier.fillMaxWidth().height(144.dp)
    )
    Row {
      Column {
        Text(recipe.title)
        for (ingredient in recipe.ingredients) {
          Text(ingredient)
        }
      }. 
      Text(recipe.description)
    }
  }
}

The preview should show a complete — if not exactly pretty — recipe card:

A preview of the recipe card, which displays the image of the recipe, the title, the ingredients, and the description.

Now that the recipe card is showing the all the recipe information, it’s time to improve its look.

Improving the Recipe Card’s Typography

The recipe card’s title should be headline-sized and in bold text, and the list of ingredients should have bullet points to make it list-like. Update the Row in RecipeCard to the following:

Row {
  Column {
    Text(
      text = recipe.title,
      style = MaterialTheme.typography.headlineLarge,
      fontWeight = FontWeight(700)
    )
    for (ingredient in recipe.ingredients) {
      Text(
        text = "• $ingredient",
        style = MaterialTheme.typography.bodyMedium
      )
    }
  }
  Text(
    text = recipe.description,
    style = MaterialTheme.typography.bodySmall
  )
}

You added some Text() parameters:

  • style, which sets the Text composable output’s font face, weight and size to those defined by the current UI theme’s styles. You used it to set the recipe title to the large headline style, the ingredients to the medium body text style, and the description, which can get long, to the small body text style.
  • fontWeight, which you used to make the recipe title a little more bold.

You also changed the Text() call for each recipe ingredient so it starts with a bullet point (•).

Note: To enter a bullet point in macOS, the key combination is option-8. On Windows, it’s alt-0149. On Linux, it’s ctrl-Shift-u followed by 2022.

The recipe card with styled text.

This is an improvement, but the card still looks crowded. Fix it by adding space around the text elements.

Adding Space to the Recipe Card

The card needs a little breathing room. You already know how to add padding to a composable by using its modifier parameter, so do that by adding a modifier at the start of the Column() composable containing the title and ingredients …

Column (modifier = Modifier.padding(16.dp)) {

...

…and update the Text() composable for the description with the same modifier:

Text(
  text = recipe.description,
  style = MaterialTheme.typography.bodySmall,
  modifier = Modifier.padding(16.dp)
)

The preview should show you a recipe card that looks less cluttered:

The recipe card, now with padding.

The placement of the description doesn’t look quite right. It should start at the same y-coordinate as the ingredients, not the title. This can be fixed with a Spacer, a composable that emits an empty UI element whose only purpose is to add space between UI elements. A Spacer that’s the same height as the title, placed above the description, would fix this problem.

Add this line before the Text() composable for the description:

Spacer(modifier = Modifier.height(42.dp))

You should see this in the preview:

The recipe card with a spacer, but no apparent effect.

The description should have been pushed further down the card, but it doesn’t look as though anything changed. What happened?

The Spacer is there, but it’s to the left of the description, not above it. Remember — the Text for the description is inside a Row, which lays out its elements from left to right. The Spacer has a specified height, but no specified width, which the Android layout engine interprets as “make it as thin as possible so it will fit.”

The way to fix this is to put both the Spacer and the description Text inside a Column. Replace the Spacer and description Text with this code:

Column(modifier = Modifier.padding(16.dp)) {
  Spacer(modifier = Modifier.height(42.dp))
  Text(
    text = recipe.description,
    style = MaterialTheme.typography.bodySmall
  )
}

The recipe card with a spacer, where the top of the description lines up with the top of the ingredients.

That’s much better.

Rounding the Recipe Card’s Corners

Rectangles with rounded corners aren’t just a design trend. There’s research showing we prefer rounded objects over ones with sharp edges, and many UI/UX designers agree withthose findings. I also think that they make for nicer-looking content containers.

The simplest (and preferred) way to add rounded corners to a composable is to compose it into a Surface composable, which is particularly good at applying color, shape and elevation to the UI element contained by it. Update the RecipeCard function as shown below:

@Composable
fun RecipeCard(recipe: Recipe) {
  Surface(
    color = MaterialTheme.colorScheme.surface,
    border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary),
    shape = RoundedCornerShape(8.dp),
    tonalElevation = 2.dp,
    shadowElevation = 10.dp
  ) {
    Column(modifier = Modifier.fillMaxWidth()) {
      Image(
        painterResource(recipe.imageResource),
        contentDescription = recipe.title,
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxWidth().height(144.dp)
      )
      Row {
        Column(modifier = Modifier.padding(16.dp)) {
          Text(
            text = recipe.title,
            style = MaterialTheme.typography.headlineLarge,
            fontWeight = FontWeight(700)
          )
          for (ingredient in recipe.ingredients) {
            Text(
              text = "• $ingredient",
              style = MaterialTheme.typography.bodyMedium
            )
          }
        }
        Column(modifier = Modifier.padding(16.dp)) {
          Spacer(modifier = Modifier.height(42.dp))
          Text(
            text = recipe.description,
            style = MaterialTheme.typography.bodySmall
          )
        }
      } 
    }
  }
}

The Surface parameters worth noting are:

  • shape: You used this to specify that the card should have corners rounded with a 12 dp radius.
  • tonalElevation: This is the first of two parameters that provide the illusion of depth. A higher value means the UI element is closer to the user. Elements closer to the user are colored darker in light theme and lighter in dark theme.
  • shadowElevation: The second of two parameters that provide the illusion of depth, this sets the size of the shadow below the UI element.

You now have a nice-looking recipe card:

The finished recipe card, complete with rounded edges and shadow.