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(...)) + tasks adds to the top. Use tasks + listOf(...) to append at the bottom.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.