Compose State
Jetpack Compose state primitives: remember, mutableStateOf, derivedStateOf, snapshotFlow, and the rememberSaveable variant that survives configuration change. Lift state to a ViewModel for non-trivial cases. The compose runtime tracks reads at composition time, then re-runs only the composables that read a changed value.
remember, derivedStateOf, snapshotFlow, rememberSaveable
EXAMPLE
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.debounce
// 1) Plain remember + mutableStateOf — local state for a single composable
@Composable
fun Counter() {
var n by remember { mutableIntStateOf(0) }
Column(Modifier.padding(16.dp)) {
Text("Count: $n")
Button(onClick = { n++ }) { Text("+") }
}
}
// 2) rememberSaveable — survives configuration changes (rotation, theme switch)
@Composable
fun RememberMyName() {
var name by rememberSaveable { mutableStateOf("") }
TextField(value = name, onValueChange = { name = it }, label = { Text("Your name") })
}
// 3) derivedStateOf — compute a value from other state; only invalidates when the RESULT changes
@Composable
fun FilteredList(items: List<String>) {
var query by remember { mutableStateOf("") }
val filtered by remember(items) {
derivedStateOf {
if (query.isBlank()) items
else items.filter { it.contains(query, ignoreCase = true) }
}
}
Column {
TextField(value = query, onValueChange = { query = it }, label = { Text("Filter") })
LazyColumn { items(filtered.size) { Text(filtered[it]) } }
}
}
// 4) snapshotFlow — bridge Compose state to a Flow (great for debounce / network)
@Composable
fun SearchScreen(onSearch: (String) -> Unit) {
var query by remember { mutableStateOf(TextFieldValue("")) }
LaunchedEffect(Unit) {
snapshotFlow { query.text }
.distinctUntilChanged()
.debounce(300)
.collect(onSearch)
}
TextField(value = query, onValueChange = { query = it })
}
// 5) Lift state — the composable becomes 'stateless' and easier to test
@Composable
fun OutlinedCounter(count: Int, onIncrement: () -> Unit) {
OutlinedButton(onClick = onIncrement) { Text("Count: $count") }
}
@Composable
fun ParentScreen() {
var c by remember { mutableIntStateOf(0) }
OutlinedCounter(count = c, onIncrement = { c++ })
}
// 6) When local state is not enough — ViewModel + StateFlow
// See the 'kotlin/android' lesson; this pattern keeps the UI declarative
// while the data layer is testable and survives configuration changes.
// 7) Side effects
// LaunchedEffect(key) { suspend body — runs when key changes }
// DisposableEffect(key) { onDispose { /* cleanup */ } }
// SideEffect { /* runs after every successful recomposition */ }
// Common bug: using snapshotFlow inside a LaunchedEffect with key=Unit
// vs key=someStateValue. Choose the key carefully — wrong key = stale closure.
Why it matters
Reach for derivedStateOf whenever you compute a value from other state inside a composable. Without it, the composable recomputes whenever any input changes, which often causes downstream recompositions that the derived value did not actually affect — a subtle source of "why is this still re-rendering?" performance papercuts.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
var name by remember { mutableStateOf("") }
TextField(value = name, onValueChange = { name = it })
// Hoist state up if more than one screen needs it.
Try it Yourself »
Discussion
Loading…