What You Will Build
A task management app with an input field for adding new tasks, checkboxes for marking completion, strikethrough text on done items, swipe-to-delete via an icon button, and a live completion counter. This is the core pattern behind apps like Todoist and Microsoft To Do.
Why This Pattern Matters
Task lists are the most common CRUD pattern in mobile apps. This implementation teaches you how to manage a mutable list of data class objects, handle checkbox state changes immutably with .map and .copy, and use .filter for deletion.
Step 1: Define the Task Model
data class Task(
val id: String = java.util.UUID.randomUUID().toString(),
val title: String,
var done: Boolean = false
)
Step 2: Build the Input Row
An OutlinedTextField paired with a FilledIconButton lets users type and submit new tasks:
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = newTask,
onValueChange = { newTask = it },
modifier = Modifier.weight(1f),
placeholder = { Text("New task...") },
singleLine = true
)
Spacer(Modifier.width(8.dp))
FilledIconButton(onClick = {
if (newTask.isNotBlank()) {
tasks = listOf(Task(title = newTask)) + tasks
newTask = ""
}
}) {
Icon(Icons.Default.Add, "Add")
}
}
Step 3: Build the Task List with Checkboxes
LazyColumn(modifier = Modifier.weight(1f)) {
items(tasks, key = { it.id }) { task ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.animateItem()
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = task.done,
onCheckedChange = { checked ->
tasks = tasks.map {
if (it.id == task.id) it.copy(done = checked) else it
}
}
)
Text(
task.title,
modifier = Modifier.weight(1f),
textDecoration = if (task.done)
TextDecoration.LineThrough else null,
color = if (task.done)
MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface
)
IconButton(onClick = {
tasks = tasks.filter { it.id != task.id }
}) {
Icon(Icons.Default.Delete, "Delete",
tint = MaterialTheme.colorScheme.error)
}
}
}
}
}
Tips and Pitfalls
- Immutable updates are critical. Use
tasks.map { ... }and.copy()rather than mutating in place. Compose only detects state changes through new object references. - animateItem() on the Card gives smooth insertion and removal animations in the LazyColumn.
- Prepend new tasks (listOf(new) + tasks) so users see them immediately at the top without scrolling.
- Completion counter:
tasks.count { it.done }/tasks.sizeupdates reactively because tasks is a state variable.
Min SDK: 21 | Compose BOM: 2024.01.00+