What You Will Build
A full task manager with an input field for adding new tasks, a progress counter (e.g., "2/5 completed"), a scrollable task list with checkboxes and delete buttons, and strikethrough styling for completed tasks. Tasks animate in and out using animateItem().
Why This Pattern Matters
Todo/task managers are the canonical CRUD app. This project teaches mutable state list management, Checkbox with data binding, item animations in LazyColumn, and TextDecoration for visual completion feedback.
Step 1: Task Model and State
data class Task(
val id: String = UUID.randomUUID().toString(),
val title: String,
var done: Boolean = false
)
var tasks by remember {
mutableStateOf(listOf(
Task(title = "Design the UI", done = true),
Task(title = "Implement navigation"),
Task(title = "Add animations"),
Task(title = "Write unit tests"),
Task(title = "Deploy to Play Store")
))
}
var newTask by remember { mutableStateOf("") }
Step 2: Input Row
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: Task List with Checkbox and Delete
Text(" ${tasks.count { it.done }}/${tasks.size} completed",
color = MaterialTheme.colorScheme.onSurfaceVariant)
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
- key = { it.id } is essential for
animateItem()to work. Without stable keys, Compose cannot track which items moved. - Immutable state updates: Use
tasks = tasks.map { ... }instead of mutating. Compose only recomposes when the state reference changes. - TextDecoration.LineThrough provides immediate visual feedback for completed tasks.
- New tasks prepended:
listOf(Task(...)) + tasksadds to the top. Usetasks + listOf(...)to append at the bottom.