Skip to main content

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"
}
Note: 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.
Warning: Avoid using 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")
}
Note: The 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.

Example: Logging in SideEffect

SideEffect {
    Log.d("Compose", "This is a side effect")
}
Note: 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
Note: rememberCoroutineScope ensures that launched coroutines are cancelled when the composable is disposed.
Warning: Avoid launching long-running coroutines without proper scope management to prevent memory leaks.

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")
    }
}
Note: 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
}
Note: 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
        )
    }
}
Note: The BackHandler is only active when the composable is in the current back stack.
Warning: If multiple 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
LaunchedEffect(key1) {
    // Perform side effect
}
DisposableEffect Performs a side effect and provides a cleanup function when the composable is disposed
DisposableEffect(key1) {
    val listener = MyListener()
    onDispose {
        // Cleanup logic
    }
}
RecompositionEffect Triggers recomposition when a specific condition changes
RecompositionEffect(key1) {
    // Triggers recomposition on key change
}

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 LaunchedEffect for side effects that should run once or when specific keys change.
  • Avoid using LaunchedEffect for UI updates; prefer State and MutableState instead.
  • Use DisposableEffect for effects that require cleanup, such as registering and unregistering listeners.
  • Use SideEffect for effects that should run after composition, such as logging or updating external state.
  • Use produceState for 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)
    }
}
Note: 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.
Warning: Avoid using 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
Note: LaunchedEffect is ideal for asynchronous operations that should run once or when dependencies change.
Warning: Avoid using LaunchedEffect inside loops or conditional blocks without proper keys to prevent memory leaks.

Example: Using DisposableEffect for Lifecycle Management

import androidx.compose.runtime.*

@Composable
fun LifecycleEffect(
    onStarted: () -> Unit,
    onDisposed: () -> Unit
) {
    DisposableEffect(Unit) {
        onStarted()
        onDispose {
            onDisposed()
        }
    }
}

Was this article helpful?