Implementing Compose Side Effects
1. Using LaunchedEffect
LaunchedEffect is a Compose side-effect that launches a coroutine and ensures it runs only once during the composition lifecycle. It's particularly useful for triggering side effects like API calls, navigation, or state updates that should happen once when a composable is composed.
| Parameter | Type | Description |
|---|---|---|
| key1 | Any? |
Used to identify the effect. If any of the keys change, the effect will be cancelled and restarted. |
| block | CoroutineScope.() -> Unit |
The coroutine block to execute. |
Example: Fetching Data with LaunchedEffect
import androidx.compose.runtime.*
import kotlinx.coroutines.*
@Composable
fun DataFetcher() {
var data by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
data = fetchDataFromAPI()
}
data?.let { Text(it) }
}
suspend fun fetchDataFromAPI(): String {
delay(1000)
return "Fetched Data"
}
LaunchedEffect is typically used with a key to ensure it only re-runs when that key changes. Using Unit as a key ensures it runs once per composition.
LaunchedEffect inside loops or conditionals that might cause recomposition. It's best used in a stable composition context.
2. Implementing DisposableEffect
The DisposableEffect composable is used to perform side effects that require cleanup when the composable is disposed. It's particularly useful for managing resources like subscriptions, timers, or external listeners.
| Parameter | Type | Description |
|---|---|---|
| effect | () -> DisposableEffectScope |
Block that defines the side effect and returns a DisposableEffectScope for cleanup. |
Example: Using DisposableEffect to manage a timer
import androidx.compose.runtime.*
import kotlinx.coroutines.*
@Composable
fun TimerScreen() {
var seconds by remember { mutableStateOf(0) }
DisposableEffect(Unit) {
val job = CoroutineScope(Dispatchers.Main).launch {
while (true) {
delay(1000)
seconds++
}
}
onDispose {
job.cancel()
}
}
Text(text = "Seconds: $seconds")
}
DisposableEffect is called every time the composable is recomposed, but cleanup only happens when the composable is disposed.3. Using SideEffect
The SideEffect composable is used to perform side effects in a Compose UI. It allows you to execute code that doesn't return a UI element, such as logging, external API calls, or updating state in a non-UI context.
| Parameter | Type | Description |
|---|---|---|
| effect | () -> Unit |
Composable function that performs the side effect. This is called during composition. |
SideEffect is called during composition, so it should not be used for expensive operations or operations that might cause recomposition.
4. Working with rememberCoroutineScope
Example: Using rememberCoroutineScope to launch coroutines
import androidx.compose.runtime.*
import kotlinx.coroutines.*
@Composable
fun CoroutineExample() {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
scope.launch {
delay(1000)
println("Coroutine completed")
}
}
Text(text = "Coroutine launched")
}
| Function | Description | Usage |
|---|---|---|
| rememberCoroutineScope() | Creates a coroutine scope tied to the composable's lifecycle | Used to launch coroutines from composables safely |
rememberCoroutineScope ensures that launched coroutines are cancelled when the composable is disposed.5. Implementing rememberUpdatedState
rememberUpdatedState is a Compose utility that allows you to safely access the most recent value of a state variable inside a LaunchedEffect, DisposableEffect, or other composables that might be called multiple times.
It is particularly useful when you need to ensure that a side effect always uses the latest value of a state variable, even if that state is updated while the effect is running.
| Parameter | Type | Description |
|---|---|---|
| value | T |
The current value to be remembered. |
Example: Using rememberUpdatedState in LaunchedEffect
import androidx.compose.runtime.*
import kotlinx.coroutines.*
@Composable
fun MyComposable() {
var count by remember { mutableStateOf(0) }
val updatedCount = rememberUpdatedState(count)
LaunchedEffect(count) {
// This will always use the latest count value
val job = launch {
delay(1000)
println("Count is now: ${updatedCount.current}")
}
}
Button(onClick = { count++ }) {
Text("Increment")
}
}
rememberUpdatedState is not needed in most cases, but it's essential when dealing with state updates that might occur during a side effect’s execution.
6. Using produceState
produceState is a Compose side-effect that allows you to manage state that is derived from external sources, such as asynchronous operations or observables. It's particularly useful for initializing state that depends on a side effect, like fetching data from a network or database.
| Parameter | Type | Description |
|---|---|---|
| initialValue | T |
The initial value of the state before the side effect is executed. |
| key1, key2, ... | Any? |
Optional keys that determine when the side effect should be re-run. If any key changes, the effect is restarted. |
Example: Fetching User Data
val userState = produceState<User?>(initialValue = null) {
val user = fetchUserFromNetwork() // Simulated async operation
value = user
}
produceState is ideal for initializing state that depends on asynchronous operations. It ensures that the state is updated once the operation completes.
7. Handling Back Press
| Method | Description | Notes |
|---|---|---|
| BackHandler | Composable that handles back press events in a composable scope. | Must be placed at the top level of the back stack to capture all back presses. |
| onBackPressed | Callback function triggered when the back button is pressed. | Can be used to perform cleanup or navigation logic before exiting. |
Example: Basic BackHandler Usage
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
@Composable
fun BackPressExample() {
var count by rememberSaveable { mutableStateOf(0) }
BackHandler(enabled = count > 0) {
count--
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Count: $count",
fontSize = 24.sp,
color = Color.Black
)
}
}
BackHandler is only active when the composable is in the current back stack.
BackHandlers are active, only the last one added will handle the back press.
8. Managing Lifecycle Effects
| Effect | Description | Usage |
|---|---|---|
| LaunchedEffect | Launches a coroutine to perform side effects that are tied to the composable's lifecycle | |
| DisposableEffect | Performs a side effect and provides a cleanup function when the composable is disposed | |
| RecompositionEffect | Triggers recomposition when a specific condition changes | |
Example: Using LaunchedEffect for Network Requests
class NetworkViewModel : ViewModel() {
private val _data = MutableStateFlow<String>("")
val data: StateFlow<String> = _data.asStateFlow()
fun fetchData() {
viewModelScope.launch {
val result = apiService.fetchData()
_data.value = result
}
}
}
@Composable
fun NetworkScreen(viewModel: NetworkViewModel) {
val data by viewModel.data.collectAsState()
LaunchedEffect(Unit) {
viewModel.fetchData()
}
Text(text = data)
}
Example: Using DisposableEffect for Resource Management
@Composable
fun MyComponent() {
DisposableEffect(Unit) {
val timer = Timer()
timer.scheduleAtFixedRate(1000) {
// Update UI
}
onDispose {
timer.cancel()
}
}
}
9. Using Effect Handlers Best Practices
Best Practices for Effect Handlers in Jetpack Compose
- Use
LaunchedEffectfor side effects that should run once or when specific keys change. - Avoid using
LaunchedEffectfor UI updates; preferStateandMutableStateinstead. - Use
DisposableEffectfor effects that require cleanup, such as registering and unregistering listeners. - Use
SideEffectfor effects that should run after composition, such as logging or updating external state. - Use
produceStatefor effects that produce a value and should be recomposed when the value changes.
| Effect Handler | Use Case | Best Practice |
|---|---|---|
| LaunchedEffect | Running coroutines or side effects that should run once or when keys change | Use key1, key2 to control when the effect should re-run |
| DisposableEffect | Registering and unregistering listeners or resources that need cleanup | Use onDispose to clean up resources |
| SideEffect | Effects that should run after composition, such as logging or external state updates | Use for effects that don't affect the composition but should run after it |
| produceState | Producing a value that should trigger recomposition when it changes | Use for effects that produce a value and need to be recomposed when the value changes |
Example: Using LaunchedEffect with Keys
LaunchedEffect(userId) {
if (userId != null) {
val user = userRepository.getUser(userId)
// Update UI with user data
}
}
Example: Using DisposableEffect for Listener Registration
DisposableEffect(lifecycleOwner) {
val listener = { event: Lifecycle.Event -> /* Handle event */ }
lifecycleOwner.lifecycle.addObserver(listener)
onDispose {
lifecycleOwner.lifecycle.removeObserver(listener)
}
}
LaunchedEffect is ideal for starting coroutines or performing side effects that should run once or when specific keys change. It's important to use keys correctly to avoid unnecessary re-runs.
LaunchedEffect for UI updates. Instead, use State and MutableState to manage UI state.
10. Implementing Custom Effects
Example: Custom Side Effect for Network Requests
import androidx.compose.runtime.*
import kotlinx.coroutines.*
@Composable
fun NetworkRequestEffect(
url: String,
onSuccess: (String) -> Unit,
onError: (Exception) -> Unit
) {
LaunchedEffect(url) {
try {
val response = withContext(Dispatchers.IO) {
// Simulate network call
"Response from $url"
}
onSuccess(response)
} catch (e: Exception) {
onError(e)
}
}
}
| Property | Description | Usage |
|---|---|---|
| LaunchedEffect | Launches a coroutine in the scope of the composable | Use for side effects that need to run once or on changes |
| DisposableEffect | Runs a side effect and provides cleanup logic | Use for effects that require resource management |
| RecompositionScope | Controls when recomposition occurs | Use to optimize performance in complex effects |
LaunchedEffect is ideal for asynchronous operations that should run once or when dependencies change.LaunchedEffect inside loops or conditional blocks without proper keys to prevent memory leaks.