Irbisa · Cheatsheet

Compose State

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

Middle Developer · 20 questions

  1. 01

    This counter's click handler runs, but the number on screen never changes. What is wrong?

    @Composablefun Counter() {    var count = 0    Button(onClick = { count++ }) {        Text("Clicked $count times")    }}

    count is a plain local variable: it is reset to 0 on every recomposition, and writing it tells Compose nothing.

  2. 02

    What is mutableStateOf, and how does changing it trigger recomposition?

    mutableStateOf(value) creates an observable MutableState<T>: when a composable reads .value, Compose records the read, and when someone writes a new value, every composable that read it is scheduled to recompose.

    @Composablefun LikeButton() {    var liked by remember { mutableStateOf(false) }    var taps by remember { mutableIntStateOf(0) } // no boxing on writes     Row {…
  3. 03

    What is the difference between remember and rememberSaveable: what survives recomposition, rotation and process death?

    remember keeps a value across recompositions only; rememberSaveable also writes it into saved instance state, so it survives a configuration change and process death.

    @Composablefun SearchBar() {    var expanded by remember { mutableStateOf(false) }     // lost on rotation    var query by rememberSaveable { mutableStateOf("") }   // restored after rotation and process death    val normalized = remember(query) { query.trim().lowercase() } // recomputed when query changes…
  4. 04

    What is state hoisting, what is the difference between stateful and stateless composables, and where should state live?

    State hoisting moves state out of a composable into its caller: the composable becomes stateless, receiving the current value and an onValueChange callback, while a stateful one owns its state with remember inside.

    // stateful: owns its state, easy to drop in, impossible to control from outside@Composablefun NameFieldStateful() {    var name by rememberSaveable { mutableStateOf("") }    NameField(name = name, onNameChange = { name = it })}…
  5. 05

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

    @Composablefun NameDialog(initialName: String, onSave: (String) -> Unit) {    var name by remember { mutableStateOf(initialName) }     AlertDialog(        onDismissRequest = {},        confirmButton = { TextButton(onClick = { onSave(name) }) { Text("Save") } },        text = { TextField(value = name, onValueChange = { name = it }) },    )}

    remember runs its lambda only the first time that call site is composed and returns the stored value afterwards, so a parameter that changes later never reaches the state it once seeded.

  6. 06

    How do you connect a ViewModel's StateFlow to Compose: collectAsStateWithLifecycle vs collectAsState vs produceState?

    The ViewModel exposes a StateFlow<UiState>, and the screen reads it with collectAsStateWithLifecycle(), which collects only while the lifecycle is at least STARTED.

    data class ProfileUiState(val name: String = "", val isLoading: Boolean = true) class ProfileViewModel(repo: ProfileRepository) : ViewModel() {    val uiState: StateFlow<ProfileUiState> = repo.observeProfile()        .map { ProfileUiState(name = it.name, isLoading = false) }        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ProfileUiState())…
  7. 07

    Why does adding an item to a MutableList held in mutableStateOf not update the screen, and what do mutableStateListOf or a new list change?

    Snapshot state tracks the reference it holds, not what happens inside that object: list.add(x) keeps the same instance, so no write to the state ever happens and no reader is invalidated.

    class TodoHolder {    // wrong: the reference never changes, so readers are never invalidated    val broken = mutableStateOf(mutableListOf<String>())    fun addBroken(item: String) {        broken.value.add(item)    }…
  8. 08

    What is derivedStateOf and what problem does it solve?

    `derivedStateOf { ...

    @Composablefun ArticleList(articles: List<Article>) {    val listState = rememberLazyListState()    val scope = rememberCoroutineScope()     // the index changes on every scroll frame; the Boolean flips rarely…
  9. 09

    Find the bug: the search box filters the list correctly, but after the parent updates its data the list stays the same.

    @Composablefun ProductList(products: List<Product>, maxPrice: Int) {    var query by rememberSaveable { mutableStateOf("") }    val visible by remember {        derivedStateOf {            products.filter { it.price <= maxPrice && it.name.contains(query, ignoreCase = true) }        }    }     Column {        TextField(value = query, onValueChange = { query = it })        LazyColumn {            items(visible, key = { it.id }) { ProductRow(it) }        }    }}

    remember without keys builds the derivedStateOf once, and its lambda keeps the products and maxPrice of the first composition.

  10. 10

    What are side effects in Compose, and when do you use LaunchedEffect, DisposableEffect, SideEffect and produceState?

    A side effect is anything that changes the world outside the composable; because a composable body can re-run at any time, effects go through effect APIs that start, restart and clean up in step with the composition.

    @Composablefun UserScreen(userId: String, repo: UserRepository, analytics: Analytics) {    // restarts when userId changes, cancelled when the screen leaves    val user by produceState<User?>(initialValue = null, userId) {        value = repo.load(userId)    }…
  11. 11

    This button should show a snackbar when tapped, but the code does not compile. Why, and should you use LaunchedEffect or rememberCoroutineScope?

    @Composablefun SaveButton(snackbarHostState: SnackbarHostState) {    Button(onClick = {        LaunchedEffect(Unit) {            snackbarHostState.showSnackbar("Saved")        }    }) {        Text("Save")    }}

    LaunchedEffect is a composable and onClick is a plain lambda, so the compiler reports @Composable invocations can only happen from the context of a @Composable function.

  12. 12

    The parent passes a new onTimeout while this splash screen is waiting, yet the old lambda is called. Why, and what does rememberUpdatedState fix?

    @Composablefun SplashScreen(onTimeout: () -> Unit) {    LaunchedEffect(Unit) {        delay(3_000)        onTimeout()    }    Logo()}

    LaunchedEffect(Unit) starts once and its coroutine captures the onTimeout from that first composition; later recompositions pass a new lambda, but the running coroutine still holds the old one.

  13. 13

    What is snapshotFlow, and when do you convert Compose state into a Flow?

    snapshotFlow { } turns reads of Compose state into a cold Flow: it runs the block, remembers which state it read, and emits a new result whenever one of those states changes.

    @Composablefun FeedAnalytics(listState: LazyListState, analytics: Analytics) {    LaunchedEffect(listState) {        snapshotFlow { listState.firstVisibleItemIndex }            .filter { it > 0 }            .collect { index -> analytics.logScrolledTo(index) }…
  14. 14

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

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

    data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp, val large: Dp = 16.dp) val LocalSpacing = staticCompositionLocalOf { Spacing() } // no sensible default: fail loudly instead of rendering with a fakeval LocalImageLoader = staticCompositionLocalOf<ImageLoader> {…
  15. 15

    When do you write a plain state holder class with a rememberXState() factory instead of putting the state in a ViewModel?

    A plain state holder is for UI logic (state that only the UI cares about and that may die with the composable); a ViewModel is for business logic and screen data that must survive configuration changes and reach the data layer.

    class CheckoutFormState(initialEmail: String) {    var email by mutableStateOf(initialEmail)        private set    var showErrors by mutableStateOf(false)        private set…
  16. 16

    Tapping the star fills it, it empties for a moment, then fills again. What is wrong with this code?

    @Composablefun FavouriteRow(item: Item, onToggle: (Long) -> Unit) {    var fav by remember(item) { mutableStateOf(item.isFavorite) }     IconToggleButton(        checked = fav,        onCheckedChange = {            fav = it            onToggle(item.id) // ViewModel sends the request        },    ) {        Icon(if (fav) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, "Favourite")    }}

    The row has two sources of truth: a local copy that flips on tap and the ViewModel value, which is re-applied with the old false before the write lands, so the star flips back until the server answer arrives.

  17. 17

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

    StateFlow keeps the ViewModel free of Compose and gives you Flow operators; mutableStateOf is simpler to read in Compose and recomposes per state object, but ties the ViewModel to the Compose runtime.

  18. 18

    Typing fast into a TextField whose value comes back from a ViewModel drops or reorders characters. Why, and how does TextFieldState avoid it?

    The value/onValueChange loop must update synchronously; any async hop lets the field render text older than what was typed.

    val DigitsOnly = InputTransformation {    if (!asCharSequence().all { it.isDigit() }) revertAllChanges()} class PhoneViewModel : ViewModel() {    // the field edits this directly; no value/onValueChange round trip…
  19. 19

    For a checkout form, what goes into rememberSaveable, what into SavedStateHandle, and what is reloaded? And how do you test the restore?

    Save what the user typed or picked: UI-only fields in rememberSaveable, what the ViewModel needs in SavedStateHandle; everything loaded from the data layer is reloaded by id.

    class CheckoutViewModel(private val handle: SavedStateHandle) : ViewModel() {    // small user input: survives process death    val note: StateFlow<String> = handle.getStateFlow("note", "")    fun onNoteChange(value: String) {        handle["note"] = value    }…
  20. 20

    How does the snapshot system under Compose state work, and what does it guarantee?

    Compose state lives in snapshots (isolated, versioned views of all state objects), which give every reader a consistent picture and make a group of writes behave like a transaction.