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).
@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)withLaunchedEffect(id): which one refreshes when the id changes? - Collect ViewModel state with
collectAsStateWithLifecycleand show a snackbar from aSharedFlow.
Done when: you can answer "why pass a key" using the numbers from your own counter.
📚 Sources and documentation
- Side effects in Composeofficialdeveloper.android.com
- Architecting your Compose UIofficialdeveloper.android.com