Building Dynamic Lists with Jetpack Compose

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.