Jetpack Compose - State & Side-Effect Essentials (2025)


Jetpack Compose - State & Side-Effect Essentials (2025)

Fifteen must-know patterns for reliable UI state and effects in Compose. Short explanations, accurate “when to use”, and tiny code ideas you can paste and adapt.

remember

Store objects across recompositions within the current composition.

When to use: Component-local state that should reset when the composable leaves the tree.

// Hold simple UI state across recompositions
val counter = remember { mutableStateOf(0) }

Button(onClick = { counter.value++ }) {      // recompose-safe
  Text("Count: ${counter.value}")
}

rememberSaveable

Like remember, but persists through config changes & process death (via Saver/Bundle).

When to use: Small, serializable UI state (text, tabs, ids) that must survive rotation or kill/restore.

// Persists across rotation/process death
var text by rememberSaveable { mutableStateOf("") }

TextField(
  value = text,
  onValueChange = { text = it },
  label = { Text("Name") }
)

State Hoisting

Move state up; pass value + onChange down for reusability & testability.

When to use: Any reusable UI piece that shouldn’t own its business state.

// Hoisted: parent owns state, child is stateless
@Composable
fun Counter(value: Int, onChange: (Int) -> Unit) {
  Button(onClick = { onChange(value + 1) }) { Text("Count: $value") } // easy to test
}

derivedStateOf

Compute derived data efficiently; recomposes only when inputs change.

When to use: Filtering, sorting, or expensive calculations based on other state.

// Avoid recomputing on every frame
val query by remember { mutableStateOf("") }
val results by remember(query) {
  derivedStateOf { list.filter { it.contains(query, ignoreCase = true) } }
}

LaunchedEffect

Run a coroutine tied to keys; cancels/relaunches when keys change.

When to use: One-shot loads, reacting to parameter changes, timed jobs.

// Fire once on first composition
LaunchedEffect(Unit) {
  viewModel.loadUser()
}

rememberCoroutineScope

Launch coroutines from callbacks (clicks, drags) with a lifecycle-aware scope.

When to use: User-triggered work: snackbars, saves, animations.

// Launch on interaction
val scope = rememberCoroutineScope()

Button(onClick = { scope.launch { saveData() } }) {
  Text("Save")
}

DisposableEffect

Set up external resources and clean them when the effect leaves composition.

When to use: Listeners, sensors, callbacks, registering receivers.

// Register/unregister safely
DisposableEffect(Unit) {
  startListener()
  onDispose { stopListener() }    // cleanup
}

SideEffect

Non-suspending action after a successful composition pass.

When to use: Logging, analytics, imperative bridge updates.

// Guaranteed after composition
SideEffect {
  Log.d("Compose", "Frame composed")
}

rememberUpdatedState

Keep long-lived effects reading the latest lambda/value.

When to use: Timers, callbacks used inside LaunchedEffect that should use fresh params.

// Always invoke newest callback
val onDoneUpdated by rememberUpdatedState(onDone)

LaunchedEffect(Unit) {
  delay(300)
  onDoneUpdated()                 // latest version
}

snapshotFlow

Convert snapshot reads to a Flow; emits when state changes.

When to use: Bridge Compose state to Flow collectors (VM, repositories).

// Observe scroll as Flow
val scroll = rememberScrollState()

LaunchedEffect(Unit) {
  snapshotFlow { scroll.value }.collect { value ->
    println("Scrolled: $value")
  }
}

produceState

Expose suspend data as Compose state with lifecycle awareness.

When to use: Loading async data directly in UI layer (simple cases).

// Suspend-to-state helper
val user by produceState<User?>(initialValue = null) {
  value = repo.loadUser()
}

rememberInfiniteTransition

Loop animations indefinitely with a stable timeline.

When to use: Pulses, shimmer-like hints, attention nudges.

// Pulse alpha forever
val infinite = rememberInfiniteTransition(label = "pulse")
val alpha by infinite.animateFloat(
  initialValue = 0.3f,
  targetValue = 1f,
  animationSpec = infiniteRepeatable(animation = tween(800), repeatMode = RepeatMode.Reverse),
  label = "alpha"
)

key()

Control identity to reset local state when the key changes.

When to use: Recreate content for different ids, force fresh subtrees.

// Reset subtree per id
key(user.id) {
  ProfileCard(user)  // local remember()s re-init per id
}

rememberUpdatedState + LaunchedEffect

Pattern to keep long coroutines in sync with latest inputs.

When to use: Event sinks, debounced searches, repeating jobs.

// Safe long-lived job with latest handler
val onEventU by rememberUpdatedState(onEvent)

LaunchedEffect(Unit) {
  events.collect { e -> onEventU(e) }  // always fresh
}

Snapshot APIs (Advanced)

Low-level control of snapshot reads/writes and transactions.

When to use: Coordinating shared mutable state or batching updates.

// Atomic mutation of multiple states
Snapshot.withMutableSnapshot {
  stateA.value++
  stateB.value = compute(stateA.value)
}


Thank You 🙏  Follow Boltuix

Daily Compose tips and practical UI recipes.

Post a Comment

Previous Post Next Post