Phase 1 complete track

Build a complete beginner Compose habit tracker.

This page is designed so a beginner does not need to leave the tutorial to find missing code. It gives the setup, file structure, imports, full screen code, expected UI, common errors, and checkpoints.

Step 1

Create the project

  1. Open Android Studio.
  2. Select New Project.
  3. Choose Empty Activity.
  4. Name the app HabitTracker.
  5. Use Kotlin and Jetpack Compose.
  6. Run the default app once before changing code.
If the default app does not run, do not continue. Fix Android Studio, emulator, SDK, or Gradle setup first. A tutorial cannot compensate for a broken local environment.
Step 2

Use this file structure

For Phase 1, keep everything in one file. This is deliberate. Architecture comes later after you can build working UI.

app/
src/main/
java/com/example/habittracker/
MainActivity.kt <- paste the full code here
AndroidManifest.xml
build.gradle.kts
Step 3

Paste this complete MainActivity.kt

Replace the package line with your actual package if Android Studio created a different one.

package com.example.habittracker

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    HabitTrackerApp()
                }
            }
        }
    }
}

data class Habit(
    val id: Int,
    val title: String,
    val isDone: Boolean = false
)

@Composable
fun HabitTrackerApp() {
    var nextId by remember { mutableStateOf(1) }
    var title by remember { mutableStateOf("") }
    var habits by remember {
        mutableStateOf(
            listOf(
                Habit(id = 0, title = "Run the first Compose app")
            )
        )
    }

    val completedCount = habits.count { it.isDone }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Habit Tracker",
            style = MaterialTheme.typography.headlineMedium
        )

        Text(
            text = "$completedCount of ${habits.size} habits complete",
            style = MaterialTheme.typography.bodyLarge
        )

        AddHabitForm(
            title = title,
            onTitleChange = { title = it },
            onAdd = {
                val cleanTitle = title.trim()
                if (cleanTitle.isNotEmpty()) {
                    habits = habits + Habit(id = nextId, title = cleanTitle)
                    nextId += 1
                    title = ""
                }
            }
        )

        if (habits.isEmpty()) {
            EmptyHabitState()
        } else {
            HabitList(
                habits = habits,
                onToggle = { selectedHabit ->
                    habits = habits.map { habit ->
                        if (habit.id == selectedHabit.id) {
                            habit.copy(isDone = !habit.isDone)
                        } else {
                            habit
                        }
                    }
                },
                onDelete = { selectedHabit ->
                    habits = habits.filterNot { it.id == selectedHabit.id }
                }
            )
        }
    }
}

@Composable
fun AddHabitForm(
    title: String,
    onTitleChange: (String) -> Unit,
    onAdd: () -> Unit
) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = title,
            onValueChange = onTitleChange,
            label = { Text("New habit") },
            singleLine = true
        )

        Button(
            onClick = onAdd,
            enabled = title.trim().isNotEmpty()
        ) {
            Text("Add habit")
        }
    }
}

@Composable
fun HabitList(
    habits: List<Habit>,
    onToggle: (Habit) -> Unit,
    onDelete: (Habit) -> Unit
) {
    LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        items(items = habits, key = { it.id }) { habit ->
            HabitRow(
                habit = habit,
                onToggle = { onToggle(habit) },
                onDelete = { onDelete(habit) }
            )
        }
    }
}

@Composable
fun HabitRow(
    habit: Habit,
    onToggle: () -> Unit,
    onDelete: () -> Unit
) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(12.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = habit.isDone,
                onCheckedChange = { onToggle() }
            )
            Text(
                modifier = Modifier.weight(1f),
                text = habit.title,
                textDecoration = if (habit.isDone) TextDecoration.LineThrough else null
            )
            Button(onClick = onDelete) {
                Text("Delete")
            }
        }
    }
}

@Composable
fun EmptyHabitState() {
    Column {
        Text("No habits yet.")
        Spacer(modifier = Modifier.height(4.dp))
        Text("Add one habit to start tracking progress.")
    }
}

@Preview(showBackground = true)
@Composable
fun HabitTrackerPreview() {
    MaterialTheme {
        HabitTrackerApp()
    }
}
Step 4

Understand the important blocks

Habit data classRepresents one row in the app. The UI reads id, title, and isDone.
remember stateKeeps title, habits, and nextId available across recomposition.
CallbacksChild composables ask the parent to change state instead of changing hidden global values.
AddHabitFormOwns the text input UI but receives the current title and event handlers from the parent.
HabitListUses LazyColumn so the list pattern works when rows grow.
HabitRowDisplays one habit and exposes toggle/delete actions.
EmptyHabitStatePrevents the beginner anti-pattern of showing a blank screen.
Step 5

Common errors and fixes

Unresolved reference: LazyColumnAdd the lazy imports at the top of MainActivity.kt. Android Studio can also auto-import them.
Composable calls can only happen...You called a composable from a normal function. Composables must be called inside another composable or setContent.
Button stays disabledCheck that the TextField uses value = title and onValueChange = onTitleChange.
List item toggles the wrong rowMake sure every Habit has a stable unique id and LazyColumn uses key = { it.id }.
Step 6

Checkpoint before Phase 2

  • You can run the app after pasting the code.
  • You can add, complete, and delete habits.
  • You can explain why title and habits are state.
  • You can add one new field to Habit and display it in HabitRow.
  • You can complete the Phase 1 quiz lab without guessing.