Sparround

Compose state management & side effects

A side effect is anything beyond a composable's job of emitting UI: API calls, snackbars, navigation, analytics. Since recomposition can repeat arbitrarily, side effects never go directly in the composable body — there are managed effect APIs:

  • LaunchedEffect(key) — starts a coroutine on entering composition; restarts when the key changes, cancels on exit. "Load when the screen opens", "refetch when the id changes".
  • DisposableEffect(key) — a register/cleanup pair: add a listener, remove it in onDispose.
  • rememberCoroutineScope — a scope for launching coroutines from callbacks (onClick).
  • snapshotFlow — turns Compose state into a Flow (e.g. observing scroll position).
  • derivedStateOf — a value derived from other state; recomposition only when the result changes.

The standard for collecting ViewModel state: collectAsStateWithLifecycle() — collection stops when the lifecycle leaves STARTED (saving resources).

kotlin
@Composable
fun TransactionDetailScreen(txId: String, viewModel: DetailViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    // txId dəyişəndə yenidən yüklə; ekrandan çıxanda cancel
    LaunchedEffect(txId) {
        viewModel.load(txId)
    }

    // Birdəfəlik event-lərin toplanması
    LaunchedEffect(Unit) {
        viewModel.messages.collect { msg ->
            snackbarHostState.showSnackbar(msg)
        }
    }

    // Callback-dən coroutine: suspend olan snackbar çağırışı
    Button(onClick = {
        scope.launch { snackbarHostState.showSnackbar("Kopyalandı") }
    }) { Text("Qəbzi kopyala") }
}

Each effect API in its place

Trap question: "why not make the API call directly in the composable body?" — the body runs on every recomposition: one click causing 10 recompositions = 10 requests. The right home: the ViewModel (init/methods) or a LaunchedEffect. Second trap: LaunchedEffect(Unit) vs LaunchedEffect(id) — Unit means once on entry, id means restart on every change; the wrong key means stale data or duplicate requests.

🛠 Practice task

Misuse the effect APIs on purpose and watch what happens:

  • Make an API call (simulated with a log) directly in the composable body and count the invocations; then move it into a LaunchedEffect.
  • Compare LaunchedEffect(Unit) with LaunchedEffect(id): which one refreshes when the id changes?
  • Collect ViewModel state with collectAsStateWithLifecycle and show a snackbar from a SharedFlow.

Done when: you can answer "why pass a key" using the numbers from your own counter.

📚 Sources and documentation