Home Android & Kotlin Books Jetpack Compose by Tutorials

4
Building Lists with Jetpack Compose Written by Tino Balint

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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 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 extra modifiers that enable 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 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 configuration and handles the scroll behavior during the recomposition so that the position is not lost.

What happens here is that you’ll show a Column, a vertical list of items. But if the items are too large to show them all at once, it will be scrollable and you’ll be able to go through each item respectively.

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
)

Using horizontal scrolling modifiers

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

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

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.

Introducing LazyColumn & LazyRow

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

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.

Book Categories
Siit Wexenereup

@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
}
@Composable
fun MyList() {
  LazyColumn {
    items(items) { item -> ListItem(item) }
  }
}
@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
  }
}
LazyRow {
  items(bookCategory.bookImageResources) { items ->
    BookImage(items)
  }
}
@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)
  )
}
List
Raxc

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 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(),
  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(),
  content: LazyListScope.() -> Unit
)
interface LazyListScope {

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

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

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

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.

Grid Calculation
Gzit Yervocemoas

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 RowScope.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.

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

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

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 RowScope.GridIcon(iconResource: IconResource) {
  val color = if (iconResource.isVisible)
    colorResource(R.color.colorPrimary)
  else Color.Transparent

    Icon(
      imageVector = iconResource.imageVector,
      tint = color,
      contentDescription = stringResource(R.string.grid_icon),
      modifier = Modifier
        .size(80.dp, 80.dp)
        .weight(1f)
    )
}
Grid With Three Columns
Pket Yozn Stpuo Bovorvw

@ExperimentalFoundationApi
@Composable
fun GridScreen() {
  LazyVerticalGrid(
    modifier = Modifier.fillMaxSize(),
    cells = GridCells.Fixed(3),
    content = {
      items(items) { item ->
        GridIcon(IconResource(item, true))
      }
    }
  )

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}
@Composable
fun GridIcon(iconResource: IconResource) {
  val color = if (iconResource.isVisible)
    colorResource(R.color.colorPrimary)
  else Color.Transparent

  Icon(
    imageVector = iconResource.imageVector,
    tint = color,
    contentDescription = stringResource(R.string.grid_icon),
    modifier = Modifier
      .size(80.dp, 80.dp)
  )
}

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 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, use a custom implementation.
  • If you use LazyVerticalGrid, keep in mind that it might soon be changed or removed.
  • 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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.