Build a Compose habit tracker from zero.
Outcome: one runnable Android app where the user can add habits, mark them done, delete them, and understand how Compose state updates the screen.
Create and verify the app shell
Create an Empty Activity Compose project named HabitTracker. Run it before changing code.
Replace the first screen
Open MainActivity.kt. Keep the generated package line. Replace the body with a simple composable.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MaterialTheme { HabitTrackerApp() } }
}
}
@Composable
fun HabitTrackerApp() {
Text("Habit Tracker")
}Add the data model
Add this below the imports. A habit needs a stable id so list actions affect the correct row.
data class Habit(
val id: Int,
val title: String,
val isDone: Boolean = false
)Add text input state
Use remember so Compose keeps the current text while the screen redraws.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun HabitTrackerApp() {
var title by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Text("Habit Tracker")
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = title,
onValueChange = { title = it },
label = { Text("New habit") }
)
Button(
enabled = title.trim().isNotEmpty(),
onClick = { title = "" }
) { Text("Add habit") }
}
}Add list state and render rows
Now store a list of habits and render it with LazyColumn.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.foundation.layout.Row
@Composable
fun HabitTrackerApp() {
var nextId by remember { mutableStateOf(1) }
var title by remember { mutableStateOf("") }
var habits by remember { mutableStateOf(listOf<Habit>()) }
Column(modifier = Modifier.padding(16.dp)) {
Text("Habit Tracker")
OutlinedTextField(value = title, onValueChange = { title = it }, label = { Text("New habit") })
Button(enabled = title.trim().isNotEmpty(), onClick = {
habits = habits + Habit(nextId, title.trim())
nextId += 1
title = ""
}) { Text("Add habit") }
if (habits.isEmpty()) {
Text("No habits yet. Add one to start.")
} else {
LazyColumn {
items(habits, key = { it.id }) { habit ->
Card(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
Row(modifier = Modifier.padding(12.dp)) {
Checkbox(checked = habit.isDone, onCheckedChange = null)
Text(habit.title)
}
}
}
}
}
}
}Add complete and delete actions
Update the row so each habit can change independently.
items(habits, key = { it.id }) { habit ->
Card(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
Row(modifier = Modifier.padding(12.dp)) {
Checkbox(
checked = habit.isDone,
onCheckedChange = {
habits = habits.map {
if (it.id == habit.id) it.copy(isDone = !it.isDone) else it
}
}
)
Text(modifier = Modifier.weight(1f), text = habit.title)
Button(onClick = {
habits = habits.filterNot { it.id == habit.id }
}) { Text("Delete") }
}
}
}Refactor into small composables
Split the screen into AddHabitForm, HabitList, and HabitRow. The behavior should not change.
Final review
- Blank habits cannot be added.
- Each row has a stable id.
- Empty state appears when all rows are deleted.
- State lives in the parent screen.
- Child rows use callbacks instead of editing hidden global data.
Common failures
Checkpoint quiz
Use the interactive Phase 1 quiz after completing the build. If you guess, revisit the exact step that introduced that concept.
Open quiz lab