Side effects in Compose let you interact with the outside world — network calls, databases, subscriptions — in a safe, predictable way.
LaunchedEffect
Runs a suspend function when the key changes. Cancels and restarts if the key changes. Cancels when leaving composition.
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// Runs when userId changes; cancels previous
LaunchedEffect(userId) {
user = userRepository.getUser(userId)
}
if (user != null) {
Text(user!!.name)
} else {
CircularProgressIndicator()
}
}
// Keys — restart when any key changes
LaunchedEffect(userId, refreshTrigger) {
data = repository.loadData(userId)
}rememberCoroutineScope
Returns a CoroutineScope that’s cancelled when leaving composition. Use for launching coroutines from event handlers (not during composition).
@Composable
fun SaveButton(viewModel: SaveViewModel) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Button(onClick = {
scope.launch {
viewModel.save()
snackbarHostState.showSnackbar("Saved!")
}
}) {
Text("Save")
}
}DisposableEffect
For side effects that need cleanup. Runs when keys change and on dispose.
@Composable
fun NetworkObserver(networkCallback: NetworkCallback) {
val context = LocalContext.current
val connectivityManager = remember {
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
DisposableEffect(Unit) {
connectivityManager.registerNetworkCallback(
NetworkRequest.Builder().build(),
networkCallback
)
onDispose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
}
// With keys — re-register when key changes
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> /* handle */ }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}produceState
Converts a non-Compose state source into Compose state. Useful for converting Flow, LiveData, or streams.
@Composable
fun loadUser(userId: String): State<Result<User>> {
return produceState(initialValue = Result.Loading, key1 = userId) {
value = userRepository.getUser(userId)
}
}
// Usage
val userResult by loadUser(userId = "123")
when (userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> UserView(userResult.data)
is Result.Error -> ErrorMessage(userResult.message)
}derivedStateOf
Creates a state that’s derived from other states. Only triggers recomposition when the derived value changes.
@Composable
fun TodoList(todos: List<Todo>) {
val showClearButton by remember {
derivedStateOf {
todos.any { it.completed }
}
}
// Only recomposes when showClearButton changes, not on every todo edit
if (showClearButton) {
IconButton(onClick = { /* clear completed */ }) {
Icon(Icons.Default.Delete, "Clear completed")
}
}
}
// Scroll-based visibility
@Composable
fun CollapsibleHeader(scrollState: ScrollState) {
val isCollapsed by remember {
derivedStateOf { scrollState.value > 100 }
}
AnimatedVisibility(visible = !isCollapsed) {
Text("Expanded Header")
}
}snapshotFlow
Converts Compose state changes into a Kotlin Flow. Useful for analytics, debounced saves, etc.
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
var query by remember { mutableStateOf("") }
// Debounced search
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.distinctUntilChanged()
.collect { q ->
viewModel.search(q)
}
}
TextField(
value = query,
onValueChange = { query = it },
label = { Text("Search") }
)
}SideEffect
Runs after every successful recomposition. Use to notify non-Compose code of Compose state.
@Composable
fun AnalyticsScreen(screenName: String) {
// Publish to analytics after every recomposition
SideEffect {
analytics.trackScreenView(screenName)
}
Text(screenName)
}Lifecycle awareness
// Observe lifecycle events
@Composable
fun LifecycleObserver() {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> { /* resume logic */ }
Lifecycle.Event.ON_PAUSE -> { /* pause logic */ }
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
}
// Collect from ViewModel with lifecycle awareness
@Composable
fun MyScreen(viewModel: MyViewModel) {
// Stops collecting when app is backgrounded
val state by viewModel.state.collectAsStateWithLifecycle()
// vs. collectAsState (keeps collecting in background)
// val state by viewModel.state.collectAsState()
}Which side effect to use?
| Effect | Use when | Cleanup | Keyed |
|---|---|---|---|
LaunchedEffect |
Run suspend function on key change | Auto-cancel | Yes |
rememberCoroutineScope |
Launch from event handlers | Auto-cancel on dispose | No |
DisposableEffect |
Register/unregister callbacks | onDispose block |
Yes |
produceState |
Convert external data to state | Auto-cancel | Yes |
derivedStateOf |
Compute derived state efficiently | N/A | N/A |
snapshotFlow |
Convert state changes to Flow | Auto-cancel | N/A |
SideEffect |
Notify non-Compose after recomposition | N/A | No |