April 3

0 comments

Jetpack Compose LazyColumn Example

By Val Okafor

April 3, 2023

todo list

In this tutorial, we will learn some practical usages of Jetpack Compose LazyColumn, a UI component in the Jetpack Compose toolkit that allows you to efficiently display a scrollable list of items in a vertically scrolling column.  It’s similar to Recyclerview in the classic Android View system. It is designed to efficiently handle large data lists by only rendering the currently visible items on the screen rather than rendering them all at once. It has slots for items, items & items indexed.

The item slot accepts a Composable that it displays once, and you can use it to display an item that should appear once in the list, such as section headers. The items slot is used to display repeatable items.

Just like with Recyclerview, you still need to create a layout for how each item in the list should be displayed. You pass this Composable to LazyColumn and manages uses it to display each item in the list. As the user scrolls through the list, LazyColumn updates the UI by recycling previously created components to display new items that become visible. This is similar to the custom layout XML you needed to provide to the ViewHolder in RecylerverView.

In this tutorial, we will use LazyColumn to display a list of mock Tasks, as shown in Figure 1.

Figure 1 – List of mock tasks

We only require a few lines of code to display the above list with LazyColumn, as shown in Code Snippet 1

      Column() {
            LazyColumn(
                verticalArrangement = Arrangement.spacedBy(8.dp),
                contentPadding = innerPadding
            ) {
                items(SampleData.getSampleTasks()) { task ->
                    TaskCard(task)
                }
            }
        }
Code Sample 1: Lazy column showing list of tasks.

As you can see, a list of sample tasks was passed to the items slot of LazyColumn, and it loops through that list and calls the TaskCard with each task. TaskCard is composable that creates the layout for each item. It accepts a TaskModel object. Code sample 2 shows this data class.

data class TaskModel(
    val title: String = "",
    val dueDate: LocalDate? = null,
    val dueTime: LocalTime? = null,
    val priority: Priority = Priority.LOW,
    val hasReminder: Boolean = false,
    val isCompleted: Boolean = false,
)
Code Sample 2: Task Model data class

Add Task Card

The TaskCard is a composable function where we design the custom layout for each item shown in the list, as shown in Figure 2.

Figure 2 – Todo List Card

This custom list contains the following component,

  • Card – root layout with rounded corner shape
  • Row layout – the first child of the Card, divided into two, with the first 5.dp used to show the priority color of the Task.
  • Column – with three rows. Row 1 shows the title and radio button, row 2 displays the date and time, and row 3 shows the reminder if set.

TaskCard also accepts an onCompleteCheckClicked callback that is used to notify when an item in the list is clicked, as shown in sample 3

fun TaskCard(task: TaskModel,  onCompleteCheckClicked: (TaskModel) -> Unit = {})
Code Sample 3: TaskCard

The root layout for this TaskCard is Material Design 3 Card, as shown in Code Sample 4.

@Composable
fun TaskCard(task: TaskModel) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant,
            contentColor = MaterialTheme.colorScheme.onSurface,
        ),
        shape = RoundedCornerShape(12.dp),
        modifier = Modifier
            .wrapContentHeight()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        
    }
}
Code Sample 4: Task Card root layout

To complete the TaskCard, add a Column layout to the Card and three rows inside the empty Column. The first row contains Text for the task title and a checkbox to mark the task as complete. The checkbox and text are wrapped with a toggleable row so the user can check a task by touching anywhere in the row. The first row is shown in Code Sample 5

Row(Modifier.fillMaxWidth().toggleable(
        value = task.isCompleted,
        onValueChange = { onCompleteCheckClicked(task) },
        role = Role.Checkbox),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = task.title,
            style = MaterialTheme.typography.titleMedium,
            color = MaterialTheme.colorScheme.onSurface,
            modifier = Modifier.weight(1f))
        Checkbox(
            checked = task.isCompleted,
            onCheckedChange = null,
            colors = CheckboxDefaults.colors(
                checkedColor = MaterialTheme.colorScheme.tertiary,
                uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant,
                checkmarkColor = MaterialTheme.colorScheme.surface
            ),
            modifier = Modifier.padding(start = 16.dp)
        )
    }
Code Sample 5: Toggleable Row

The next row contains date and time text and icons. We use Spacer to separate the two rows vertically like so Spacer(modifier = Modifier.height(4.dp)). Add the next Row shown in Code Sample 6. Notice the user of Spacer again to separate the time and date values.

                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Icon(
                        imageVector = Icons.Default.CalendarMonth,
                        contentDescription = "Calender icon",
                        tint = if (task.isOutDate()) {
                            MaterialTheme.colorScheme.error
                        } else {
                            MaterialTheme.colorScheme.onSurfaceVariant
                        }
                    )

                    Text(
                        text = task.getTaskDate(),
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        style = MaterialTheme.typography.bodyMedium,
                        modifier = Modifier.padding(horizontal = 8.dp)
                    )

                    Spacer(
                        modifier = Modifier
                            .height(16.dp)
                            .width(1.dp)
                            .background(MaterialTheme.colorScheme.outline)
                    )

                    Icon(
                        painter = painterResource(id = R.drawable.ic_baseline_access_time_24),
                        contentDescription = "Calender icon",
                        tint = if (task.isOutDate()) {
                            MaterialTheme.colorScheme.error
                        } else {
                            MaterialTheme.colorScheme.onSurfaceVariant
                        },
                        modifier = Modifier.padding(start = 8.dp)
                    )

                    Text(
                        text = task.getTaskTime(),
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        style = MaterialTheme.typography.bodyMedium,
                        modifier = Modifier.padding(horizontal = 8.dp)
                    )
                }
Code Sample 6: Row containing date and time

The last row is optional because some tasks may not have a reminder, so we need to wrap that row with an if statement, as shown in the next code sample.

                if (task.hasReminder) {
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        modifier = Modifier
                            .background(
                                color = MaterialTheme.colorScheme.secondaryContainer,
                                shape = RoundedCornerShape(16.dp)
                            )
                    ) {
                        Text(
                            text = stringResource(id = R.string.reminder_on),
                            color = MaterialTheme.colorScheme.tertiary,
                            modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp)
                        )

                        Icon(
                            imageVector = Icons.Outlined.Alarm,
                            contentDescription = "Alarm icon",
                            tint = MaterialTheme.colorScheme.tertiary,
                            modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp)
                        )
                    }
                }
Code Sample 7: Row showing reminder

Add Sample Data

We need sample data to display on the LazyColumn list I create a list of dummy tasks that you can get from in this Gihub gist.

Passing Data to LazyColumn

As shown in Code Sample 1, we can just pass a list of data to LazyColumn. If the composable containing the list is called ProntoList, we can simply call it like this and get the checked item in the lambda.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ProntoList(){ task ->
                Timber.d("${task.title} checked")
            }
        }
    }
}
Code Sample 8 – MainActivity calling LazyColumn

If we want to know the position of the clicked/selected item, we can track it with itemsIndexed, and LazyColumn exposes the index of each item as shown in sample 9.

 Column() {
            LazyColumn(
                verticalArrangement = Arrangement.spacedBy(8.dp),
                contentPadding = innerPadding
            ) {
                itemsIndexed(viewModel.sampleTasks) { index, item ->
                    TaskCard(item, index) {position ->
                        viewModel.onTaskChecked(position)
                    }
                }
            }
        }
Code Sample 9: LazyColumn ItemsIndezed.

The index could be passed to a ViewModel, where we track/update the list of the sample data in a mutablestatelist as shown in sample 10.

class TaskListViewModel(): ViewModel() {
    private val _sampleTasks  = SampleData.getSampleTasks().toMutableStateList()

    val sampleTasks: List<TaskModel>
        get() = _sampleTasks

    fun onTaskChecked(index: Int) {
        val task = sampleTasks[index]
        val updatedTask = task.copy(isCompleted = !task.isCompleted)
        _sampleTasks[index] = updatedTask
    }
}
Code Sample 10: Sample ViewModel to back LazyColumn

Summary

Overall, Jetpack Compose LazyColumn provides a powerful and efficient way to display large amounts of data in a scrollable list, making it a valuable tool for creating complex UI layouts in modern Android app development. You can download the example source code from Github.

Val Okafor

About the author

I am a family-first Software Engineer and Creator. I create mobile-first apps that grow businesses and increase efficiency. I learn and share my journey figuring out the money side of code. It is written "Man shall not live by code alone"

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Never miss a good story!

 Subscribe to our newsletter to keep up with the latest trends!

>