Jetpack Compose: Side Effects
In Jetpack Compose, side effects refer to operations that occur outside the composable functions but are still tied to their lifecycle. These operations can include things like launching coroutines, performing UI updates, or cleaning up resources. Handling side effects correctly is critical to building performant and stable apps.
Get Manish Kumar’s stories in your inbox
Join Medium for free to get updates from this writer.
In this blog, we will cover the five main side-effect handlers in Jetpack Compose:
- LaunchedEffect: Launches coroutines for operations that should run on composable entry or when a key changes.
- rememberCoroutineScope: Provides a coroutine scope for UI interactions that need to run in a coroutine.
- SideEffect: Triggers simple side effects during recomposition (e.g., logging or UI updates).
- DisposableEffect: Handles resource cleanup when composables leave the composition.
- snapshotFlow: Allows you to observe state changes with a flow and react to them in a structured manner.
Each side effect handler has a unique use case and can be employed in different scenarios. Let’s explore them with code examples to understand their usage.
1. LaunchedEffect
LaunchedEffect is used to launch a coroutine and trigger a side effect when the composable enters the composition or when specific keys change. It’s great for asynchronous operations that you want to execute when a composable is created or when certain state changes.
Example: Using LaunchedEffect for Initial Setup
@Composable
fun LaunchedEffectDemo() {
// LaunchedEffect runs only once when the composable is first displayed
LaunchedEffect(Unit) {
// This block runs as a side effect
println("LaunchedEffect triggered!")
delay(1000L)
println("Side effect finished!")
}
Text("LaunchedEffect Demo")
}Explanation:
LaunchedEffect(Unit)will trigger the coroutine when the composable is first created. If you want to trigger theLaunchEffectin recomposition then you need to pass the state variable as params in place ofUnit.- The code inside
LaunchedEffectwill run asynchronously, allowing you to handle time-consuming operations like fetching data or updating state.
2. rememberCoroutineScope
rememberCoroutineScope provides a coroutine scope that can be used within a composable. This is particularly useful when you need to launch a coroutine in response to UI interactions or other state changes.
If you have priror experience of working with MVVM and coroutine then its similar to
viewModelScopeorlifeCycleScope
Example: Using rememberCoroutineScope for Button Click
@Composable
fun CoroutineScopeDemo() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
println("Button clicked, coroutine started")
delay(2000L)
println("Coroutine finished!")
}
}) {
Text("Start Coroutine")
}
}Explanation:
rememberCoroutineScope()gives you a scope to launch a coroutine that runs within the composable’s lifecycle.- The coroutine starts when the button is clicked, simulating a long-running task.
3. SideEffect
SideEffect is used to trigger simple side effects like updating the system UI or performing any non-compositional tasks that should happen when the composable recomposes.
Example: Using SideEffect to Log Recomposition
@Composable
fun SideEffectDemo() {
var count = 0
// SideEffect is triggered every time the composable recomposes
SideEffect {
println("Recomposition occurred. Current count: $count")
}
Button(onClick = { count++ }) {
Text("Increment Count")
}
}Explanation:
SideEffectis called every time the composable recomposes. This is ideal for logging or performing actions that don't directly modify the UI but are needed when recomposition occurs.
SideEffectwill be triggered at the end of recomposition despite of location in code. Suppose you clicked the button and recompositon started, one TextField got recomposed and reached till end of method, then at last code insideSideEffectwill be triggered
4. DisposableEffect
DisposableEffect is used to manage resources that need to be cleaned up when a composable leaves the composition. This can include operations like removing listeners, canceling coroutines, or releasing any external resources.
@Composable
fun DisposableEffectDemo() {
var count by remember { mutableStateOf(0) }
// Registering a listener or resource with DisposableEffect
DisposableEffect(Unit) {
println("Listener registered")
onDispose {
println("Listener unregistered")
}
}
Button(onClick = { count++ }) {
Text("Increase Count")
}
Text("Current count: $count")
}Explanation:
DisposableEffect(Unit)allows you to perform operations when a composable enters the composition.- The
onDisposeblock runs when the composable leaves the composition, which is useful for cleaning up resources such as listeners or network requests.
Basically its just the
LaunchedEffectwhich provides separate block asonDisposeto clear the resources.
5. snapshotFlow
snapshotFlow is used to create a Flow that observes changes to Compose state. It's great for reacting to state changes in a more structured, flow-based way, and is useful for when you want to track state over time.
Example: Using snapshotFlow to Collect State Changes
@Composable
fun SnapshotFlowDemo() {
var count by remember { mutableStateOf(0) }
// snapshotFlow collects state changes and triggers actions on change
LaunchedEffect(count) {
snapshotFlow { count } // Converts state to flow
.collect { newCount ->
println("Count has changed to: $newCount")
}
}
Button(onClick = { count++ }) {
Text("Increase Count")
}
Text("Current count: $count")
}Explanation:
snapshotFlow { count }creates aFlowthat emits the current value ofcountwhenever it changes.- The
collectfunction is used to observe state changes, and the side effect logic (in this case, logging) happens whenever the state is updated.
Happy coding!!







