CS 3180 Mobile Application Development

Week 4 Wednesday: State in Jetpack Compose

CS 3180 Mobile Application Development

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Today’s Demo Plan

  1. What is state in Compose? (20 min)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Today’s Demo Plan (continued)

  1. What is state in Compose? (20 min)
  2. Working with state (25 min)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Today’s Demo Plan (continued)

  1. What is state in Compose? (20 min)
  2. Working with state (25 min)
  3. State hoisting (30 min)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

What is State?

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

What is State? (continued)

  • Any value that can change over time
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

What is State? (continued)

  • Any value that can change over time
  • UI should update when state changes
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

The Problem Without State

@Composable
fun HoleScore() {
    var strokes = 0  // ❌ Won't work!

    Button(onClick = { strokes++ }) {
        Text("Strokes: $strokes")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

The Fix: remember + mutableStateOf

@Composable
fun HoleScore() {
    var strokes by remember { mutableStateOf(0) }

    Button(onClick = { strokes++ }) {
        Text("Strokes: $strokes")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Recomposition Basics

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Recomposition Basics (continued)

  • State changes trigger recomposition
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Recomposition Basics (continued)

  • State changes trigger recomposition
  • Composables can run multiple times
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Types

// Property delegation (preferred)
var strokes by remember { mutableStateOf(0) }

// Without delegation
val strokes = remember { mutableStateOf(0) }
strokes.value++
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Multiple State Variables

@Composable
fun ScoreEntry() {
    var currentHole by remember { mutableStateOf(1) }
    var strokes by remember { mutableStateOf(0) }
    var isConfirmed by remember { mutableStateOf(false) }

    Column {
        Text("Hole $currentHole")
        Row {
            Button(onClick = { if (strokes > 0) strokes-- }) { Text("-") }
            Text("$strokes", fontSize = 24.sp)
            Button(onClick = { strokes++ }) { Text("+") }
        }
        Row {
            Checkbox(
                checked = isConfirmed,
                onCheckedChange = { isConfirmed = it }
            )
            Text("Confirm score")
        }
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State with Data Classes

data class HoleState(
    val holeNumber: Int = 1,
    val par: Int = 4,
    val strokes: Int = 0,
    val isConfirmed: Boolean = false
)

@Composable
fun HoleScoreCard() {
    var state by remember { mutableStateOf(HoleState()) }

    Column {
        Text("Hole ${state.holeNumber} (Par ${state.par})")
        TextField(
            value = state.strokes.toString(),
            onValueChange = {
                state = state.copy(strokes = it.toIntOrNull() ?: 0)
            }
        )
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Derived State

@Composable
fun HoleResult() {
    var strokes by remember { mutableStateOf(4) }
    val par = 4

    val result = when {
        strokes < par -> "Under Par! 🎉"
        strokes == par -> "Par"
        strokes == par + 1 -> "Bogey"
        strokes == par + 2 -> "Double Bogey"
        else -> "Keep practicing!"
    }

    Column {
        Text("Strokes: $strokes")
        Text("Result: $result")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Lists as State

@Composable
fun ScorecardList() {
    var scores by remember {
        mutableStateOf(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
    }
    var currentHole by remember { mutableStateOf(0) }

    Column {
        scores.forEachIndexed { index, score ->
            Row {
                Text("Hole ${index + 1}:")
                Text("$score strokes")
            }
        }

        Button(onClick = {
            scores = scores.toMutableList().apply {
                this[currentHole] = this[currentHole] + 1
            }
        }) {
            Text("Add Stroke")
        }

        Text("Total: ${scores.sum()}")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting: The Problem

@Composable
fun HoleScoreInput() {
    var strokes by remember { mutableStateOf(0) }

    Row {
        IconButton(onClick = { if (strokes > 0) strokes-- }) {
            Icon(Icons.Default.Remove, "Decrease")
        }
        Text("$strokes")
        IconButton(onClick = { strokes++ }) {
            Icon(Icons.Default.Add, "Increase")
        }
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting: The Solution

@Composable
fun HoleScoreInput(
    strokes: Int,
    onStrokesChange: (Int) -> Unit
) {
    Row {
        IconButton(onClick = { if (strokes > 0) onStrokesChange(strokes - 1) }) {
            Icon(Icons.Default.Remove, "Decrease")
        }
        Text("$strokes")
        IconButton(onClick = { onStrokesChange(strokes + 1) }) {
            Icon(Icons.Default.Add, "Increase")
        }
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting: Parent Usage

@Composable
fun ScorecardScreen() {
    var scores by remember { mutableStateOf(List(18) { 0 }) }

    Column {
        scores.forEachIndexed { index, score ->
            Text("Hole ${index + 1}")
            HoleScoreInput(
                strokes = score,
                onStrokesChange = { newScore ->
                    scores = scores.toMutableList().apply {
                        this[index] = newScore
                    }
                }
            )
        }

        Text("Total: ${scores.sum()}")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting Pattern

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting Pattern (continued)

  • State flows down (parameters)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting Pattern (continued)

  • State flows down (parameters)
  • Events flow up (lambdas)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

State Hoisting Pattern (continued)

  • State flows down (parameters)
  • Events flow up (lambdas)
  • Composable becomes reusable
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Benefits of Hoisting

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Benefits of Hoisting (continued)

  • Reusable across 18 holes
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Benefits of Hoisting (continued)

  • Reusable across 18 holes
  • Testable with mock values
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Benefits of Hoisting (continued)

  • Reusable across 18 holes
  • Testable with mock values
  • Controllable from parent
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

When to Hoist State

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

When to Hoist State (continued)

  • Shared state across multiple composables
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

When to Hoist State (continued)

  • Shared state across multiple composables
  • Parent needs control (reset, submit)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

When NOT to Hoist State

  • Internal UI-only state (animation, transient focus)
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Activity: Team Selector Refactor

Refactor a stateful selector into a stateless component

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Team Selector (Before)

@Composable
fun TeamSelector(teams: List<String>) {
    var selectedTeam by remember { mutableStateOf(teams.first()) }

    Column {
        Text("Select Team", style = MaterialTheme.typography.titleMedium)
        teams.forEach { team ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { selectedTeam = team }
                    .background(
                        if (team == selectedTeam) Color(0xFFE8F5E9) else Color.Transparent
                    )
                    .padding(16.dp)
            ) {
                Text(team)
                if (team == selectedTeam) {
                    Icon(Icons.Default.Check, "Selected", tint = Color(0xFF2E7D32))
                }
            }
        }
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Team Selector (After)

@Composable
fun TeamSelector(
    teams: List<String>,
    selectedTeam: String,
    onTeamSelected: (String) -> Unit
) {
    Column {
        Text("Select Team", style = MaterialTheme.typography.titleMedium)
        teams.forEach { team ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onTeamSelected(team) }
                    .background(
                        if (team == selectedTeam) Color(0xFFE8F5E9) else Color.Transparent
                    )
                    .padding(16.dp)
            ) {
                Text(team)
                if (team == selectedTeam) {
                    Icon(Icons.Default.Check, "Selected", tint = Color(0xFF2E7D32))
                }
            }
        }
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Team Selector: Parent Usage

@Composable
fun ScoreEntryScreen() {
    val teams = listOf("The Birdies", "Par-Tee Animals", "Fore Amigos", "The Sandbaggers")
    var selectedTeam by remember { mutableStateOf(teams.first()) }

    Column {
        TeamSelector(
            teams = teams,
            selectedTeam = selectedTeam,
            onTeamSelected = { selectedTeam = it }
        )

        Text("Entering scores for: $selectedTeam")
    }
}
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Wrap-Up

Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Wrap-Up (continued)

  • State makes Compose interactive
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Wrap-Up (continued)

  • State makes Compose interactive
  • Derived state keeps logic clean
Week 4 Wednesday: State in Jetpack Compose
CS 3180 Mobile Application Development

Wrap-Up (continued)

  • State makes Compose interactive
  • Derived state keeps logic clean
  • Hoisting enables reuse and control
Week 4 Wednesday: State in Jetpack Compose

SPEAKER NOTES: Today is demo-heavy. We’ll make the scorecard interactive using state. Connect to Hope Foundation: a scorecard is useless if the strokes reset. State fixes that.

SPEAKER NOTES: We’ll define state and show what goes wrong without `remember`.

SPEAKER NOTES: We’ll explore multiple state variables, data class state, and lists.

SPEAKER NOTES: We’ll refactor a component to be stateless and reusable.

SPEAKER NOTES: State is one of the most important concepts in Compose. Let's break it down.

SPEAKER NOTES: Examples: text input, checkbox, counter, selected item. Relate to golf: current hole, strokes, selected team.

SPEAKER NOTES: Compose automatically updates UI when the state changes.

SPEAKER NOTES: Explain recomposition: every time Compose redraws, `strokes` resets to 0.

SPEAKER NOTES: `remember` keeps state across recompositions. `mutableStateOf` makes it observable. Explain Kotlin `by` delegation briefly.

SPEAKER NOTES: Understanding recomposition is key to mastering Compose. Let's explore it.

SPEAKER NOTES: Only affected composables re-run, not the entire screen.

SPEAKER NOTES: This is normal. Don’t do side effects inside composables.

SPEAKER NOTES: Show the syntactic convenience of `by` and `getValue`/`setValue`.

SPEAKER NOTES: This is a realistic score-entry component: hole, strokes, confirmation.

SPEAKER NOTES: Data classes keep related state together and make updates explicit via `copy`.

SPEAKER NOTES: Derived state is computed from other state. No need to store it separately.

SPEAKER NOTES: Key idea: create a new list instance so Compose detects the change.

SPEAKER NOTES: State is trapped inside the composable. Parent can’t reset or coordinate totals.

SPEAKER NOTES: Now the composable is stateless and reusable. State lives in the parent.

SPEAKER NOTES: The parent owns the single source of truth and can calculate totals easily.

SPEAKER NOTES: State hoisting is the key pattern for building reusable Compose components.

SPEAKER NOTES: Data goes from parent to child. This is the Compose pattern.

SPEAKER NOTES: Child emits events through callbacks. Parent updates state.

SPEAKER NOTES: Stateless composables are easier to test and reuse.

SPEAKER NOTES: Let's look at the concrete benefits of hoisting state to the parent.

SPEAKER NOTES: Same component works for each hole.

SPEAKER NOTES: We can pass in sample values without creating state.

SPEAKER NOTES: Now you can add “Clear All Scores” or submit logic from the parent.

SPEAKER NOTES: Not all state needs to be hoisted. Let's learn when and when not to.

SPEAKER NOTES: If multiple components rely on the same data, hoist it.

SPEAKER NOTES: If the parent should coordinate behavior, hoist the state.

SPEAKER NOTES: Keep small internal state local when it doesn’t affect other components.

SPEAKER NOTES: We’ll see why hoisting makes the team selector reusable across screens.

SPEAKER NOTES: State is inside the component—hard to coordinate with the rest of the app.

SPEAKER NOTES: Now it’s stateless, reusable, and testable.

SPEAKER NOTES: The parent can now coordinate the score entry screen and selected team.

SPEAKER NOTES: Let's summarize the key takeaways from state management in Compose.

SPEAKER NOTES: Without state, the UI resets. With state, it feels like a real app.

SPEAKER NOTES: Compute totals from existing state rather than storing duplicates.

SPEAKER NOTES: This prepares us for larger screens where many components share data.