Jetpack Compose is a preferred choice for many developers due to its fun, easy, effective, and straightforward nature, along with its ability to build custom components easily and declaratively. However, to fully leverage its capabilities, it’s important to have a good grasp of side-effects and effect handlers.
What is Side-effect?
When building UIs in Android, managing side effects can be one of the biggest challenges developers face. It is a change of state of the app that happens outside the scope of a composable function.
// Side Effect | |
private var i = 0 | |
@Composable | |
fun SideEffect() { | |
var text by remember { | |
mutableStateOF("") | |
} | |
Column { | |
Button(onClick = { text += "@" }) { | |
i++ | |
Text(text) | |
} | |
} | |
} |
In this example, SideEffect creates a mutable state object using
mutableStateOf, with an initial value of an empty string. Now on button click we are updating the text and on text update we want to update the value of
i. But
Button composable can recompose even without the click which will not change the text but will increment value of
i. If it was a network call then it would make a network call on every recomposition of
Button.
Ideally your composable should be Side-effect free but there are times when you need Side-effects. e.g. to trigger one-off event such as making a network call or collecting a flow.
To solve these issues, Compose offers various side-effects for different situations, including the following:
LaunchedEffect
LaunchedEffect is a composable function that is used to launch a coroutine inside the scope of composable, when
LaunchedEffect enters the composition, it launches a coroutine and cancels when it leaves composition.
LaunchedEffect takes multiple keys as params and if any of the key changes it cancels the existing coroutine and launch again. This is useful for performing side effects, such as making network calls or updating a database, without blocking the UI thread.
// Launched Effect | |
private var i = 0 | |
@Composable | |
fun SideEffect() { | |
var text by remember { | |
mutableStateOF("") | |
} | |
LaunchedEffect(key1 = text) { | |
i++ | |
} | |
Column { | |
Button(onClick = { text += "@" }) { | |
Text(text) | |
} | |
} | |
} |
In the example above, each time the text is updated, a new coroutine is launched and the value of i
is updated accordingly. This function is side-effect-free, since i
is only incremented when the text value changes.
rememberCoroutineScope
To ensure that LaunchedEffect
launches on the first composition, use it as is. But if you need manual control over the launch, use rememberCoroutineScope
instead. It can be use to obtain a composition-aware scope to launch coroutine outside composable. It is a composable function that returns a coroutine scope bound to the point of Composable where its called. The scope will be cancelled when the call leaves the composition.
@Composable | |
fun MyComponent() { | |
val coroutineScope = rememberCoroutineScope() | |
val data = remember { mutableStateOf("") } | |
Button(onClick = { | |
coroutineScope.launch { | |
// Simulate network call | |
delay(2000) | |
data.value = "Data loaded" | |
} | |
}) { | |
Text("Load data") | |
} | |
Text(text = data.value) | |
} |
Here, rememberCoroutineScope
is used to create a coroutine scope that is tied to the Composable function’s lifecycle. This lets you manage coroutines efficiently and safely by ensuring they are cancelled when the Composable function is removed from the composition. You can use the launch
function within the scope to easily and safely manage asynchronous operations.
rememberUpdatedState
When you want to reference a value in effect that shouldn’t restart if the value changes then use rememberUpdatedState
. LaunchedEffect restart when one of the value of the key parameter get updated but sometimes we want to capture the changed value inside the effect without restarting it. This process is helpful if we have long running option that is expensive to restart.
@Composable | |
fun ParentComponent() { | |
setContent { | |
ComposeTheme { | |
// A surface container using the 'background' color from the theme | |
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { | |
var dynamicData by remember { | |
mutableStateOf("") | |
} | |
LaunchedEffect(Unit) { | |
delay(3000L) | |
dynamicData = "New Text" | |
} | |
MyComponent(title = dynamicData) | |
} | |
} | |
} | |
} | |
@Composable | |
fun MyComponent(title: String) { | |
var data by remember { mutableStateOf("") } | |
val updatedData by rememberUpdatedState(title) | |
LaunchedEffect(Unit) { | |
delay(5000L) | |
data = updatedData | |
} | |
Text(text = data) | |
} |
Job Offers
Initially, data
is an empty string. After 3 seconds, updatedData
becomes “New Text”. After 5 seconds, data
becomes “New Text” as well, triggering a recomposition of the UI. This updates the Text
composable. So the total delay was 5 seconds and if we haven’t used the rememberUpdatedState
then we had to relaunch the second LaunchedEffect
which would’ve taken 8 seconds.
DisposableEffect
The DisposableEffect
composable is utilized to execute an effect when a Composable function is initially created. It then clears the effect when the Composable is removed from the screen.
@Composable | |
fun MyComponent() { | |
var data by remember { mutableStateOf("") } | |
val disposableEffect = remember { mutableStateOf<Disposable?>(null) } | |
DisposableEffect(Unit) { | |
val disposable = someAsyncOperation().subscribe { | |
data = it | |
} | |
onDispose { | |
disposable.dispose() | |
} | |
disposableEffect.value = disposable | |
} | |
// rest of the composable function | |
} |
In this example, we create a Composable called MyComponent. It has two mutable state variables:
data and
disposableEffect.
In DisposableEffect, we call an asynchronous operation using
someAsyncOperation() which returns an
Observable that emits a new value when the operation completes. We subscribe to it and update
data.
We also use onDispose to dispose of
disposable and stop the operation when the Composable is removed.
Finally, we set disposableEffect to the disposable object so it can be accessed by the calling Composable.
SideEffect
SideEffect
is used to publish compose state to non-compose code. The SideEffect
is triggered on every recomposition and it is not a coroutine scope, so suspend functions cannot be used within it.
When I initially discovered this side effect, I was uncertain about its significance and the extent of its importance, so I delved deeper into the matter for a better understanding.
class Ref(var value: Int) | |
@Composable | |
inline fun LogCompositions(tag: String) { | |
val ref = remember { Ref(0) } | |
SideEffect { ref.value++ } | |
Logger.log("$tag Compositions: ${ref.value}") | |
} |
When the effect is invoked, it will log the number of compositions that were created.
produceState
produceState
converts non-compose state into compose state. It launches a coroutine scoped to the composition that can push values into a returned state. The producer is started when produceState
enters the Composition and is stopped when it leaves the Composition. The returned State
combines; setting the same value will not cause a recomposition.
Here’s an example of how to use produceState
to load an image from the network. The loadNetworkImage
composable function provides a State
that can be used in other composables. This example has been picked from official documentation.
@Composable | |
fun loadNetworkImage( | |
url: String, | |
imageRepository: ImageRepository = ImageRepository() | |
): State<Result<Image>> { | |
// Creates a State<T> with Result.Loading as initial value | |
// If either `url` or `imageRepository` changes, the running producer | |
// will cancel and will be re-launched with the new inputs. | |
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { | |
// In a coroutine, can make suspend calls | |
val image = imageRepository.load(url) | |
// Update State with either an Error or Success result. | |
// This will trigger a recomposition where this State is read | |
value = if (image == null) { | |
Result.Error | |
} else { | |
Result.Success(image) | |
} | |
} | |
} |
derivedStateOf
derivedStateOf is a composable that can be used to derive new state based on the values of other state variables. It is useful when you need to compute a value that depends on other values, and you want to avoid recomputing the value unnecessarily.
Here’s an example of how to use derivedStateOf:
@Composable | |
fun MyComponent() { | |
var email by remember { mutableStateOf("") } | |
val isValidEmail = remember { | |
derivedStateOf { | |
isEmailValid(email) | |
} | |
} | |
} |
In the example, we declare a Composable function called MyComponent that has two mutable state variables called
firstName and
lastName. We then use
derivedStateOf to create a new state variable called
fullName , which concatenates the values of
firstName and
lastName. Whenever
firstName or
lastName changes,
derivedStateOf recomposes the composable and updates
fullName. Finally, we use the
Text composable to display
fullName.
derivedStateOf is useful for computing derived state variables that depend on other state variables, without recomputing the derived state unnecessarily.
snapshotFlow
snapshotFlow is a function that allows you to create a flow that emits the current value of a state object, and then emits any subsequent changes to that object. This can be useful for creating reactive UIs that respond to changes in state, without having to manually manage callbacks or listeners.
Here’s an example of how snapshotFlow can be used in Compose:
@Composable | |
fun MyComponent() { | |
val count = remember { mutableStateOf(0) } | |
val countFlow = snapshotFlow { count.value } | |
LaunchedEffect(countFlow) { | |
countFlow.collect { value -> | |
// Handle the new value | |
} | |
} | |
Button(onClick = { count.value++ }) { | |
Text("Clicked ${count.value} times") | |
} |
In this example, MyComponent creates a mutable state object using
mutableStateOf(0).
snapshotFlow is then called with a lambda that returns the current value of the state object. The resulting
countFlow flow emits the current value and any subsequent changes to the state object.
LaunchedEffect is used to collect from the
countFlow flow, ensuring that the collection only occurs when the component is active and stops when it’s removed. Finally, a
Button is used to update the state object when clicked.
This article was previously published on proandroiddev.com