State: remember & mutableStateOf

Every composable up to this point has been static — it renders once from the arguments it's given and never changes on its own. A real screen needs to react to taps, typing, and toggles. RecipeBox's Recipe List needs a favorite button that flips from outline to filled when tapped, entirely on its own, without any parent composable telling it what to do. That requires state.

Why a Plain var Doesn't Work

It's tempting to reach for an ordinary local variable to track whether a recipe is favorited:

@Composable
fun FavoriteButton() {
    // This does NOT work as you'd expect
    var isFavorite = false

    IconButton(onClick = { isFavorite = !isFavorite }) {
        Icon(
            imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = "Toggle favorite"
        )
    }
}

Tapping this button visibly does nothing. The reason is scope: isFavorite is a plain local variable inside the FavoriteButton function body. Every time Compose re-invokes FavoriteButton (recomposition, explained fully next lesson), the function starts fresh from the top — isFavorite is declared again and reset to false every single time. The click handler does flip it briefly, but by the time Compose redraws the screen, the function has already re-run and reinitialized the variable back to its starting value. A composable function has no memory of its own between invocations — it's just a function, and function-local variables don't persist across separate calls.

remember { mutableStateOf(...) }: Giving a Composable Memory

Compose solves this with two cooperating tools. mutableStateOf(initialValue) creates a special observable holder — a MutableState<T> — that Compose can watch: reading its value inside a composable registers that composable as interested in it, and writing a new value automatically schedules recomposition for every composable that read it. remember { } solves the other half of the problem — it tells Compose to hold on to whatever the lambda produces across recompositions, so the same MutableState object survives instead of being recreated from scratch every time the function re-runs.

@Composable
fun FavoriteButton() {
    var isFavorite = remember { mutableStateOf(false) }

    IconButton(onClick = { isFavorite.value = !isFavorite.value }) {
        Icon(
            imageVector = if (isFavorite.value) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = "Toggle favorite",
            tint = if (isFavorite.value) Color.Red else Color.Gray
        )
    }
}

Now tapping the button works: isFavorite.value = !isFavorite.value mutates the remembered MutableState, Compose notices this composable read .value during its last composition, and schedules FavoriteButton to recompose. This time, remember { } hands back the same state object instead of creating a new one, so the new value (true) is what gets read — the icon and its tint flip correctly, and stay flipped on every subsequent recomposition until toggled again.

The by Delegate: Reading State Like a Plain Property

Writing .value everywhere works, but Kotlin's by property delegate syntax makes state read and write like an ordinary variable, which is both more readable and the far more common style in real Compose code:

@Composable
fun FavoriteButton() {
    var isFavorite by remember { mutableStateOf(false) }

    IconButton(onClick = { isFavorite = !isFavorite }) {
        Icon(
            imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = "Toggle favorite",
            tint = if (isFavorite) Color.Red else Color.Gray
        )
    }
}

With by, isFavorite reads and writes as if it were a plain Boolean — isFavorite = !isFavorite instead of isFavorite.value = !isFavorite.value — but under the hood it's exactly the same remember { mutableStateOf(false) } mechanism. This requires importing androidx.compose.runtime.getValue and androidx.compose.runtime.setValue, which supply the delegate operators Kotlin needs; Android Studio's auto-import will add these the first time you type by remember.

Note: remember without a key survives recomposition, but NOT a configuration change like a screen rotation — the composable's whole state is torn down and rebuilt then. Surviving rotation needs rememberSaveable instead, covered later in this track alongside derivedStateOf.
Next up: You now have working local state, but what exactly makes Compose decide to re-run FavoriteButton after that click? Understanding Recomposition covers the mechanism in detail.