Lazy Layouts in Jetpack Compose
Learn how to use Lazy Composables in Jetpack Compose to simply display data in your app. By Enzo Lizama.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Lazy Layouts in Jetpack Compose
20 mins
- Getting Started
- Understanding Lazy Lists
- Understanding LazyListScope
- Adding Lazy Composables in Your App
- Adding a LazyRow
- Adding a LazyColumn
- Adding a LazyVeriticalGrid
- Spacing Your Data Items
- Adding Space to a LazyRow
- Adding Space to a LazyColumn
- Adding Space to a LazyVerticalGrid
- Dealing With State on Lazy Composables
- Passing State to the Lazy Composables
- Understanding remember and derivedStateOf
- Using State to Scroll to the Top of the List
- Performance Concerns with Lazy Composables
- Adding Keys to List Items
- Avoiding Operations Inside of Composables
- Where to Go From Here
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.
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, andCatFeedScreen
is the whole composable screen that displays the information for those cute cats. You can also find theCatFeedViewModel
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.
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
, LazyRow
and 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:
- The
item
receiver allows adding a singlecomposable
item into the Lazy layout. You can add as manyitem
receivers as you like, but if you want to add many, check theitems
option below. - 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. - 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.
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:
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:
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, theshowScrollToTop
variable will change only when the instance ofLazyListState
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:
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!