Jetpack Compose, Android’s modern UI toolkit, offers a declarative way to build UIs. One of the most common UI patterns is displaying a list of data, which Compose handles efficiently and flexibly through the LazyColumn
and LazyRow
composables. These components allow developers to create dynamic, performant lists with minimal boilerplate. This blog post will guide you through building dynamic lists with Jetpack Compose, covering everything from basic lists to advanced customizations.
What are Dynamic Lists in Jetpack Compose?
Dynamic lists in Jetpack Compose are lists that can change over time in response to user interactions or data updates. The LazyColumn
and LazyRow
composables are designed to efficiently handle large or infinite lists by only composing and laying out the items that are currently visible on the screen. This lazy loading mechanism makes dynamic lists highly performant.
Why Use Dynamic Lists?
- Performance: Efficiently handles large datasets by only composing visible items.
- Flexibility: Supports dynamic content, updates, and animations.
- Declarative Syntax: Simplifies UI development with a more readable and maintainable code.
- Built-in Features: Offers built-in scrolling, item layouts, and state management.
How to Implement Dynamic Lists with Jetpack Compose
To implement dynamic lists, you’ll primarily use the LazyColumn
and LazyRow
composables. Here’s how:
Step 1: Set Up Your Project
Ensure you have the latest Jetpack Compose dependencies in your build.gradle
file:
dependencies {
implementation("androidx.compose.ui:ui:1.6.0") // Or newer
implementation("androidx.compose.material:material:1.6.0") // Or newer
implementation("androidx.compose.foundation:foundation:1.6.0") // Or newer
implementation("androidx.compose.runtime:runtime:1.6.0") // Or newer
implementation("androidx.activity:activity-compose:1.8.2") // Or newer
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.0")
}
Step 2: Basic LazyColumn
Example
Create a simple vertical scrolling list using LazyColumn
:
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun BasicLazyColumn() {
val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
LazyColumn(
contentPadding = PaddingValues(16.dp)
) {
items(items.size) { index ->
Text(text = items[index], modifier = Modifier.padding(8.dp))
}
}
}
@Preview(showBackground = true)
@Composable
fun BasicLazyColumnPreview() {
BasicLazyColumn()
}
This code defines a LazyColumn
that displays a list of text items with padding. The items
lambda efficiently populates the list based on the provided data.
Step 3: Basic LazyRow
Example
Create a simple horizontal scrolling list using LazyRow
:
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun BasicLazyRow() {
val items = listOf("Item A", "Item B", "Item C", "Item D", "Item E")
LazyRow(
contentPadding = PaddingValues(16.dp)
) {
items(items.size) { index ->
Text(text = items[index], modifier = Modifier.padding(8.dp))
}
}
}
@Preview(showBackground = true)
@Composable
fun BasicLazyRowPreview() {
BasicLazyRow()
}
The LazyRow
composable creates a horizontally scrolling list, perfect for displaying a row of items such as tags or buttons.
Step 4: Using Data Classes in Lists
For more complex data structures, use data classes:
data class ListItem(val id: Int, val title: String, val description: String)
@Composable
fun ListWithDataClass() {
val items = listOf(
ListItem(1, "Item 1", "Description for Item 1"),
ListItem(2, "Item 2", "Description for Item 2"),
ListItem(3, "Item 3", "Description for Item 3")
)
LazyColumn {
items(items) { item ->
Column(modifier = Modifier.padding(8.dp)) {
Text(text = item.title)
Text(text = item.description)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ListWithDataClassPreview() {
ListWithDataClass()
}
Using data classes makes it easier to manage and display structured data in your lists.
Step 5: Adding Item Keys for Efficiency
When the content of the list changes, it’s important to provide stable keys for each item. This helps Compose optimize recompositions. Use the key
parameter:
@Composable
fun ListWithKeys() {
val items = remember {
mutableStateListOf(
ListItem(1, "Item 1", "Description for Item 1"),
ListItem(2, "Item 2", "Description for Item 2"),
ListItem(3, "Item 3", "Description for Item 3")
)
}
LazyColumn {
items(
items = items,
key = { item -> item.id }
) { item ->
Column(modifier = Modifier.padding(8.dp)) {
Text(text = item.title)
Text(text = item.description)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ListWithKeysPreview() {
ListWithKeys()
}
Providing a unique and stable key (e.g., item.id
) ensures that Compose can efficiently update the list when items are added, removed, or reordered.
Step 6: Handling Clicks in Lists
To make the list interactive, handle item clicks:
import androidx.compose.foundation.clickable
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@Composable
fun ClickableList() {
val items = remember {
mutableStateListOf(
ListItem(1, "Item 1", "Description for Item 1"),
ListItem(2, "Item 2", "Description for Item 2"),
ListItem(3, "Item 3", "Description for Item 3")
)
}
val selectedItem = remember { mutableStateOf(null) }
LazyColumn {
items(
items = items,
key = { item -> item.id }
) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable {
selectedItem.value = item
},
backgroundColor = if (selectedItem.value == item) Color.LightGray else Color.White
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(text = item.title)
Text(text = item.description)
}
}
}
}
selectedItem.value?.let {
// Optionally display more details about the selected item
Text(text = "Selected: ${it.title}")
}
}
@Preview(showBackground = true)
@Composable
fun ClickableListPreview() {
ClickableList()
}
This example demonstrates how to add click listeners to list items and update the UI based on the selected item.
Advanced Techniques for Dynamic Lists
1. Adding Headers and Footers
You can easily add headers and footers to your LazyColumn
or LazyRow
:
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.Column
@Composable
fun ListWithHeadersAndFooters() {
val items = (1..5).map { "Item $it" }
LazyColumn {
item {
Text(text = "Header", modifier = Modifier.padding(8.dp))
}
items(items) { item ->
Text(text = item, modifier = Modifier.padding(8.dp))
}
item {
Text(text = "Footer", modifier = Modifier.padding(8.dp))
}
}
}
@Preview(showBackground = true)
@Composable
fun ListWithHeadersAndFootersPreview() {
ListWithHeadersAndFooters()
}
The item
block allows you to add composables that are not part of the repeating list.
2. Displaying Loading Indicators
Display a loading indicator while fetching data:
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.foundation.lazy.LazyColumn
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
@Composable
fun ListWithLoadingIndicator() {
val isLoading = remember { mutableStateOf(true) }
val items = remember { mutableStateOf(listOf()) }
// Simulate loading data
runBlocking {
delay(2000)
items.value = (1..5).map { "Item $it" }
isLoading.value = false
}
if (isLoading.value) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
LazyColumn {
items(items.value) { item ->
Text(text = item, modifier = Modifier.padding(8.dp))
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ListWithLoadingIndicatorPreview() {
ListWithLoadingIndicator()
}
3. Implementing Pull-to-Refresh
Add pull-to-refresh functionality to update the list data:
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshList() {
val refreshing = remember { mutableStateOf(false) }
val items = remember { mutableStateOf((1..5).map { "Item $it" }) }
val refreshScope = rememberCoroutineScope()
fun refresh() = refreshScope.launch {
refreshing.value = true
delay(1500) // Simulate network request
items.value = (6..10).map { "Refreshed Item $it" }
refreshing.value = false
}
val pullRefreshState = rememberPullRefreshState(refreshing.value, ::refresh)
Box(Modifier.pullRefresh(pullRefreshState)) {
LazyColumn(
Modifier.fillMaxSize(),
) {
items(items.value) { item ->
Text(text = item, modifier = Modifier.padding(8.dp))
}
}
PullRefreshIndicator(refreshing.value, pullRefreshState, Modifier.align(Alignment.TopCenter),
color = Color.Blue)
}
}
@Preview(showBackground = true)
@Composable
fun PullToRefreshListPreview() {
PullToRefreshList()
}
Conclusion
Dynamic lists are a fundamental part of modern Android UIs. With Jetpack Compose’s LazyColumn
and LazyRow
composables, creating and managing dynamic lists is both efficient and straightforward. By leveraging techniques like item keys, click handling, headers, footers, and pull-to-refresh, you can build complex and performant list-based UIs that provide a great user experience.