Techbase Academy
Android · Lesson 21

Scrollable Lists and Grids: The Affirmations App

11 phasesYoruba Proverbs AppAndroid Studio
Phase 1 of 11
Lesson Introduction

Open almost any app on your phone: WhatsApp chats, a bank's transaction history, a shop's product catalogue, your music library. Every one of them is built around a scrolling list (or a grid) of items. If you can build lists well, you can build the core of most apps.

In this lesson you will build Affirmations, an app that shows a scrollable list of cards, each with a picture and an encouraging sentence. Then you will turn the list into a grid, and finish by giving your app its own launcher icon.

This lesson covers four items from Unit 3 of Google's official Android Basics with Compose course: Intro to Affirmations app, Add a scrollable list, Change the app icon and Practice: Build a grid.

By the end you will be able to:

  • Model list data with a Kotlin data class.
  • Store text in strings.xml and images in res/drawable, and reference them with @StringRes and @DrawableRes.
  • Build a card with Card, Image and Text.
  • Display a long list efficiently with LazyColumn and items.
  • Display a grid with LazyVerticalGrid and GridCells.
  • Create an adaptive launcher icon with Image Asset Studio.

Big idea: a list on screen is just a list in your code, turned into UI one item at a time. Data first, then a function that draws one item, then a lazy container that repeats it.

✏️ 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
  • Data classes (lesson 11). data class gives you a tidy way to bundle values together and automatically adds toString, equals and copy.
  • Lists (lesson 14). listOf(...) creates a read-only list, and you loop over it.
  • Row, Column and Modifier (lesson 16). Lists are built from the same layout pieces.
  • Resources. Android keeps text, images, colours and sizes in the res folder, separate from code. This makes translation and theming easy. You will use res/values/strings.xml and res/drawable today.
✏️ 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: Why "Lazy"?

You could draw a list of 1,000 items by putting 1,000 Texts inside a Column. It would work for ten items and then your app would freeze, because the Column creates every child immediately, even the 990 that are far off screen.

A lazy container is smarter. It only composes the items you can actually see (plus a few just outside the screen), and it throws away or reuses the rest as you scroll. Think of a waiter at a huge buffet who only brings the dishes you are looking at, instead of piling every dish on your table at once.

Container Composes Use it for
Column All children, always A handful of items
LazyColumn Only visible items Long or unknown-length vertical lists
LazyRow Only visible items Horizontal lists (carousels)
LazyVerticalGrid Only visible cells Grids of cards or photos

Inside a lazy container you do not write a loop. You describe the items with a small DSL (a mini-language built from lambdas), and the container decides when to run your code:

kotlin
LazyColumn {
 items(myList) { item ->
 Text(item)
 }
}

The lambda after items(myList) is run once per visible item, and item is the element for that row.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Why "Lazy"?. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: The Data

Create a new Empty Activity project called Affirmations (package com.example.affirmations).

The model

Right-click the package, choose New > Kotlin Class/File, name it Affirmation, and write a data class:

Affirmation.kt
package com.example.affirmations.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class Affirmation(
 @StringRes val stringResourceId: Int,
 @DrawableRes val imageResourceId: Int
)

Notice that it stores numbers, not text and pictures. Android generates a unique integer ID for every resource in the class R. R.string.affirmation1 is an Int. The annotations @StringRes and @DrawableRes are safety labels: Android Studio now warns you if you accidentally pass a drawable ID where a string ID is expected, even though both are just Ints.

(You will need a folder named model under the package. Create it with New > Package, or just keep the file in the main package and adjust the package line.)

The text resources

Open app/src/main/res/values/strings.xml and add ten sentences. Here are the first few:

strings.xml
<resources>
 <string name="app_name">Affirmations</string>
 <string name="affirmation1">I am strong.</string>
 <string name="affirmation2">I believe in myself.</string>
 <string name="affirmation3">Each day is a new opportunity to grow.</string>
 <string name="affirmation4">I can learn anything I set my mind to.</string>
 <string name="affirmation5">Small steps still move me forward.</string>
 <string name="affirmation6">I am proud of how far I have come.</string>
 <string name="affirmation7">I choose to see the good in today.</string>
 <string name="affirmation8">Mistakes are proof that I am trying.</string>
 <string name="affirmation9">I am becoming a better developer every day.</string>
 <string name="affirmation10">Today I will build something worth sharing.</string>
</resources>

Putting text in strings.xml instead of inside code means you can later translate the app (for example into Yoruba or Hausa) by adding a values-yo or values-ha folder, without touching a single line of Kotlin.

The image resources

You need ten pictures in res/drawable. Pick the route that suits you:

  1. Your own photos or images. Copy ten small JPG or PNG files into res/drawable. File names must use only lowercase letters, digits and underscores, for example image1.jpg. Keep them small (under 300 KB each) so the app stays light.
  2. No images? Use free built-in icons. Choose File > New > Vector Asset (or right-click res/drawable, then New > Vector Asset), click the clip-art button, pick an icon, name it image1, and finish. Repeat for ten icons. These are scalable and tiny.

Whichever you choose, name the ten resources image1 to image10.

The data source

Create Datasource.kt that builds the list:

Datasource.kt
package com.example.affirmations.data
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation
class Datasource {
 fun loadAffirmations(): List<Affirmation> {
 return listOf(
 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)
 )
 }
}

Why a class with a function instead of a bare list? Later in this course (lessons 30 and 34) the data will come from a repository, a web service or a database, and a function is the natural seam where that change happens.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: The Data. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: One Card

Always build one item first and preview it before you worry about the list. Open MainActivity.kt and add this composable. Add the imports as Android Studio suggests (Alt + Enter), or copy this block:

kotlin
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation
@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 = stringResource(affirmation.stringResourceId),
 modifier = Modifier.padding(16.dp),
 style = MaterialTheme.typography.headlineSmall
 )
 }
 }
}
@Preview(showBackground = true)
@Composable
fun AffirmationCardPreview() {
 AffirmationCard(Affirmation(R.string.affirmation1, R.drawable.image1))
}

What to notice:

  • Card is a Material 3 container with rounded corners and elevation.
  • stringResource(id) turns the Int ID into the actual text, and it respects the user's language.
  • painterResource(id) loads the drawable.
  • contentDescription is read aloud by screen readers, so we reuse the sentence. (A purely decorative picture can use null; lesson 47 explains when.)
  • ContentScale.Crop fills the box and crops the extra, so differently-shaped pictures look uniform.
  • MaterialTheme.typography.headlineSmall takes its size and weight from the theme, so you can restyle the whole app later in one place (lesson 22).

Open the Preview pane. You should see one card: a picture on top, the sentence beneath.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: One Card. 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 Scrollable List

Now repeat that card for every affirmation.

kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import com.example.affirmations.data.Datasource
@Composable
fun AffirmationList(
 affirmationList: List<Affirmation>,
 modifier: Modifier = Modifier
) {
 LazyColumn(
 modifier = modifier,
 contentPadding = PaddingValues(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(affirmationList, key = { it.stringResourceId }) { affirmation ->
 AffirmationCard(affirmation = affirmation)
 }
 }
}
@Composable
fun AffirmationsApp(modifier: Modifier = Modifier) {
 AffirmationList(
 affirmationList = Datasource().loadAffirmations(),
 modifier = modifier
 )
}

Then call it from setContent. The Empty Activity template wraps content in a Scaffold so that your content does not hide behind the system bars. Use the padding it gives you:

kotlin
setContent {
 AffirmationsTheme {
 Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
 AffirmationsApp(modifier = Modifier.padding(innerPadding))
 }
 }
}

(Scaffold needs import androidx.compose.material3.Scaffold and fillMaxSize needs import androidx.compose.foundation.layout.fillMaxSize.)

Understanding each piece

  • contentPadding adds space inside the scrolling area, so the first card is not glued to the top and the last card can scroll clear of the bottom edge.
  • Arrangement.spacedBy(8.dp) puts an 8 dp gap between neighbouring items, so you never need margins on each card.
  • items(list) { ... } runs the lambda for each visible element.
  • key = { it.stringResourceId } gives each item a stable identity. When a list changes (an item is added, removed or moved), keys let Compose track which item is which instead of rebuilding everything. Keys must be unique and stable, and the string resource ID is both. Lesson 39 covers keys in depth.

Run the app and scroll. Ten cards, smooth scrolling.

Experiment: change ten to ten thousand. Make a loop that repeats loadAffirmations() a thousand times (without keys, because duplicates would clash). The app stays smooth, because only a handful of items exist at any moment. That is the entire point of a lazy list.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: The Scrollable List. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: A Grid with LazyVerticalGrid

A grid lays items out in columns. The container takes a columns rule that says how many:

kotlin
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@Composable
fun AffirmationGrid(
 affirmationList: List<Affirmation>,
 modifier: Modifier = Modifier
) {
 LazyVerticalGrid(
 columns = GridCells.Adaptive(minSize = 160.dp),
 modifier = modifier,
 contentPadding = PaddingValues(8.dp),
 horizontalArrangement = Arrangement.spacedBy(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(affirmationList) { affirmation ->
 AffirmationCard(affirmation = affirmation)
 }
 }
}

There are two ways to say how many columns:

Rule Meaning Example
GridCells.Fixed(2) Always exactly two columns. A catalogue that must look the same everywhere.
GridCells.Adaptive(minSize = 160.dp) As many columns as fit, each at least 160 dp wide. A grid that shows two columns on a phone and five on a tablet.

Adaptive is usually the better choice, because it quietly adjusts when the user rotates the phone or opens your app on a large screen. Note that items for the grid comes from a different import (androidx.compose.foundation.lazy.grid.items) than the items for LazyColumn. Importing the wrong one is the most common error with grids.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: A Grid with LazyVerticalGrid. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Changing the App Icon

The default icon is the green Android robot. Every real app needs its own.

Using Image Asset Studio

  1. In the Android view, right-click the res folder.
  2. Choose New > Image Asset.
  3. For Icon Type choose Launcher Icons (Adaptive and Legacy).
  4. On the Foreground Layer tab choose a source: Image (your own logo), Clip Art (a built-in icon) or Text. Use Resize to keep the artwork inside the safe zone, the central circle.
  5. On the Background Layer tab pick a colour or image.
  6. Check the preview in different shapes (circle, square, squircle), then click Next and Finish.

What "adaptive" means

Since Android 8, an icon is made of two layers, a foreground and a background. Each phone maker applies its own mask (circle, rounded square, and so on) and the system can move the layers slightly for parallax effects. If you put important artwork near the edges it gets cropped. Keeping it inside the safe zone makes the icon look right on every phone.

Newer Android versions also support a themed icon that takes on the user's wallpaper colours. For that, provide a single-colour monochrome layer. Image Asset Studio can generate one from your foreground.

Finally, check AndroidManifest.xml. The <application> tag points at your icon through android:icon="@mipmap/ic_launcher" and android:roundIcon="@mipmap/ic_launcher_round". Run the app and look at the launcher: your icon replaces the robot.

✏️ Your Task
Practise what you just learned about Changing the App Icon. 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: Build a grid of topics (the Practice: Build a grid exercise)

Scenario: A school wants an app that shows the subjects it teaches as a two-column grid of cards.

Requirements:

  1. Create a data class Topic(@StringRes val name: Int, val courseCount: Int, @DrawableRes val image: Int).
  2. Make a list of at least eight topics (for example Mathematics, English, Biology, Chemistry, Physics, Economics, Government, Computer Science).
  3. Each card is a Card with the picture on the left (68 dp square) and the name plus the course count on the right. Use a Row.
  4. Show exactly two columns using GridCells.Fixed(2).
kotlin
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.MaterialTheme
import androidx.compose.material3.Text
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
data class Topic(
 @StringRes val name: Int,
 val courseCount: Int,
 @DrawableRes val image: Int
)
@Composable
fun TopicCard(topic: Topic, modifier: Modifier = Modifier) {
 Card(modifier = modifier) {
 Row(verticalAlignment = Alignment.CenterVertically) {
 Image(
 painter = painterResource(topic.image),
 contentDescription = null,
 modifier = Modifier.size(68.dp),
 contentScale = ContentScale.Crop
 )
 Column(modifier = Modifier.padding(horizontal = 16.dp)) {
 Text(
 text = stringResource(topic.name),
 style = MaterialTheme.typography.bodyMedium
 )
 Text(
 text = "${topic.courseCount} courses",
 style = MaterialTheme.typography.labelMedium
 )
 }
 }
 }
}
@Composable
fun TopicGrid(topics: List<Topic>, 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(topics) { topic -> TopicCard(topic) }
 }
}

Notes: contentDescription = null is correct here because the text beside the picture already says everything, so a screen reader would only repeat itself. Alignment.CenterVertically centres the text against the square picture.

🎯 Your Challenge

Exercise 2: Click to select

Make the Affirmation list react to taps. When the user taps a card, show a Text banner at the top reading You chose: ... with the sentence. Use rememberSaveable to remember the chosen ID (lesson 17) and the clickable modifier on the card.

kotlin
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@Composable
fun AffirmationsApp(modifier: Modifier = Modifier) {
 var chosenId by rememberSaveable { mutableStateOf<Int?>(null) }
 Column(modifier = modifier) {
 if (chosenId != null) {
 Text(
 text = "You chose: " + stringResource(chosenId!!),
 modifier = Modifier.padding(16.dp),
 style = MaterialTheme.typography.titleMedium
 )
 }
 LazyColumn(
 contentPadding = PaddingValues(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(Datasource().loadAffirmations(), key = { it.stringResourceId }) { affirmation ->
 AffirmationCard(
 affirmation = affirmation,
 modifier = Modifier.clickable { chosenId = affirmation.stringResourceId }
 )
 }
 }
 }
}

Why it works: the click lambda changes state, which triggers recomposition, and the banner reads the state. Same loop as the dice roller. Production code would avoid !! by using chosenId?.let { ... }, which you can try as an improvement.

✏️ Your Task

Change GridCells.Adaptive(minSize = 160.dp) to GridCells.Fixed(3) and compare how the grid looks on a phone and in landscape. Which rule would you pick for an app that must look good on tablets too, and why?

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

Mistake #1: A Column for a long list. What happens: every item is composed immediately, memory climbs, scrolling stutters or the list does not scroll at all unless you add verticalScroll. The fix: LazyColumn.

Mistake #2: Nested vertical scrolling. The wrong code: putting a LazyColumn inside a Column that has verticalScroll, or inside another vertical LazyColumn. What happens: a crash saying vertically scrollable components were measured with an infinite maximum height. The fix: one scrolling container per direction. Put extra items inside the same LazyColumn as separate item { } entries.

Mistake #3: Wrong items import. Symptom: red underline on items inside a grid. The fix: for LazyVerticalGrid, import androidx.compose.foundation.lazy.grid.items; for LazyColumn, androidx.compose.foundation.lazy.items.

Mistake #4: Duplicate or changing keys. What happens: a crash complaining that the key was already used. The fix: keys must be unique and stable. Never use the item's position in the list as the key if the list can change order.

Mistake #5: Wrong resource folder or file names. Symptom: R.drawable.Image1 does not exist. The fix: resource names are lowercase letters, digits and underscores only. Image1.jpg is rejected, image1.jpg works.

Mistake #6: Huge images. What happens: the app is slow and uses a lot of memory because full-size camera photos are decoded. The fix: resize images before adding them, or use vector assets. Lesson 31 shows how to load remote images efficiently.

Mistake #7: Missing contentDescription thinking. The fix: give meaningful pictures a description and pass null only for decorative ones.

Mistake #8: Hard-coded text. What happens: Android Studio warns Hardcoded string. The fix: put text in strings.xml and read it with stringResource.

✏️ 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. Why is LazyColumn better than Column for a long list?

LazyColumn only composes (and keeps) the items that are visible, plus a few near the screen edge. Column composes all children at once, which wastes memory and time for long lists.

Question 2. What is the purpose of @StringRes and @DrawableRes?

They are annotations that mark an Int as a resource ID of a specific type. Resource IDs are plain integers, so the annotations let Android Studio warn you when you pass the wrong kind of ID.

Question 3. What is the difference between GridCells.Fixed(2) and GridCells.Adaptive(160.dp)?

Fixed(2) always creates exactly two columns. Adaptive(160.dp) creates as many columns as fit when each column is at least 160 dp wide, so the count grows on wider screens.

Question 4. Why give items a key?

A key gives every item a stable identity so Compose can match old and new items when the list changes, preserving item state and animating or reusing correctly. Keys must be unique and stable.

Question 5. What are the two layers of an adaptive icon, and why keep artwork in the safe zone?

A foreground and a background layer. Phone makers apply different masks (circle, square, squircle), which crop the edges, so important artwork must stay inside the central safe zone.

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
Yoruba Proverbs App

Project goal: Build a scrollable app of ten Yoruba proverbs. Each card shows a decorative icon, the proverb in Yoruba and its English meaning. It teaches the same pattern you will reuse for products, contacts and messages.

Requirements:

  1. Create data class Proverb(@StringRes val yoruba: Int, @StringRes val english: Int, @DrawableRes val icon: Int).
  2. Add ten proverbs and their English meanings to strings.xml, using resource names proverb1 to proverb10 and meaning1 to meaning10. (Use only proverbs you are confident about, or write your own sayings in any language.)
  3. Show them in a LazyColumn with spacing and padding.
  4. Each card has the Yoruba text in titleMedium style and the meaning in bodyMedium style underneath.
  5. Add a toggle button in the top bar that switches the layout between the list and a two-column grid.
  6. Change the launcher icon with Image Asset Studio.

Stretch goal: Add values-yo translations for the app name.

Proverbs.kt
package com.example.proverbs
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items as gridItems
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
data class Proverb(
 @StringRes val yoruba: Int,
 @StringRes val english: Int,
 @DrawableRes val icon: Int
)
val proverbs = listOf(
 Proverb(R.string.proverb1, R.string.meaning1, R.drawable.icon1),
 Proverb(R.string.proverb2, R.string.meaning2, R.drawable.icon2),
 Proverb(R.string.proverb3, R.string.meaning3, R.drawable.icon3)
 // ... continue to ten
)
@Composable
fun ProverbCard(proverb: Proverb, modifier: Modifier = Modifier) {
 Card(modifier = modifier) {
 Column(modifier = Modifier.padding(16.dp)) {
 Text(
 text = stringResource(proverb.yoruba),
 style = MaterialTheme.typography.titleMedium
 )
 Text(
 text = stringResource(proverb.english),
 style = MaterialTheme.typography.bodyMedium,
 modifier = Modifier.padding(top = 8.dp)
 )
 }
 }
}
@Composable
fun ProverbsApp(modifier: Modifier = Modifier) {
 var asGrid by rememberSaveable { mutableStateOf(false) }
 Column(modifier = modifier) {
 Button(
 onClick = { asGrid = !asGrid },
 modifier = Modifier.padding(8.dp)
 ) {
 Text(if (asGrid) "Show as list" else "Show as grid")
 }
 if (asGrid) {
 LazyVerticalGrid(
 columns = GridCells.Fixed(2),
 contentPadding = PaddingValues(8.dp),
 horizontalArrangement = Arrangement.spacedBy(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 gridItems(proverbs, key = { it.yoruba }) { ProverbCard(it) }
 }
 } else {
 LazyColumn(
 contentPadding = PaddingValues(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(proverbs, key = { it.yoruba }) { ProverbCard(it) }
 }
 }
 }
}

Notes on the solution: both items functions are imported, the grid one under the alias gridItems, which is how you can use both in one file without a clash. The toggle button flips a boolean in state, and Compose swaps the container. Extend the proverbs list to ten entries and add the matching strings.xml lines. The proverbs themselves are yours to choose, and checking meanings with an elder or a reliable source is part of the exercise.

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 21 project”.

3

On github.com click New repository, name it android-lesson-21-scrollable-lists-and-grids-the-affirmation, 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 21 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Why "Lazy"?
  • Step-by-Step Build 1: The Data
  • Step-by-Step Build 2: One Card
  • Step-by-Step Build 3: The Scrollable List
  • Step-by-Step Build 4: A Grid with LazyVerticalGrid
  • Changing the App Icon
  • 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