Compose State
Irbisa · cheatsheetSeptember 13, 2026

Compose State

remember, mutableStateOf, derivedStateOf, state hoisting, ViewModel + StateFlow

Middle Developer20 itemscompressed for a skim
  1. 01

    What is remember and rememberSaveable in Compose?

    Medium

    remember keeps a value across recompositions, and rememberSaveable additionally survives configuration changes and process death.

    @Composable
    fun Counter() {
      // remember — survives recomposition, lost on rotation if Activity recreates
      var count by remember { mutableStateOf(0) }
    
      // rememberSaveable — survives rotation + process death (for Parcelable / primitives)
    …
  2. 02

    What is mutableStateOf, mutableStateListOf, derivedStateOf?

    Medium

    mutableStateOf makes a value observable, mutableStateListOf does the same for a collection, and derivedStateOf caches a computation over other state.

    @Composable
    fun TodoList() {
      val items = remember { mutableStateListOf("buy milk", "write tests") }
      var draft by remember { mutableStateOf("") }
    
      Column {
    …
  3. 03

    What is state hoisting? Why is it the recommended pattern?

    Medium

    State hoisting moves state OUT of a composable into its caller.

    // Before — stateful, hard to reuse
    @Composable
    fun StatefulSearchBar() {
      var query by remember { mutableStateOf("") }
      TextField(value = query, onValueChange = { query = it })
    }
    …
  4. 04

    How do you connect a ViewModel + StateFlow to Compose UI?

    Medium

    The ViewModel owns the screen state as a StateFlow<UiState> and the composable reads it with collectAsStateWithLifecycle().

    // State + ViewModel
    data class HomeUiState(val isLoading: Boolean = false, val items: List<Post> = emptyList(), val error: String? = null)
    
    class HomeViewModel(private val repo: PostsRepo) : ViewModel() {
      private val _state = MutableStateFlow(HomeUiState(isLoading = true))
      val state: StateFlow<HomeUiState> = _state.asStateFlow()
    …
  5. 05

    What are LaunchedEffect, DisposableEffect, SideEffect, and produceState?

    Hard

    Compose effect handlers exist because a composable body may re-run at any moment, so a side effect needs to start, restart and clean up in step with composition.

    // Network call when key changes
    @Composable
    fun User(id: String) {
      var user by remember { mutableStateOf<User?>(null) }
      LaunchedEffect(id) {                           // restarts when id changes
        user = api.fetchUser(id)
    …
  6. 06

    What causes excessive recomposition and how do you minimize it?

    Hard

    Excessive recomposition comes from composables Compose cannot skip, and the fix is nearly always to make parameters comparable or to move the state read further down the tree.

    // ❌ Read state high in the tree — Header and Body re-run on every keystroke
    @Composable
    fun ParentRead(text: String) { Header(); Body(); Text(text) }
    
    // ✅ Push the read down to the leaf that needs it
    @Composable
    …
  7. 07

    A counter composable keeps var count = 0 in its body and does count++ on click, but the number on screen never moves. What is wrong?

    Easy

    A plain local variable is re-initialised on every recomposition and, worse, writing to it tells Compose nothing.

    @Composable
    fun BrokenCounter() {
      var count = 0                                  // re-initialised on every recomposition
      Button(onClick = { count++ }) {                // and nothing is observing it
        Text("Clicked " + count)
      }
    …
  8. 08

    When is a CompositionLocal the right way to get a value down the tree, and when is it an abuse of one?

    Medium

    A CompositionLocal hands a value implicitly to everything below the provider, which is right for ambient context and wrong for the data a screen is about.

    // Changes during the session -> trackable local
    val LocalDateFormatter = compositionLocalOf { DateFormatter(Locale.getDefault()) }
    
    // Never changes, and there is no sensible default -> static, and fail loudly
    val LocalAnalytics = staticCompositionLocalOf<Analytics> { error("LocalAnalytics not provided") }
    …
  9. 09

    Your screen composable has accumulated eight remember values plus the logic that coordinates them. What do you extract, and does it belong in a ViewModel?

    Medium

    Split the state by who cares about it: UI-only state moves into a plain state holder class remembered in the composition, and anything the rest of the app cares about moves into a ViewModel.

    @Stable
    class CheckoutFormState(
      initialEmail: String,
      private val scope: CoroutineScope,
      val listState: LazyListState
    ) {
    …
  10. 10

    Typing fast into a TextField whose value comes back from a ViewModel drops or reorders characters. Why does that happen, and how does the state-based text field API avoid it?

    Medium

    The value plus onValueChange text field is a round trip through your state holder, so if any step of that loop is asynchronous the field can render text older than what the user already typed.

    // ❌ round trip through an async pipeline — fast typing drops characters
    @Composable
    fun BrokenSearch(vm: SearchViewModel) {
      val state by vm.state.collectAsStateWithLifecycle()  // debounced upstream
      TextField(value = state.query, onValueChange = vm::onQueryChange)
    }
    …
  11. 11

    What is the snapshot system underneath Compose state, and what does it actually guarantee?

    Hard

    Compose state lives in snapshots — versioned, isolated views of memory that give every reader a consistent picture and make a group of writes behave like a transaction.

    val name = mutableStateOf("ada")
    val age = mutableIntStateOf(36)
    
    // Batch writes: no reader can observe a half-updated pair
    Snapshot.withMutableSnapshot {
      name.value = "grace"
    …
  12. 12

    After inserting a row at the top of a LazyColumn, the expanded row is suddenly the wrong one. What is going on, and how do you fix it?

    Hard

    Remembered state is identified by position in the composition, so when items shift the state stays with the slot instead of following the item.

    // ❌ no key — inserting at the top shifts every row into its neighbour's slot
    LazyColumn {
      items(posts) { post -> PostRow(post) }
    }
    
    // ✅ stable identity: state follows the item, and the move can be animated
    …
  13. 13

    A dialog holds remember { mutableStateOf(initialName) }, the parent passes a new name, and the field still shows the old one — why?

    Easy

    remember runs its calculation the first time that call site is reached and never again, so a parameter that changes later never gets back into the state it seeded.

    // ❌ initialName is read once; the parent can never change it afterwards
    @Composable
    fun RenameDialog(initialName: String, onSave: (String) -> Unit) {
      var name by remember { mutableStateOf(initialName) }
      TextField(value = name, onValueChange = { name = it })
      Button(onClick = { onSave(name) }) { Text("Save") }
    …
  14. 14

    A reviewer asks you to rewrite val scroll = remember { mutableStateOf(0) } with by and mutableIntStateOf — what do those two edits actually change?

    Easy

    by is a property delegate over the very same MutableState object — pure syntax — while mutableIntStateOf swaps the boxed backing field for a primitive one, so a value written every frame stops allocating.

    @Composable
    fun Demo(listState: LazyListState) {
      // 1. plain — you hold the MutableState and can pass it on
      val tab = remember { mutableStateOf(0) }
      Text("tab ${tab.value}")
    …
  15. 15

    Your ViewModel exposes StateFlow<UiState> and a teammate wants var uiState by mutableStateOf(...) instead — what actually differs between them?

    Medium

    Both are legitimate; what really differs is the dependency the ViewModel takes on, whether you get Flow operators, and how fine-grained the resulting recomposition is.

    // StateFlow — the common default: operators, no Compose types in the ViewModel
    class SearchViewModel(repo: Repo) : ViewModel() {
      private val _query = MutableStateFlow("")
      val query: StateFlow<String> = _query.asStateFlow()
    
      val results: StateFlow<List<Hit>> = _query
    …
  16. 16

    The log shows the new item inside the state, but the list on screen never redraws it — the ViewModel added it with _state.value.items.add(item). What happened?

    Medium

    The list was mutated in place, so the state value is still the same instance and still equal to itself, and both StateFlow and Compose state drop an update that compares equal.

    data class CartUiState(val items: List<Item> = emptyList(), val total: Int = 0)
    
    class CartViewModel : ViewModel() {
      private val _state = MutableStateFlow(CartUiState())
      val state: StateFlow<CartUiState> = _state.asStateFlow()
    …
  17. 17

    Each step of a three-screen wizard calls viewModel(), and step three sees nothing the user entered in step one — why, and how do you share one owner?

    Medium

    Inside a NavHost, viewModel() resolves against that destination's own NavBackStackEntry, so every step gets a separate ViewModel, cleared the moment its entry pops.

    @Serializable object Wizard
    @Serializable object Step1
    @Serializable object Step2
    @Serializable data class Confirm(val orderId: String)
    
    NavHost(navController, startDestination = Wizard) {
    …
  18. 18

    Tapping the favourite star fills it, then it empties for a beat before filling again — what is wrong with how that state is owned?

    Medium

    Two owners are writing the same value: the row keeps a local remember copy that flips instantly, and the ViewModel's state arrives a frame later still holding the old value and overwrites it.

    // ❌ two owners: the local mirror wins for one frame, then the ViewModel overwrites it
    @Composable
    fun PostRow(post: Post, onFavourite: (Long) -> Unit) {
      var favourite by remember { mutableStateOf(post.isFavourite) }
      IconToggleButton(checked = favourite, onCheckedChange = {
        favourite = it            // instant
    …
  19. 19

    A header that slides as the user scrolls recomposes on every frame — how do you make the scroll offset move it without recomposing anything?

    Hard

    Read the scroll value inside a lambda that Compose invokes during layout or draw instead of in the composable body — the phase in which a state is read decides how much work its change costs.

    // ❌ read in the composable body: every scrolled pixel recomposes the header
    @Composable
    fun CollapsingHeader(listState: LazyListState, title: String) {
      val offsetPx = listState.firstVisibleItemScrollOffset
      val offsetDp = with(LocalDensity.current) { -offsetPx.toDp() }
      Box(Modifier.offset(y = offsetDp)) { Text(title) }
    …
  20. 20

    A user fills your form, takes a twenty-minute call, comes back and the screen is blank — which of your state should have survived that, and how do you test it?

    Hard

    The process was killed in the background and the Activity was rebuilt from its saved instance state, which the ViewModel is not part of — only rememberSaveable and SavedStateHandle cross that line.

    class EditorViewModel @Inject constructor(
      private val handle: SavedStateHandle,
      repo: NoteRepo
    ) : ViewModel() {
    
      private val args = handle.toRoute<Editor>()          // nav argument, restored for free
    …