Home Android & Kotlin Tutorials

Lazy Layouts in Jetpack Compose

Learn how to use Lazy Composables in Jetpack Compose to simply display data in your app.

Version

  • Kotlin 1.6, Android 9.0, Android Studio 2021.2.1

One of the most common things apps do is display data. From your phone contacts to your favorite artist’s songs on Spotify, you’re always viewing sorted information that’s been formatted in some way — columns, rows, grids, and more.

Different platforms do this in different ways, of course, and methods have changed over time. Today’s Android apps use Jetpack Compose with Lazy composables — a modern, easy and efficient solution to display large lists of data. Android developers have evolved from using the now-deprecated ListView to the current RecyclerView. Both methods use XML code to represent the user interface and create adapters to handle each element.

Jetpack Compose was introduced at Google I/O 2019. It completely removed the XML code and offered a much easier way to handle common features like displaying data.

In this tutorial, you’ll learn about Lazy composables in Jetpack Compose. Specifically, you’ll learn:

  • What a Lazy composable is and how it works under the hood.
  • How to work with LazyColumn, LazyRow and Lazy grids.
  • About the main benefits of using them over the non-lazy options.
Note: This tutorial assumes you know the basics about Jetpack Compose. If you’re new to Jetpack Compose, check out Jetpack Compose Tutorial for Android: Getting Started.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

You’re going to build an app that allows you to display information about cute cats in list and grid formats using Lazy composables. The sample app has a very simple project structure:

  • data.model: The Cat model that represents the structure of what you’re going to retrieve from the internet service. Cute cats!
  • data.network: The requests to external services. Retrofit is the third-party library you’re going to use during this project to make HTTP requests. It’s straightforward and useful.
  • data.repository: FeedRepository has the responsibility to make the API calls and handle the response.
  • ui.theme: In this package lives some theme configurations that come by default with the Compose starter project boilerplate. They’re not the main focus of this article, though.
  • ui.cats: This is the starting point and the main package for this tutorial. You’re going to work with composables and learn how to integrate them into the app. The CatItem represents the list item, and CatFeedScreen is the whole composable screen that displays the information for those cute cats. You can also find the CatFeedViewModel that deals with the current state of the app here.

Build and run.

You’ll see an empty screen — that’s because you haven’t put the Lazy composables in place yet.

https://koenig-media.raywenderlich.com/uploads/2022/06/1-starter-app.png

Understanding Lazy Lists

To better understand the benefits Lazy composables offer, you first have to understand what they are and how they work.

Imagine that you want to display a large amount of data with an unknown number of items. If you decide to use a Column/Row layout, this could translate into a lot of performance issues because all the items will compose whether they’re visible or not. The Lazy option lets you lay out the components when they’re visible in the widget viewport. The available list of components includes LazyColumn, LazyRowand their grid alternatives.

Lazy composables follow the same set of principles as the RecyclerView widget but remove all that annoying boilerplate code.

Understanding LazyListScope

An interesting detail about Lazy composables is that they’re slightly different from other kinds of layouts because instead of expecting a @Composable instance, they offer a DSL block from LazyListScope. DSL, or Domain Specific Language, allows the app to create specific instructions to solve a specific problem. In these scenarios, Kotlin uses type-safe builders to create a DSL that fits perfectly for building complex hierarchical data structures in a semi-declarative way. The LazyListScope plays the role of a receiver scope in LazyRow and LazyColumn.

@LazyScopeMarker
interface LazyListScope {
    // 1
    fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)

    // 2
    fun items(
        count: Int,
        key: ((index: Int) -> Any)? = null,
        itemContent: @Composable LazyItemScope.(index: Int) -> Unit
    )
		
    // 3
    @ExperimentalFoundationApi
    fun stickyHeader(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
}

Here’s what’s happening in the code above:

  1. The item receiver allows adding a single composable item into the Lazy layout. You can add as many item receivers as you like, but if you want to add many, check the items option below.
  2. The items receiver expects a count of items instead of defining the content of every item individually. Here, you define the length of the list and create the specifications for every item.
  3. Finally, the stickyHeader adds a sticky item at the top. It will remain pinned even when scrolling. The header will remain pinned until the next header takes its place. This is very useful for sub-list scenarios like contact apps or movie categories.

In the next section, you’re going to practice what you’ve learned about Lazy composables and learn how to implement them in the app you’re building.

Adding Lazy Composables in Your App

Now it’s time for the fun part: coding.

Note: This tutorial uses version 1.1.1 of Jetpack Compose.

First, be sure you already have the right dependencies for this tutorial. Open the build.gradle file for the module, and notice it has the following dependencies:

def compose_version = "1.1.1"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"

Adding a LazyRow

Now that you have confirmed the dependencies, open CatItem.kt and look for the // TODO : Add list of tags comment. Replace it with this:

LazyRow(
    modifier = Modifier.align(Alignment.CenterHorizontally)
) {
  items(cat.tags) {
    CatTag(tag = it)
  }
}

The code above takes advantage of LazyRow to display the tags of a cat item in a horizontal scrollable view using the CatTag composable function.

If you’re a very detailed reader, you’ll notice the items method you’re implementing is slightly different from the previous one. This one expects a List of items instead of the count. But how is this possible?

Just remember that Kotlin offers extension functions to make your life easier. Command-click or Control-click the method, and you’ll be redirected to the implementation. The code is shown below:

inline fun <T> LazyListScope.items(
    items: List<T>,
    noinline key: ((item: T) -> Any)? = null,
    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
    itemContent(items[it])
}

Notice how the method passes the item’s size to LazyListScope.items and also deals with the keys for the scenario when they’re not null.

Adding a LazyColumn

Next, open CatFeedScreen.kt and replace the // TODO: Show cats in a scrollable column with the following code:

LazyColumn {
  stickyHeader {
    CuteCatsHeader()
  }
  items(cats) {
    CatItem(cat = it)
  }
}

Remember that the DSL for Lazy composables allows you to emit items of different types. In this case, you add a stickyHeader at the top of the list, followed by a list of CatItem using items. Per the stickyHeader documentation, it’s an experimental feature for now. The annotation ExperimentalFoundationApi has been added to the top of the file above your imports:

@file:OptIn(ExperimentalFoundationApi::class)

Adding a LazyVeriticalGrid

Last but not least, the LazyVerticalGrid makes it easy to implement a grid view. This layout is also an experimental version, so the ExperimentalFoundationApi annotation is useful for this as well. To implement this, find the //TODO: Display cats in grid view comment and replace it with the code below:

LazyVerticalGrid(
 cells = GridCells.Fixed(2),
) {
  item {
    CuteCatsHeader()
  }
  items(cats) {
    CatItem(cat = it)
  }
}

The code above adds a LazyVerticalGrid to serve as the grid view. This layout offers some different elements — like the cells parameter where you define what kind of grid you want, fixed or adaptative.

Here, you use a fixed length of two items using GridCells.Fixed(2). At the end, you’re just adding the list of CatItem, like in the previous step, but this time it will render in grid mode. Also, the item receiver emits an item intending it to be part of the list of elements that render on the grid, no matter if it’s of the same type or not. This is incredibly beneficial for multiple scenarios. In the old RecyclerView / GridView approach, you needed to create multiple adapters for a View, but now you just need a single item.

Build and run, to grasp what you achieved tap on the toggle icon on the top right corner and it will automatically change the layout. Here’s what you’ll see:

https://koenig-media.raywenderlich.com/uploads/2022/06/2-add-lazy-composables.gif

Congrats! You finished this section successfully. In the following sections, you’re going to improve this app and take it to another level.

Spacing Your Data Items

You may have noticed that the elements inside your Lazy composables are very close together — and it doesn’t look ideal. Fortunately, Lazy composables offer attributes to handle these scenarios efficiently.

To add some spacing between items, you can use the Arrangement.spacedBy via the verticalArrangement and horizontalArrangement parameters. To add padding around the edges of the content, pass PaddingValues to the contentPadding parameter.

Adding Space to a LazyRow

Open the CatItem.kt file and go to the CatItem composable. Find the LazyRow that you implemented previously and add this code:

LazyRow(
    modifier = Modifier.align(Alignment.CenterHorizontally),
    // New horizontal content spacing
    horizontalArrangement = Arrangement.spacedBy(12.dp),
) 
...
}

The code above adds space between the items inside the row of tags. As you can imagine, the horizontalArrangement parameter is only available for LazyRow just as verticalArrangement is only available for LazyColumn.

Adding Space to a LazyColumn

Next, move to CatFeedScreen.kt and then to the LazyListCats composable. Find the LazyColumn you implemented previously and add these lines of code as parameters:

@Composable
fun LazyListCats(cats: List<Cat>, state: LazyListState) {
  LazyColumn(
      // New content padding 
      contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp),
      // New vertical spacing 
      verticalArrangement = Arrangement.spacedBy(12.dp),
  ) {
    ...
}

The content padding adds some padding around the content of the LazyColumn — specifically, 32.dp horizontal and 16.dp vertical. Very easy, right? As you’ve already learned, the arrangement will add spacing between the items of the column, but this time on the vertical axis.

Adding Space to a LazyVerticalGrid

In the same file, look for the LazyGridCats composable. Find the LazyColumn you implemented previously, and add these lines of code as parameters:

@Composable
fun LazyGridCats(cats: List<Cat>, state: LazyListState) {
  LazyVerticalGrid(
      cells = GridCells.Fixed(2),
      // Content padding for the grid
      contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
      // LazyGrid supports both vertical and horizontal arrangement
      verticalArrangement = Arrangement.spacedBy(8.dp),
      horizontalArrangement = Arrangement.spacedBy(16.dp),
  ) {
    ...
  }
}

Notice the LazyVerticalGrid also supports the content padding and arrangement, and in this case, both horizontal and vertical at the same time. It’s very helpful for dealing with different spacing scenarios on lists and grids without modifying the child composable.

Build and run, to grasp what you achieved tap on the toggle icon on the top right corner and it will automatically change the layout. Here’s what you’ll see:

https://koenig-media.raywenderlich.com/uploads/2022/06/3-spacing-composables.gif

Hooray! You achieved a new goal. The app now looks amazing, and you learned about some of the properties the Lazy composables offer to you. Now it’s time to add more fantastic features to this app.

Dealing With State on Lazy Composables

One of the biggest advantages of Lazy composables over non-lazy approaches like Column or Row is that you can interact with the state of the layout. But what exactly is the state? It’s an object you can use to control and observe scrolling. You’ll access this using the rememberLazyListState method. You can easily access different attributes of this object that allow you to create fabulous features — like the scroll-to-the-top feature you’ll add now with just a few lines of code.

Passing State to the Lazy Composables

You probably already noticed the LazyGridCats and LazyListCats composables have an unused LazyListState parameter. To take advantage of it, go to the CatsFeedScreen.kt file, then find the following composables. Finally, change the following to define the state parameter for both composables.

@Composable
fun LazyGridCats(cats: List<Cat>, state: LazyListState) {
  LazyVerticalGrid(
      cells = GridCells.Fixed(2),
      contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
      verticalArrangement = Arrangement.spacedBy(8.dp),
      horizontalArrangement = Arrangement.spacedBy(16.dp),
      // Add LazyListState controller for grid
      state = state,
  ) {
    ...
  }
}

@Composable
fun LazyListCats(cats: List<Cat>, state: LazyListState) {
  LazyColumn(
      contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp),
      verticalArrangement = Arrangement.spacedBy(12.dp),
      // Add LazyListState controller for column
      state = state,
  ) {
    ...
  }
}

This will allow you to have control and give instructions to the Lazy composables in a very straightforward way.

Understanding remember and derivedStateOf

In the same file, find the //TODO: Use derivedStateOf to check index state comment and replace it with the following code:

val showScrollToTop = remember {
  derivedStateOf {
    lazyListState.firstVisibleItemIndex > 0
  }
}

showScrollToTop is a variable that’s true if the first visible item on the list is greater than the first item in the entire list.

Here’s how this works:

  • remember is a method that returns the value produced during the composition of the composable. During recomposition, it will always return the value produced by composition. That’s incredibly useful to perform some heavy operations and avoid being called again for every recomposition.
  • derivedStateOf creates an object whose value returns a cached result. Calling the value from this object repeatedly won’t cause the operation to call again. Taking advantage of it, the showScrollToTop variable will change only when the instance of LazyListState changes its value, and not for every recomposition or invocation of the variable.

Using State to Scroll to the Top of the List

In the same file, look for the //TODO: Show button when index is bigger than the first comment and replace it with the following code:

AnimatedVisibility(visible = showScrollToTop.value) {
  ScrollToTop(state = lazyListState)
}

The AnimateVisibility composable is responsible for making the scrolling animation, depending on the value of the showScrollToTop variable. The content inside is a simple button that will emit the scroll event when it’s clicked.

In the same file, look for the ScrollToTop composable and find the //TODO: Animate list state to the first index comment. Replace it with the following code:

coroutineScope.launch {
  state.animateScrollToItem(index = 0)
}

You’re using the coroutineScope to do the animation. As you may notice, the state has access to a method that allows animating an item by index. So for this example, you’ll push to the top, which means scroll to index 0.

Build and run. Here’s what you’ll see:

https://koenig-media.raywenderlich.com/uploads/2022/06/4-managing-state.gif

How exciting! The basic functionality of the app is done. The animation is smooth and clean, and it looks awesome. Now you have a better understanding of how to manage the state of Lazy composables.

Performance Concerns with Lazy Composables

Performance is a key piece of any software — including mobile apps. Taking the time to care about the small details during your development process could make a great difference in how your users experience your app. So in this section, you’re going to learn some tips on how to improve the performance of your Lazy composables.

Adding Keys to List Items

The first tip is a classic improvement for any group of indexed data — like lists or grids. The item’s key permits every item inside a list to have a unique identifier. This provides a lot of improvements — especially if you want to reorder the list, animate some items, do some tests on the list, and much more. Just remember that for now, this is only available for LazyRow and LazyColumn, not for grids.

Open the CatFeedScreen.kt file, and go to the LazyListCats composable. Add this line inside the items method:

items(
    cats,
    // Add a key for every item
    key = { it.id },
) {
  CatItem(cat = it)
}

By adding the code above, every item in the list now has a key represented by it’s unique id.

Avoiding Operations Inside of Composables

Another way to improve the app performance is to avoid doing some operations inside the composables. Operations like validations, assertions and others should move away from the composable body — instead, take advantage of the remember method.

In the same CatFeedScreen.kt file, look for the //TODO: Use remember for showGrid comment and replace it with the following code:

val drawableIcon = remember(showGrid) {
  if (showGrid) {
    R.drawable.ic_baseline_list_24
  } else {
    R.drawable.ic_baseline_grid_on_24
  }
}

Using remember allows that the drawableIcon will change only when the showGrid parameter changes, and not every time the layout is recomposed — making that component incredibly more efficient.

Finally, replace the code block that’s inside the painterResource property inside the Icon composable with the drawableIcon variable, like this:

painterResource(id = drawableIcon),

You’re done! The app’s appearance won’t change much from the previous step, but you made a lot of improvements in its performance. This could make the difference between a 4-star and 5-star app, so always take it into account.

Where to Go From Here

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this article, you learned about :

  • How Lazy composables work under the hood.
  • The different types of Lazy composables available.
  • How to use Lazy composables in your app.
  • The different kinds of properties the Lazy composables offer.
  • How to manage the LazyListState object and develop amazing features.
  • Performance tips for Lazy composables.

To learn more, take a look at resources like the Jetpack Compose by Tutorial book and Jetpack Compose Video Course.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Contributors

Comments

Reviews

More like this