iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…