Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

4. Building Lists With Jetpack Compose
Written by Prateek Prasad

In previous chapters, you learned about different elements in Compose and how to group and position them inside layouts to build complex UIs. Using that knowledge, you can create most screens you can think of.

However, you’ve not learned how to build one of the most common UI components mobile apps use. What happens when you have to display more elements than you can fit on the screen? In that case, while the elements are all composed, the limited screen size prevents you from seeing all of them. There are even situations where you want to dynamically add new elements on the screen and still be able to see them all, like a social media feed for instance.

The solution to this problem is allowing your content to scroll, either vertically or horizontally. The traditional way of achieving this in XML is to use ScrollView, which allows you to scroll content vertically. For horizontal scrolling, you use HorizontalScrollView. Both of them can have only one child view inside them, so to add multiple elements, you need to use a single layout that wraps those elements.

Jetpack Compose gives you a new way to achieve the same result — using scrollable and lazily composed containers.

In this chapter, you’ll learn how to make lists and grids in Jetpack Compose. You’ll learn how to show content that scrolls vertically or horizontally and how to build an alternative for the traditional RecyclerView using composable functions.

Using Vertical Scrolling Modifiers

As you know by now, Column is the replacement for LinearLayout in the vertical orientation. In Jetpack Compose, you can use the same Column composable with an additional modifier that enables scrolling! Let’s see how to implement a simple scrolling Column.

To follow along with the code examples, open Android Studio and select Open an Existing Project. Then, navigate to 04-building-lists-with-jetpack-compose/projects and select the starter folder.

Once the project builds, you’ll see the following structure:

Project Structure
Project Structure

You’ll start off by building a vertically scrollable Column after which you’ll explore its horizontal counterpart. To do that, open ScrollingScreen.kt and you’ll see two composable functions — ScrollingScreen() and MyScrollingScreen():

@Composable
fun ScrollingScreen() {
  MyScrollingScreen()

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun MyScrollingScreen() {
  //TODO add your code here
}

@Composable
fun BookImage(@DrawableRes imageResId: Int, @StringRes contentDescriptionResId: Int){
  Image(
    bitmap = ImageBitmap.imageResource(imageResId),
    contentDescription = stringResource(contentDescriptionResId),
    contentScale = ContentScale.FillBounds,
    modifier = Modifier.size(476.dp, 616.dp)
  )
}

As in the previous chapters, ScrollingScreen() is already set up to handle the back navigation, so you only need to implement MyScrollingScreen(). There is also a BookImage composable which is predefined. It creates an image of a book in a specific size with the image and content description passed as a parameter.

Change the code of MyScrollingScreen() to the following, and include the required imports with the help of Android Studio:

@Composable
fun MyScrollingScreen(modifier: Modifier = Modifier) {
  Column(modifier = modifier.verticalScroll(rememberScrollState())) {
    BookImage(R.drawable.advanced_architecture_android, R.string.advanced_architecture_android)
    BookImage(R.drawable.kotlin_aprentice, R.string.kotlin_apprentice)
    BookImage(R.drawable.kotlin_coroutines, R.string.kotlin_coroutines)
  }
}

Here, you added three existing BookImage composables to the Column. You used existing drawable and string resources for the parameters. To make the Column scrollable, you called verticalScroll() , and passed in rememberScrollState(). This creates a scroll state based on the scroll orientation and persists the scroll position so it isn’t lost after recomposition.

What happens here is that you’ll show a Column, a vertical list of items. But if the items no longer fit the screen, it will be scrollable and you’ll be able to go through each item individually.

Build and run the app, then select Scrolling from the navigation menu. You’ll see the three images, one below the other — but unfortunately, they don’t fit on the screen together. Luckily, you made the screen scrollable! :]

Scroll down to see the images that aren’t displayed yet.

Scrolling Column
Scrolling Column

Using a scrollable Column is very easy, but there is much more you can do with it. Let’s explore how it works.

Exploring the Scrollable Modifier

Look at its source code to see what a verticalScroll can do and how it works when you use it:

fun Modifier.verticalScroll(
  state: ScrollState,
  enabled: Boolean = true,
  flingBehavior: FlingBehavior? = null,
  reverseScrolling: Boolean = false
)

First, look at the function parameters. You are already familiar with some of them, but there are a few important new ones:

  • scrollState is the current state of the scroll. It determines the offset from the top and can also start or stop smooth scrolling and fling animations.
  • enabled enables or disables scrolling. If it’s disabled, you can still programmatically scroll to a specific position using the state property. But the user can’t use scrolling gestures.
  • flingBehavior is used to perform a fling animation with a given velocity.
  • reverseScrolling allows you to reverse the direction of the scroll. In other words, setting it to true lets you scroll up. Note that its default value is false.

It’s important to understand that verticalScroll() is a modifier. This means that you can make your custom composables scrollable as well, by applying it to their modifiers, if that suits your use case.

You applied vertical scrolling to a Column. If you want to apply horizontal scrolling, you use a Row instead.

Using Horizontal Scrolling Modifiers

Vertical scrolling now works on your screen — but in some cases you need a horizontal scroll, instead.

Just as you had to use a different component for horizontal scrolling called HorizontalScrollView, Jetpack Compose offers its own composable called Row, but you need to set the modifier . To achieve horizontal scroll, you need to apply horizontalScroll(), which works the same as verticalScroll() but in a different direction.

Let’s implement a scrollable Row. Inside MyScrollingScreen(), replace the Column with a Rowand verticalScroll with a horizontalScroll:

@Composable
fun MyScrollingScreen(modifier: Modifier = Modifier) {
  Row(modifier = modifier.horizontalScroll(rememberScrollState())) { // here
    ...
  }
}

You don’t have to do anything else! The scrollable Row is almost identical to the scrollable Column in terms of the default behavior. It sets up the horizontal scroll automatically, using horizontalScroll().

Build and run the app and then select Scrolling again in the navigation menu. You’ll still see the same three images, but now, the scroll works horizontally. And you accomplished this by changing just one line of code!

Scrolling Row
Scrolling Row

Scrollable columns and rows are great when you have static content, like in the previous examples. However, they aren’t a good idea for data collections that are dynamic. That’s because scrollable composables compose and render all the elements inside eagerly, which can be a heavy operation when you have a large number of elements to display.

In such cases, as you know from the traditional View system, you’d use a RecyclerView to optimize the loading and rendering of the visible elements on the screen. But how does Jetpack Compose deal with this issue? Let’s find out! :]

Lists in Compose

To display a large collection of elements in Android, you used the RecyclerView. The only elements RecyclerView renders are the ones visible on the screen. Only after the user begins to scroll does it render the new elements and display them on screen. It then recycles the elements that go off the screen into a pool of view holders.

When you scroll back to see the previous elements, it re-renders them from the pool. Thanks to this behavior, re-rendering is so quick that it’s almost as if the elements were never removed from the screen in the first place. This optimization mechanism gives RecyclerView its name.

Loading data only when it’s needed is called lazy loading and Jetpack Compose doubles down on this method to handle lists. The main two components you use for lazy lists in Compose are the LazyColumn and LazyRow.

Introducing LazyColumn & LazyRow

LazyColumn and LazyRow are used for vertical and horizontal scenarios, respectively.

RecyclerView uses a LayoutManager to set its orientation, but Jetpack Compose doesn’t have LayoutManagers. Instead, you use two different composable functions to change the orientation. The composables work in almost the same way as RecyclerView, but without needing to recycle.

When you use LazyColumn or LazyRow, the framework composes only the elements that can be shown on the screen. When you scroll, new elements are composed and the old ones are disposed off. When you scroll back, the old elements are recomposed. Jetpack Compose doesn’t need a recycled ViewHolder pool because its recomposition handles caching more efficiently.

Let’s implement both vertical and horizontal lists to categorize the books you showed earlier.

Creating Lists With LazyColumn & LazyRow

There are many awesome books in our kodeco.com library and in different categories. It’s best to show them all categorized, so you can easily pick and choose your favorites.

To do this, you’ll build a screen with a vertical list, where each composable item inside the list is another horizontal list. You’ll split the vertical list into book categories and each book category will have a horizontal list of books that belong there. Look at the image below to get a better understanding:

Book Categories
Book Categories

You can see the list of book categories scrolls vertically, while the categories themselves contain books that scroll horizontally. Your task is to duplicate that implementation, except with a dynamic number of categories and books. That way, as write more books, you can just add them to the list!

Now, open ListsScreen.kt. This file contains a predefined property named items with a list of book categories. That’s the data you’ll display on the screen. At the bottom of the file, you’ll find the following composable functions:

@Composable
fun ListScreen() {
  MyList()
  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun MyList() {
  //TODO add your code here
}

@Composable
fun ListItem(bookCategory: BookCategory, modifier: Modifier = Modifier) {
  //TODO add your code here
}

ListsScreen() is a provided composable that handles the navigation for you, so you don’t need to worry about it. Your task is to implement MyList() and the ListItem().

Add the following code inside MyList() and include the required imports from the androidx.compose.material package for Text composable and androidx.compose.foundation for other composables:

@Composable
fun MyList() {
  LazyColumn {
    items(items) { item -> ListItem(item) }
  }
}

Here, you added a LazyColumn() and set the items parameter with the items property containing your data. items is a list of objects of the BookCategory type. Each BookCategory contains a String with the category name and a list of images showing the books that should appear in that category.

Within the trailing lambda, for each item parameter inside the list of items, you create a new ListItem component. This lambda represents the function to transform each of the objects within items to composable elements.

This way you can call any number of composable functions to represent your items and you can add special rendering logic depending on the item type, its position and more!

Next, you’ll implement ListItem(). Replace ListItem() with the following code and, once again, don’t forget to include the required imports with the help of Android Studio:

@Composable
fun ListItem(bookCategory: BookCategory, modifier: Modifier = Modifier) {
  Column(modifier = Modifier.padding(8.dp)) {
    Text(
      text = stringResource(bookCategory.categoryResourceId),
      fontSize = 22.sp,
      fontWeight = FontWeight.Bold,
      color = colorResource(id = R.color.colorPrimary)
    )
    Spacer(modifier = modifier.height(8.dp))

    // TODO
  }
}

This looks like a lot of code, but what it does is quite straightforward. First, you added a Column() as the parent layout of the composable so you can align its children vertically. The Column() uses a padding modifier to add some space near the borders.

The top child of Column() is a Text(). You need this to display the title of the category, which is passed as the text argument. Note how you styled the text by changing the font size, weight and color.

The next element is a Spacer, which adds some space between the category name and the rest of the content. This will let you show the category name on top of the horizontal list of books.

Now add the following code underneath the Spacer, to add the horizontal list of books:

LazyRow {
  items(bookCategory.bookImageResources) { items ->
    BookImage(items)
  }
}

Similar to how you built a vertical list with LazyColumn, using a LazyRow you create a horizontal list. It receives the list of book images as a parameter and a lambda that builds BookImages. Also add the BookImage() in a separate function:

@Composable
fun BookImage(imageResource: Int) {
  Image(
    modifier = Modifier.size(170.dp, 200.dp),
    painter = painterResource(id = imageResource),
    contentScale = ContentScale.Fit,
    contentDescription = stringResource(R.string.book_image)
  )
}

A BookImage is a wrapper for an Image composable. Image() displays the book image for each element in the list. You used a size modifier to set a static size of 170dp width and 200dp height.

Since the list you passed as an argument to LazyRow() contains resource IDs instead of the actual images, you need to use painterResource() to retrieve the correct asset. Finally, by using ContentScale.Fit, you make the image adapt to the size you specified earlier and set the content description with the provided string.

Now, build and run the app. Once the main screen loads, click the List button in the navigation menu. Your app will show the following screen:

List
List

As you see, the books are sorted by category. You can scroll vertically to browse book categories and horizontally to browse books in each category.

By the way, if you’re interested in any of the books you see, you can find them in our book library! :]

Compared to the verbose implementation of a RecyclerView, lists in Jetpack Compose are easy to use and understand. Their signature requires only a few parameters to make them work and its extremely flexible with how you want to customize them. Let’s dive a bit deeper into their implementation.

Exploring Lists

Now you understand the difference and how to implement specific lists, take a look at the signature for LazyColumn and LazyRow:

@Composable
fun LazyColumn(
  modifier: Modifier = Modifier,
  state: LazyListState = rememberLazyListState(),
  contentPadding: PaddingValues = PaddingValues(0.dp),
  reverseLayout: Boolean = false,
  verticalArrangement: Arrangement.Vertical =
      if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
  horizontalAlignment: Alignment.Horizontal = Alignment.Start,
  flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
  userScrollEnabled: Boolean = true,
  content: LazyListScope.() -> Unit
)

@Composable
fun LazyRow(
  modifier: Modifier = Modifier,
  state: LazyListState = rememberLazyListState(),
  contentPadding: PaddingValues = PaddingValues(0.dp),
  reverseLayout: Boolean = false,
  horizontalArrangement: Arrangement.Horizontal =
      if (!reverseLayout) Arrangement.Start else Arrangement.End,
  verticalAlignment: Alignment.Vertical = Alignment.Top,
  flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
  userScrollEnabled: Boolean = true,
  content: LazyListScope.() -> Unit
)

The most important parameter to notice here is content which represents the content inside the list. This content is of a LazyListScope type and not your usual Composable type.

Take a look at the LazyListScope interface to learn why is it so important.

interface LazyListScope {

    fun item(key: Any? = null,contentType: Any? = null,content: @Composable LazyItemScope.() -> Unit)

    fun items(
        count: Int,
        key: ((index: Int) -> Any)? = null,
        contentType: (index: Int) -> Any? = { null },
        itemContent: @Composable LazyItemScope.(index: Int) -> Unit
    )

    @ExperimentalFoundationApi
    fun stickyHeader(key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit)
}

The interface provides a set of functions which help you when building lists:

  • items() allows you to set a list of item data you would like to use in each of your list items. Once you set the data, you also need to provide an itemContent which is a composable used for displaying every item in your list.
  • item() allows you to add a new composable item to your list. Note that you can use different composable types every time.
  • stickyHeader() allows you to set the header composable that will remain visible on the top of the list, even after you scroll down to see new items. Note that this function is annotated with @ExperimentalFoundationApi which means that it’s still in experimental stage and might change or be removed in the future.

Unlike the RecyclerView, lists in Jetpack Compose don’t require an adapter, view holder layout managers and an RecyclerView element in your XML files just to make it work. Using one of the two very simple functions, you can either show a horizontal or a vertical list that is performant and customizable!

There are also extension functions like itemsIndexed, which has same features as items() but also provides you with an index for each of your items.

That’s all for the theory. So far you’ve implemented simple lists and a list of horizontal lists for your books. The last thing you need to learn how to do is build grids.

Grids in Compose

When working with a RecyclerView, you can use different types of LayoutManagers to place your elements on the screen in different ways. To make grids, for example, you use a GridLayoutManager and then set the number of columns inside the grid.

Implementing grids in Jetpack Compose is far simpler than that. The grid you’ll implement resembles what you saw in the last list example. This time, however, the elements won’t scroll horizontally but will be fixed in place, instead.

To better visualize the problem, look at the following image:

Grid Calculation
Grid Calculation

As you see, your grid contains ten elements distributed across three columns. The last row shows only one element in the first column, because that’s the last element in your dataset. There are two more elements next to it, but they’re marked as invisible for demonstrational purposes.

Implementing a Grid

Open GridScreen.kt and take a moment to look inside. You’ll find the usual function to handle the navigation and a list containing the icons that you’ll use as the grid’s content. At the bottom of the file, you’ll find the following composable functions that you need to implement:

@Composable
fun GridScreen() {
  //TODO add your code here

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun GridIcon(iconResource: ImageVector) {
  //TODO add your code here
}

Replace the GridScreen code with the following.

@Composable
fun GridScreen() {
  LazyVerticalGrid( //1
    modifier = Modifier.fillMaxSize(), //2
    columns = GridCells.Fixed(3), //3
    content = {
      items(items.size) { index -> //4
        GridIcon(items[index]) //5
      }
    }
  )

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

Here’s a breakdown of the code you just replaced:

  1. You added a LazyVerticalGrid as the container for your grid.
  2. You used the fillMaxSize modifier to ensure the grid takes up all the available space in the screen.
  3. You set the number of columns that the grid should have to 3, using the GridCells.Fixed() property.
  4. You passed in the size of your data set into the items().
  5. For each item in the list, you called the GridIcon composable, passing in the icon as a parameter.

Note: There are two types of GridCells:

Fixed sets the fixed amount of cells on the screen.

Adaptive adds as many rows or columns as possible to fit the screen with the provided minSize as the minimum size parameter.

The content lambda here works the same as with LazyRow or LazyColumn. You provide the collection of data and a composable which is used for every grid cell. LazyVerticalGrid then calculates and positions your composable in a grid depending on the cells parameter.

You will now implement the GridIcon composable to render your grid.

Implementing GridIcon

Each GridItem() will show the icon you passed in. Replace the GridIcon with the following code to achieve such behavior:

@Composable
fun GridIcon(iconResource: ImageVector) {
  Icon(
    imageVector = iconResource, //1
    tint = colorResource(R.color.colorPrimary), //2
    contentDescription = stringResource(R.string.grid_icon), //3
    modifier = Modifier //4
      .size(80.dp)
      .padding(20.dp)
  )
}

Here’s a breakdown of the code block:

  1. You set the imageVector of the Icon to the parameter passed to the composable.
  2. You set the tint of the icon to colorPrimary using the colorResource composable.
  3. You set the contentDescription of the icon for accessibility.
  4. You set the size of the icon to 80dp and the padding around the icon to 20dp.

Build and run the app, then click the Grid button in the navigation menu. You’ll see the following screen:

Grid With Three Columns
Grid With Three Columns

Awesome! You have a grid of icons on the screen, placed in three columns. You can increase the number of icons inside the items list to make the grid scrollable. To experiment with different column counts, increase the size of the list to the desired value and see the result. Keep in mind that you’re limited to the number of columns that fit the screen.

Congratulations! You’ve learned a lot about how to lay out large numbers of elements in Jetpack Compose.

Key Points

  • Use Column with the verticalScroll modifier to make the content vertically if it doesn’t fit the screen.
  • Use Row with the horizontalScroll modifier to make the content scroll horizontally if it doesn’t fit the screen.
  • You can make your own composables scrollable by adding the verticalScroll or horizontalScroll modifiers.
  • Use scroll modifiers only for a fixed amount of content.
  • For dynamic and larger amounts of content, use the lazy counterparts of Row, Column and Grid instead.
  • The composable alternatives to RecyclerView are called LazyColumn and LazyRow for the vertical and horizontal scenarios, respectively.
  • You can group lists inside each other to make content scrollable in both directions.
  • To make grids, use a LazyVerticalGrid.
  • You can use LazyRow and LazyColumn components if you want to manually add items to the list, allowing you to build headers and footers. Learn more about them here: https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/package-summary#lazycolumn.

Where to Go From Here?

In this chapter, you learned how to make scrollable content, scrollable lists for dynamically created elements and custom grids.

You’re ready to implement this UI functionality in your own apps. This wrapped up the entire first section! In the next section and the next chapter, you’ll learn how to build more complex custom composables using all the knowledge you’ve gained so far.

See you there! :]

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.