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.

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 classAdd 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.

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 layoutTo 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 RowThe 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 reminderAdd 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 LazyColumnIf 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 LazyColumnSummary
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.