Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

4. Building Lists with Jetpack Compose
Written by Tino Balint

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 could potentially build any screen.

However, you’re missing some functionality that you’ll eventually need. What happens when you have to display more elements than you can fit on the screen? In that case, the elements are all composed, but the limited screen size prevents you from seeing all of them. There are even situations where you want to dynamically add an infinite number of new elements on the screen and still be able to see them all.

The solution to this problem is allowing your content to scroll, either vertically or horizontally. The traditional way of implementing this feature 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 to help you fit all your content on the screen. 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 ScrollableColumn

As you know by now, Column is the replacement for LinearLayout in the vertical orientation. For vertical scrolling elements, there’s a similar layout called ScrollableColumn, which is actually a variation of the Column that enables scrolling! Let’s see how to implement a simple ScrollableColumn.

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 ScrollableColumn 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
}

As in the previous chapters, ScrollingScreen() is already set up to handle the back navigation, so you only need to implement MyScrollingScreen().

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

@Composable
fun MyScrollingScreen() {
  ScrollableColumn {
    Image(bitmap = imageResource(R.drawable.advanced_architecture_android))
    Image(bitmap = imageResource(R.drawable.kotlin_aprentice))
    Image(bitmap = imageResource(R.drawable.kotlin_coroutines))
  }
}

Here, you added three existing images from the drawable folder to the screen. You also used ScrollableColumn() to make the wrapped content scrollable and to display it in a vertical orientation. What happens here is that you’ll show a Column, a vertical list of items. But if the items are too large to show all at once, it will be scrollable and you’ll be able to go through each item specifically.

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 ScrollableColumn is very easy, but there is much more you can do with it. Let’s explore how it works.

Exploring ScrollableColumn

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

@Composable
@OptIn(InternalLayoutApi::class)
fun ScrollableColumn(
    modifier: Modifier = Modifier,
    scrollState: ScrollState = rememberScrollState(0f),
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    reverseScrollDirection: Boolean = false,
    isScrollEnabled: Boolean = true,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    content: @Composable ColumnScope.() -> Unit
) {
    Column(
        modifier = modifier
            .verticalScroll(
                scrollState,
                isScrollEnabled,
                reverseScrolling = reverseScrollDirection
            )
            .padding(contentPadding),
        verticalArrangement = verticalArrangement,
        horizontalAlignment = horizontalAlignment,
        content = content
    )
}

First, look at the function parameters. Some of them you already know, 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.
  • reverseScrollDirection 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.
  • isScrollEnabled enables or disables scrolling. If it’s disabled, you can still programatically scroll to a specific position using the scrollState property. But the user can’t use scrolling gestures.
  • contentPadding adds padding around the content inside it. It adds the padding after the content is clipped.

These parameters are used in the body of the ScrollableColumn because a ScrollableColumn is a Column with a few extra default modifiers. Among these modifiers is verticalScroll, which adds the scrolling functionality to the Column.

Note how verticalScroll uses the parameters passed as arguments to ScrollableColumn. This means that you can actually make your custom composables scrollable as well, by applying the verticalScroll modifier. However, if you don’t have a specific case that requires it, you should use the provided composable.

The ScrollableColumn is a vertical scrollable container. If you want to apply horizontal scrolling, you use a ScrollableRow instead.

Using ScrollableRow

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 ScrollableRow. ScrollableRow is the counterpart implementation of ScrollableColumn, but it applies to Rows. This means that ScrollableRow uses horizontal arrangements and vertical alignments, while ScrollableColumn uses vertical arrangements and horizontal alignments.

Let’s implement a ScrollableRow. Inside MyScrollingScreen(), replace the ScrollableColumn with a ScrollableRow:

@Composable
fun MyScrollingScreen() {
  ScrollableRow { // here
    Image(bitmap = imageResource(R.drawable.advanced_architecture_android))
    Image(bitmap = imageResource(R.drawable.kotlin_aprentice))
    Image(bitmap = imageResource(R.drawable.kotlin_coroutines))
  }
}

You don’t have to do anything else! The ScrollableRow is almost identical to the ScrollableColumn in terms of the default behavior. It sets up the horizontal scroll automatically, without you passing in special parameters.

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, as in the previous examples. However, they aren’t a good idea for data collections that change at runtime. That’s because scrollables compose and render all the elements inside at the same time, 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 RecyclerView to optimize the loading and rendering of the visible elements on the screen. But how does Jetpack Compose deal with this issue? In the next section, you’ll 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 onscreen. 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 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 it should show on the screen. When you scroll, new elements are composed and the old ones are disposed of. 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 raywenderlich.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, if 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 = items,
        itemContent = {
          ListItem(it)
        }
      )
  }
}

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 of the itemContent() parameter, you used a ListItem(it). This lambda represents the function to transform each of the objects within items to a list of composable elements. This way you can call any number of composable functions to represent your items and you can add special 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 simple. 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(bookCategory.bookImageResources) {
  items(
      items = bookCategory.bookImageResources,
      itemContent = {
        BookImage(imageResource = it)
      }
  )
}

Similar to how you built a vertical list, 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),
    bitmap = imageResource(id = imageResource),
    contentScale = ContentScale.Fit
  )
}

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 imageResource to retrieve the correct asset. Finally, by using ContentScale.Fit you make the image adapt to the size you specified earlier.

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! :]

Lists are very easy to use and understand, especially because their signature requires only a few parameters to make them work. Let’s dive a bit deeper into their implementation.

Exploring Lists

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

@Composable
fun <T> 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,
    content: LazyListScope.() -> Unit
)

@Composable
fun <T> LazyRow(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    reverseLayout: Boolean = false,
    horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: LazyListScope.() -> Unit
)

The most important parameter to notice here is content which is used for 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 <T> items(
        items: List<T>,
        itemContent: @Composable LazyItemScope.(item: T) -> Unit
    )

    fun item(content: @Composable LazyItemScope.() -> Unit)

    fun <T> itemsIndexed(
        items: List<T>,
        itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
    )

    @ExperimentalFoundationApi
    fun stickyHeader(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.
  • itemsIndexed has same features as items function but also provides you with an index for each of your items.
  • 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 anotated annotated with @ExperimentalFoundationApi which means that it’s still in experimental stage and will 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 RV 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!

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.

Unfortunately, Jetpack Compose doesn’t include a ready-to-use component to accomplish the same thing. However, thanks to the power of Compose, building your own component isn’t hard. You’ll see how to do this step-by-step in this section.

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 list. There are two more elements next to it, but they’re marked as invisible in the image. That’s a little trick to position the last element properly in the first column — you add invisible elements to occupy the rest of the space. Otherwise, the last element would be in the center of the row.

There are the basic requirements of the grid, but let’s dive into the code to make your own grid.

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 GridView(columnCount: Int) {
  //TODO add your code here
}

@Composable
fun RowItem(rowItems: List<IconResource>) {
  //TODO add your code here
}

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

Implementing GridView

First, you’ll deal with GridView(). This composable takes a parameter named columnCount, which determines the maximum number of elements you need to place in each row.

Add the following code to the body of GridView:

@Composable
fun GridView(columnCount: Int) {
  val itemSize = items.size
  val rowCount = ceil(itemSize.toFloat() / columnCount).toInt()
  val gridItems = mutableListOf<List<IconResource>>()
  var position = 0
}

To fill the grid, you use the prepared list of icons called items. First, you store the item size, because you’ll use it multiple times.

You then calculate the number of rows you need to display the items. You get this value by dividing the number of items by the column count and using ceil() to ensure that you include the last row, even if it isn’t full. Now add the next piece of code start building a grid:

@Composable
fun GridView(columnCount: Int) {
  ...
  for (i in 0 until rowCount) {
    val rowItem = mutableListOf<IconResource>()
    for (j in 0 until columnCount) {
      if (position.inc() <= itemSize) {
        rowItem.add(IconResource(items[position++], true))
      }
    }
  // TODO
}

Next, for each row, you create a list of items that hold an IconResource. This is a model class that contains an icon resource and holds a Boolean property to set the icon’s visibility. All items added inside the row this way are set to visible by passing in true as the second constructor parameter. Because grids have rows and columns, you need to use a nested for loop to prepare all the items. The next step is to add empty dummy views and finally build the list:

@Composable
fun GridView(columnCount: Int) {
  ...
  for (i in 0 until rowCount) {
    val rowItem = mutableListOf<IconResource>()
    for (j in 0 until columnCount) {
      if (position.inc() <= itemSize) {
        rowItem.add(IconResource(items[position++], true))
      }
    }
    // here  
    val itemsToFill = columnCount - rowItem.size

    for (j in 0 until itemsToFill) {
      rowItem.add(IconResource(Icons.Filled.Delete, false))
    }
    gridItems.add(rowItem)
  }
  // here
  LazyColumn(modifier = Modifier.fillMaxSize()) {
    items(
        items = gridItems,
        itemContent = {
          RowItem(it)
        }
    )
  }
}

You calculate if there’s a need to include dummy invisible items by subtracting the current row size from the required columnCount. If columnCount is larger than rowItem.size it means you’re in the last row and it isn’t full. In that case, you add dummy icons along with the isVisible property as false, to make them invisible.

Finally, you use a LazyColumn, passing the rows that you calculated in gridItems.

RowItem() is a composable that renders each row inside the column. Implementing this is your next task. :]

Implementing RowItem

Each RowItem() will represent a series of GridIcons for that row. Replace the code of the RowItem() with the following:

@Composable
fun RowItem(rowItems: List<IconResource>) {
  Row {
    for (element in rowItems)
      GridIcon(element)
  }
}

Here, you use a Row to lay out the different items within a given row. Each item is then a GridIcon, which you’ll implement next.

Implementing GridIcon

Each GridItem() will show the icon you passed in, or show an invisible icon if you need to add dummy elements to the grid, to fill up the row. Replace the GridIcon with the following code to achieve such behavior:

@Composable
fun GridIcon(iconResource: IconResource) {
  val color = if (iconResource.isVisible)
    colorResource(R.color.colorPrimary)
  else Color.Transparent

  with(RowScope) {
    Icon(
      imageVector = iconResource.imageVector,
      tint = color,
      modifier = Modifier
        .size(80.dp, 80.dp)
        .weight(1f)
    )
  }
}

Here’s a breakdown of the previous code block. First, you calculated the color of the icon using the visibility property. Since Jetpack Compose doesn’t have an option to set a composable to invisible, you’ll achieve this result by using a transparent color.

Next, you add the calculated color as a tint to the Icon and set the size and weight modifiers. To use the weight modifier, Compose needs a Scope, which you get from the Row parent of GridIcon by using with(RowScope). Then within the with block, you get to use all the members from the RowScope, such as weight(). weight() is important to spread the icons evenly between other icons inside a Row().

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, replace the value of columnCount inside GridScreen() with the desired value to 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 ScrollableColumn as a parent to scroll the content vertically.
  • Use ScrollableRow as a parent to scroll the content horizontally.
  • You can make your own composables scrollable by adding the verticalScroll or horizontalScroll modifiers.
  • Use scrollers only for a fixed amount of content.
  • For dynamic and larger amounts of content, use lists 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, you need a custom implementation.
  • Use a transparent color or set an alpha to zero to make an invisible composable.
  • Alternatively, 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.