Question #386HardNative Android

How do you handle side effects in Jetpack Compose?

#jetpack-compose#side-effects#coroutines

Answer

Overview

Side effects are operations that escape the scope of a composable function.


Side Effect APIs

APIUse Case
LaunchedEffectCoroutines, async operations
DisposableEffectCleanup (listeners, subscriptions)
SideEffectUpdate non-Compose state
rememberCoroutineScopeLaunch coroutines outside composables
derivedStateOfExpensive computations

1. LaunchedEffect

kotlin
@Composable
fun LoadDataScreen(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        user = repository.fetchUser(userId)
    }
    
    user?.let { Text(it.name) }
}

Cancellation

kotlin
LaunchedEffect(key1) {
    // Cancelled when key1 changes or composable leaves composition
    val data = fetchData()
    updateUI(data)
}

2. DisposableEffect

kotlin
@Composable
fun LocationUpdates() {
    DisposableEffect(Unit) {
        val listener = LocationListener { location ->
            // Handle location
        }
        locationManager.addListener(listener)
        
        onDispose {
            locationManager.removeListener(listener)
        }
    }
}

3. SideEffect

kotlin
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        analytics.logScreenView(screenName)
    }
}

4. rememberCoroutineScope

kotlin
@Composable
fun ScrollToTopButton(listState: LazyListState) {
    val scope = rememberCoroutineScope()
    
    Button(
        onClick = {
            scope.launch {
                listState.animateScrollToItem(0)
            }
        }
    ) {
        Text("Scroll to Top")
    }
}

Common Patterns

Network Call

kotlin
@Composable
fun UserProfile(userId: String, viewModel: UserViewModel) {
    val user by viewModel.user.collectAsState()
    
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }
    
    user?.let { Text(it.name) }
}

Timer

kotlin
@Composable
fun Timer() {
    var seconds by remember { mutableStateOf(0) }
    
    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            seconds++
        }
    }
    
    Text("Seconds: $seconds")
}

Listener Registration

kotlin
@Composable
fun ConnectivityStatus() {
    var isOnline by remember { mutableStateOf(true) }
    
    DisposableEffect(Unit) {
        val callback = ConnectivityCallback { online ->
            isOnline = online
        }
        connectivityManager.registerCallback(callback)
        
        onDispose {
            connectivityManager.unregisterCallback(callback)
        }
    }
    
    Text(if (isOnline) "Online" else "Offline")
}

derivedStateOf

kotlin
@Composable
fun TodoList(todos: List<Todo>) {
    val completedCount by remember {
        derivedStateOf {
            todos.count { it.isCompleted }
        }
    }
    
    Text("Completed: $completedCount / ${todos.size}")
}

Rule: Use side effects APIs for operations outside Compose. Never perform side effects directly in composable bodies.