derivedStateOf & rememberSaveable

This lesson closes out the State & Architecture section with two narrower, more surgical tools. derivedStateOf stops Compose from doing more recomposition work than it needs to when you compute one value from another. rememberSaveable solves a problem plain remember and even ViewModel don't fully cover: surviving the process being killed entirely, not just a configuration change.

The Problem derivedStateOf Solves

Suppose RecipeBox's search screen tracks a search query and shows how many recipes currently match it. A naive approach computes the filtered count directly in the composable body:

@Composable
fun RecipeSearchScreen(recipes: List<Recipe>) {
    var query by remember { mutableStateOf("") }

    // Recalculated on EVERY recomposition of this composable,
    // even ones triggered by something unrelated to query or recipes
    val matchCount = recipes.count { it.title.contains(query, ignoreCase = true) }

    // ...
}

That's not wrong, exactly — but matchCount gets recalculated every single time this composable recomposes for any reason, not just when query or recipes actually changed. For a filter over a small list that's harmless, but the pattern matters once the computation is heavier, or once other unrelated state in the same composable changes frequently (an animation value, a scroll position) and would otherwise force a wasted recalculation on every frame.

derivedStateOf: Compute From State, Recompose Only When the Result Changes

derivedStateOf {} wraps a calculation and produces a Compose State object that only reports a change — and therefore only triggers recomposition of readers — when the computed result itself actually differs from its last value, regardless of how often the inputs are read or the surrounding composable recomposes:

@Composable
fun RecipeSearchScreen(recipes: List<Recipe>) {
    var query by remember { mutableStateOf("") }

    val matchCount by remember {
        derivedStateOf {
            recipes.count { it.title.contains(query, ignoreCase = true) }
        }
    }

    Column {
        TextField(value = query, onValueChange = { query = it }, label = { Text("Search recipes") })
        Text("$matchCount matching recipes")
    }
}

Note the derivedStateOf call is itself wrapped in remember — this is the standard pairing: remember keeps the same derived-state object alive across recompositions, while derivedStateOf is what decides whether reading it should actually invalidate anything downstream. If query changes from "egg" to "eggs" but the resulting matchCount happens to stay at, say, 3 both times, anything reading only matchCount — like the Text above — will not recompose, even though query itself changed and the composable containing it did recompose.

When to reach for it: derivedStateOf earns its keep when the computation is non-trivial, or when it reads state that changes far more often than its own result does (as above — every keystroke, but the count only changes sometimes). For a cheap one-line computation over infrequently changing state, plain reads are simpler and the overhead of derivedStateOf isn't worth it.

rememberSaveable: Surviving Process Death, Not Just Recomposition

remember survives recomposition. A ViewModel survives configuration changes like rotation. Neither one survives the Android system killing the app's process entirely to reclaim memory while it's in the background — something that happens routinely on real devices, not just in theory. When the user returns to the app afterward, Android recreates the Activity from scratch, and both plain remember state and a fresh ViewModel instance start over from nothing, unless something saved the value into the instance-state Bundle first.

rememberSaveable is a drop-in replacement for remember that automatically saves its value into that Bundle and restores it — surviving both configuration changes and process death — for any type that's already Parcelable, or any of the simple types (String, Int, Boolean, and similar) that Android's saved-state mechanism understands natively:

@Composable
fun RecipeSearchScreen(recipes: List<Recipe>) {
    var query by rememberSaveable { mutableStateOf("") }

    val matchCount by remember {
        derivedStateOf {
            recipes.count { it.title.contains(query, ignoreCase = true) }
        }
    }

    Column {
        TextField(value = query, onValueChange = { query = it }, label = { Text("Search recipes") })
        Text("$matchCount matching recipes")
    }
}

The only change from the earlier example is remember becoming rememberSaveable on the query line — everything else, including the derivedStateOf block, stays exactly the same. Now if the user types "choc" into the search box, rotates the phone, or the OS kills the app in the background while they switch to another app for a while, query comes back as "choc" instead of resetting to an empty string, without RecipeBox writing a single line of manual save/restore code.

rememberrememberSaveableViewModel
Survives recompositionYesYesYes
Survives rotationNoYesYes
Survives process deathNoYesNo, by itself
Good forPurely transient UI stateSmall bits of UI state worth restoring, like a search boxScreen-level state and business logic
Note: ViewModel state can also survive process death, but only by explicitly using the separate SavedStateHandle API — a ViewModel on its own does not survive process death for free. For a single simple value like a search query, rememberSaveable is usually the more direct tool.
Next up: LaunchedEffect starts the Side Effects section — how to safely run suspend functions and one-off work, like triggering that initial data load, from inside a composable.