Side-Effects in Compose - Jetpack Compose - Android Developers
Side-Effects in Compose - Jetpack Compose - Android Developers
A side-effect is a change to the state of the app that happens outside the scope of a
composable function. Due to composables' lifecycle and properties such as
unpredictable recompositions, executing recompositions of composables in different
orders, or recompositions that can be discarded, composables should ideally be side-
effect free (/develop/ui/compose/mental-model).
However, sometimes side-effects are necessary, for example, to trigger a one-off event
such as showing a snackbar or navigate to another screen given a certain state condition.
These actions should be called from a controlled environment that is aware of the
lifecycle of the composable. In this page, you'll learn about the different side-effect APIs
Jetpack Compose offers.
Key Term: An effect is a composable function that doesn't emit UI and causes side effects to run
when a composition completes.
Due to the different possibilities effects open up in Compose, they can be easily
overused. Make sure that the work you do in them is UI related and doesn't break
unidirectional data flow as explained in the Managing state documentation
(/develop/ui/compose/state#unidirectional-data-flow-in-jetpack-compose).
Note: A responsive UI is inherently asynchronous, and Jetpack Compose solves this by embracing
coroutines at the API level instead of using callbacks. To learn more about coroutines, check out the
Kotlin coroutines on Android (/kotlin/coroutines) guide.
LaunchedEffect : run suspend functions in the scope of a composable
To call suspend functions safely from inside a composable, use the LaunchedEffect
(/reference/kotlin/androidx/compose/runtime/package-
summary#LaunchedEffect(kotlin.Any,kotlin.coroutines.SuspendFunction1))
composable. When LaunchedEffect enters the Composition, it launches a coroutine with
the block of code passed as a parameter. The coroutine will be cancelled if
LaunchedEffect leaves the composition. If LaunchedEffect is recomposed with
different keys (see the Restarting Effects (#restarting-effects) section below), the existing
coroutine will be cancelled and the new suspend function will be launched in a new
coroutine.
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
snackbarHostState: SnackbarHostState
) {
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
// ...
}
}
/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt#L56-L97)
In the code above, a coroutine is triggered if the state contains an error and it'll be
cancelled when it doesn't. As the LaunchedEffect call site is inside an if statement, when
the statement is false, if LaunchedEffect was in the Composition, it'll be removed, and
therefore, the coroutine will be cancelled.
Following the previous example, you could use this code to show a Snackbar when the
user taps on a Button :
@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
Button(
onClick = {
// Create a new coroutine in the event handler to show a
scope.launch {
snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
nippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt#L101-L125)
For example, suppose your app has a LandingScreen that disappears after some time.
Even if LandingScreen is recomposed, the effect that waits for some time and notifies
that the time passed shouldn't be restarted:
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
To create an effect that matches the lifecycle of the call site, a never-changing constant
like Unit or true is passed as a parameter. In the code above, LaunchedEffect(true) is
used. To make sure that the onTimeout lambda always contains the latest value that
LandingScreen was recomposed with, onTimeout needs to be wrapped with the
rememberUpdatedState function. The returned State , currentOnTimeout in the code,
should be used in the effect.
Warning: LaunchedEffect(true) is as suspicious as a while(true). Even though there are valid use
cases for it, always pause and make sure that's what you need.
As an example, you might want to send analytics events based on Lifecycle events
(/topic/libraries/architecture/lifecycle#lc) by using a LifecycleObserver
(/reference/androidx/lifecycle/LifecycleObserver). To listen for those events in Compose, use a
DisposableEffect to register and unregister the observer when needed.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
In the code above, the effect will add the observer to the lifecycleOwner . If
lifecycleOwner changes, the effect is disposed and restarted with the new
lifecycleOwner .
For example, your analytics library might allow you to segment your user population by
attaching custom metadata ("user properties" in this example) to all subsequent analytics
events. To communicate the user type of the current user to your analytics library, use
SideEffect to update its value.
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
The producer is launched when produceState enters the Composition, and will be
cancelled when it leaves the Composition. The returned State conflates; setting the
same value won't trigger a recomposition.
Even though produceState creates a coroutine, it can also be used to observe non-
suspending sources of data. To remove the subscription to that source, use the
awaitDispose
(/reference/kotlin/androidx/compose/runtime/ProduceStateScope#awaitDispose(kotlin.Function0))
function.
The following example shows how to use produceState to load an image from the
network. The loadNetworkImage composable function returns a State that can be used
in other composables.
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
Key Point: Under the hood, produceState makes use of other effects! It holds a result variable using
remember { mutableStateOf(initialValue) }, and triggers the producer block in a
LaunchedEffect. Whenever value is updated in the producer block, the result state is updated to
the new value.
You can easily create your own effects building on top of the existing APIs.
Caution: derivedStateOf is expensive, and you should only use it to avoid unnecessary
recomposition when a result hasn't changed.
Correct usage
The following snippet shows an appropriate use case for derivedStateOf :
@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
Box {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
}
ippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt#L240-L265)
In this snippet, firstVisibleItemIndex changes any time the first visible item changes.
As you scroll, the value becomes 0 , 1 , 2 , 3 , 4 , 5 , etc. However, recomposition only needs
to occur if the value is greater than 0 . This mismatch in update frequency means that this
is a good use case for derivedStateOf .
Incorrect usage
A common mistake is to assume that, when you combine two Compose state objects, you
should use derivedStateOf because you are "deriving state". However, this is purely
overhead and not required, as shown in the following snippet:
Warning: The following snippet shows an incorrect usage of derivedStateOf. Do not use this code in
your project.
In this snippet, fullName needs to update just as often as firstName and lastName .
Therefore, no excess recomposition is occurring, and using derivedStateOf is not
necessary.
The following example shows a side effect that records when the user scrolls past the
first item in a list to analytics:
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Restarting effects
Some effects in Compose, like LaunchedEffect , produceState , or DisposableEffect ,
take a variable number of arguments, keys, that are used to cancel the running effect and
start a new one with the new keys.
Due to the subtleties of this behavior, problems can occur if the parameters used to
restart the effect are not the right ones:
Restarting effects less than they should could cause bugs in your app.
As a rule of thumb, mutable and immutable variables used in the effect block of code
should be added as parameters to the effect composable. Apart from those, more
parameters can be added to force restarting the effect. If the change of a variable
shouldn't cause the effect to restart, the variable should be wrapped in
rememberUpdatedState (#rememberupdatedstate). If the variable never changes because
it's wrapped in a remember with no keys, you don't need to pass the variable as a key to
the effect.
Key Point: Variables used in an effect should be added as a parameter of the effect composable, or
use rememberUpdatedState.
In the DisposableEffect code shown above, the effect takes as a parameter the
lifecycleOwner used in its block, because any change to them should cause the effect
to restart.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Constants as keys
You can use a constant like true as an effect key to make it follow the lifecycle of the
call site. There are valid use cases for it, like the LaunchedEffect example shown above.
However, before doing that, think twice and make sure that's what you need.
Content and code samples on this page are subject to the licenses described in the Content License
(/license). Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.