Blog Infos
Author
Published
Topics
Published

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)
}

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

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}")
}
view raw SideEffect.kt hosted with ❤ by GitHub

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 fullNamederivedStateOf 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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu