In Jetpack Compose, side effects refer to operations that cause a change in the outside world or system state.
In the context of Jetpack Compose, side effects are necessary to interact with the environment, like triggering a network request, modifying a UI element, or interacting with a system service.
Jetpack Compose provides a set of APIs for handling side effects, and knowing when and how to use them is crucial for writing maintainable and efficient applications.
LaunchedEffect
is a side effect API used in Compose for launching coroutines that react to changes in keys or trigger side effects within the composable scope.
When you need to launch a coroutine in response to changes in a specific key.
For performing one-time effects (like fetching data, starting animations, etc.) when a composable is first composed or when a certain state changes.
LaunchedEffect
takes a key as its parameter. It runs the provided block of code only when the key changes. It is the recommended API for performing side effects within a composable.
@Composable
fun DataFetcher(key: String) {
LaunchedEffect(key) {
// Simulating network call or data fetch
val data = fetchDataFromNetwork(key)
// Do something with the data (e.g., update state)
}
}
Avoid launching long-running operations in LaunchedEffect
that might block the main thread. Always ensure that the work is offloaded to a background thread.
Don't use LaunchedEffect
for actions that you expect to happen repeatedly. Consider using rememberUpdatedState
for state changes that don’t require side effects.
SideEffect
allows you to run a block of code every time the composition is recomposed. It is a more lightweight side effect compared to LaunchedEffect
and doesn’t use coroutines.
When you need to trigger side effects that are not coroutines-based.
For actions like logging, updating analytics, or performing UI updates that are dependent on the composition state.
SideEffect
runs every time the composable recomposes, making it suitable for quick side effects like logging or updating UI-related states.
@Composable
fun Counter(counter: Int) {
// Runs every time the composable recomposes
SideEffect {
println("Counter value has changed to: $counter")
}
Text("Counter: $counter")
}
Don’t perform expensive operations inside SideEffect
because it runs on each recomposition, which could lead to unnecessary overhead.
Avoid putting side effects that need to run once or after certain actions. Use LaunchedEffect
for those cases.
rememberUpdatedState
is used to remember the current state of a variable across recompositions. It ensures that the latest value of a variable is always used without causing unnecessary recompositions.
When you need to keep a reference to a value that changes but should not trigger recompositions when that value changes.
For side effects that depend on changing state or variables but don’t want to re-trigger the effect unnecessarily.
rememberUpdatedState
keeps track of the latest value, and the composable only recomposes when the composable itself is recomposed.
@Composable
fun ExampleWithUpdatedState() {
var counter by remember { mutableStateOf(0) }
val updatedCounter = rememberUpdatedState(counter)
// Perform some action on the updated value without triggering recompositions
Button(onClick = {
// Using updated counter value
someAction(updatedCounter.value)
}) {
Text("Increase Counter")
}
}
fun someAction(counterValue: Int) {
// Use the updated value for non-recomposable operations
}
Don’t use rememberUpdatedState
as a replacement for triggering state changes in the UI. It's meant for holding and using values across recompositions without forcing recomposition itself.
DisposableEffect
is used when you need to set up resources that should be disposed of when the composable leaves the composition, such as registering listeners or opening a file. It provides a cleanup block that runs when the composable is removed from the composition.
When you need to set up resources that require cleanup.
For managing lifecycle-sensitive resources such as event listeners, sensors, or observers.
DisposableEffect
takes a key and ensures that the setup and cleanup are tied to the lifecycle of the composable.
@Composable
fun EventListener() {
// Set up event listener that needs cleanup on disposal
DisposableEffect(Unit) {
val listener = MyEventListener()
registerListener(listener)
// Clean up the listener when the composable is removed
onDispose {
unregisterListener(listener)
}
}
}
Don’t use DisposableEffect
for tasks that don’t need explicit cleanup, like simple UI updates or non-lifecycle-dependent tasks.
Avoid registering or disposing of resources on every recomposition if the effect doesn’t rely on the composition lifecycle.
rememberCoroutineScope
is used to obtain a coroutine scope that is tied to the composable’s lifecycle. It allows launching coroutines within a composable and is useful for handling side effects like network requests or long-running operations.
When you need to launch coroutines directly within a composable, ensuring they are canceled when the composable is removed from the composition.
You can use the coroutine scope to launch and manage long-running tasks like network requests or background operations.
@Composable
fun CoroutineScopeExample() {
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
// Long-running task (e.g., fetching data)
val data = fetchDataFromNetwork()
// Handle the data (e.g., update UI state)
}
}) {
Text("Fetch Data")
}
}
Don’t use rememberCoroutineScope
for tasks that should not be tied to the composable lifecycle. For tasks like background work that should continue independently, use other coroutine scopes (like GlobalScope
or custom scopes).
Avoid launching coroutines inside @Composable
functions that don’t need to be aware of the lifecycle of the composable.
@Composable
fun MyComposable(counter: Int) {
// 1st: remember state
val updatedCounter = rememberUpdatedState(counter)
// 2nd: SideEffect (runs after recomposition)
SideEffect {
println("SideEffect: Counter updated to $counter")
}
// 3rd: LaunchedEffect (coroutine runs after composition)
LaunchedEffect(counter) {
delay(1000) // Simulating background work
println("LaunchedEffect: Counter is $counter after delay")
}
// UI updates (Composables that react to state)
Text("Counter: $counter")
// 4th: DisposableEffect (runs during setup and cleanup)
DisposableEffect(counter) {
val listener = MyEventListener()
registerListener(listener)
// Cleanup when removed from composition
onDispose {
unregisterListener(listener)
}
}
// 5th: Coroutine launched using rememberCoroutineScope (user action triggers coroutine)
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
println("Coroutine launched from rememberCoroutineScope: Counter is $counter")
}
}) {
Text("Launch Coroutine")
}
}
Execution Flow:
rememberUpdatedState
is called first to remember the latest value of the state.
SideEffect
runs to log any immediate UI changes that occur due to recomposition.
LaunchedEffect
starts a coroutine asynchronously after the UI updates (e.g., fetching data or triggering animations).
DisposableEffect
handles resource cleanup if the composable is disposed, such as unregistering event listeners or cleaning up resources.
rememberCoroutineScope
is used to launch coroutines in response to user actions, keeping them independent of the composable's lifecycle.
SideEffect
runs every time the composable recomposes and does not involve coroutines.
LaunchedEffect
runs asynchronously and responds to key changes. It’s ideal for coroutines.
DisposableEffect
is used for setting up and cleaning up resources tied to the composable’s lifecycle.
rememberCoroutineScope
provides a scope for launching coroutines that can be triggered by user interactions.
Understanding the appropriate time to use each effect ensures that you maintain clean and efficient code, avoiding unnecessary recompositions and keeping your app’s performance in check.
In summary:
Use LaunchedEffect
for launching coroutines that react to key changes.
Use SideEffect
for non-coroutine side effects triggered on recomposition.
Use rememberUpdatedState
for tracking updated values without forcing recompositions.
Use DisposableEffect
for managing lifecycle-related resources that need cleanup.
Use rememberCoroutineScope
for launching coroutines tied to the composable’s lifecycle.
Akshay Nandwana
Founder AndroidEngineers
You can connect with me on:
Join our upcoming classes
https://www.androidengineers.in/courses