DEV Community

Cover image for Side Effects Summary in Jetpack Compose
Vincent Tsen
Vincent Tsen

Posted on • Originally published at vtsen.hashnode.dev

Side Effects Summary in Jetpack Compose

Do you find side effects in Jetpack Compose confusing? I do. So, I document the summary of using side effects here for my future reference.

A side effect in Compose is a change to the state of the app that happens outside the scope of a composable function.

To handle these side effects, you can use effect handlers. There are 2 types of effect handlers.

Suspended Effect Handler Non-suspended Effect Handler
LaunchedEffect() DisposableEffect()
rememberCoroutineScope() SideEffect()

Suspended Effect Handler

A suspended effect handler allows you to perform side effects in compose using coroutines.

Here is the summary that covers different scenarios:

Scenarios LaunchedEffect() rememberCoroutineScope()
Launch effect/coroutine from within a composable? Yes No
Launch effect/coroutine outside a composable? E.g. from the callback No Yes
When effect/coroutine is started? LaunchedEffect() enters composition CoroutineScope.launch() is called
When effect/coroutine is canceled? LaunchedEffect() leaves composition, LaunchedEffect() is restarted (effect's key changed) CoroutineScope leaves composition. Note: When coroutine is restarted, the previous coroutine will NOT be canceled
When effect/coroutine is restarted? LaunchedEffect()'s key changed (while it is still in composition) CoroutineScope.launch() is called again

Non-suspended Effect Handler

For non-suspended/non-coroutine side effects, you can use these non-suspended effect handlers.

Scenarios DisposableEffect() SideEffect()
Launch effect from within a composable? Yes Yes
Launch effect outside a composable? E.g. from the callback No No
When effect is started? DisposableEffect() enters composition, after the current composition completes SideEffect() enters composition, after the current composition completes
When effect is canceled? Effect can NOT be canceled, since the execution can NOT be suspended(non-suspended function) Effect can NOT be canceled - same as DisposableEffect()
When effect is restarted? DisposableEffect()'s key changed (while it is still in composition) SideEffect() enters recomposition - every recomposition triggers the SideEffect() to run
When onDispose() is called? DisposableEffect() leaves composition, DisposableEffect() is restarted (effect's key changed) Note: When the effect completes, it will NOT trigger onDispose() N/A

Various Side-effect States

These are the various compose state helpers for effect handlers above.

rememberUpdatedState

rememberUpdatedState makes sure the MutableState is always updated with the latest value instead of caching the initial composition value.

See notes (1), (2), (3), (4) and (5) below.

@Composable
fun RememberUpdatedStated(value: String) {
    val oldValue by remember { mutableStateOf(value) }
    val newValue by rememberUpdatedState(value)

    // (2) LaunchedEffect is skipped during the second recomposition
    //     when value is changed/updated
    LaunchedEffect(true) {
        // (1) let's assume value is updated with a new value within 1 second delay
        delay(1000)
        // Access value, oldvalue and newValue here

        // (3) value is the initial value when LaunchedEffect is first called
        // (4) oldValue is the initial value from first composition
        // (5) newValue is the new value from second recomposition        
    }
}
Enter fullscreen mode Exit fullscreen mode

Only the newValue has the latest updated value from the second recomposition

If you look at the rememberUpdatedState implementation, it applies the new value to the MutableState whenever it is called or during recomposition.

@Composable  
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {  
  mutableStateOf(newValue)  
}.apply { value = newValue }
Enter fullscreen mode Exit fullscreen mode

Technically,

val newValue by rememberUpdatedState(value)
Enter fullscreen mode Exit fullscreen mode

is equivalent to

var newValue by remember { mutableStateOf(value) }  
newValue = value
Enter fullscreen mode Exit fullscreen mode

produceState

produceState is the sugar syntax for LaunchedEffect().

LaunchedEffect() below can be written as

@Composable
fun DemoLaunchedEffect(){
    var text by remember { mutableStateOf("")}

    LaunchedEffect(true) {
        repeat(10) { count ->
            delay(1000)
            text = count.toString()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

produceState() below:

@Composable
fun DemoProduceState(){
    val text by produceState(initialValue ="") {

        repeat(10) { count ->
            delay(1000)
            value = count.toString()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

One small difference of produceState is it produces State<T> instead of MutableState<T>. Thus, you see the text above is declared with val instead of var.

One additional thing produceState can do is awaitDispose() function, which allows you to detect when the produceState leaves the composition. This is similar to onDispose() from DisposableEffect()

@Composable
fun DemoProduceStateAwaitDispose(){
    val text by produceState(initialValue ="") {
        val job = MainScope().launch {
            repeat(10) { count ->
                delay(1000)
                value = count.toString()
            }
        }

        awaitDispose {
            job.cancel()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

However, to use awaitDispose() you MUST manually launch a coroutine with a CoroutineScope. The following example uses the MainScope().

Without the MainScope() or any CorutineScope, the awaitDispose() will not be called. For example, the following code won't work.

@Composable
fun DemoProduceStateAwaitDispose(){
    val text by produceState(initialValue ="") {

        repeat(10) { count ->
            delay(1000)
            value = count.toString()
        }

        // Won't work - awaitDispose() won't be called
        awaitDispose {
            job.cancel()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For some reason, this requirement is not documented. It takes me a while to figure out the awaitDispose() requires you to launch the coroutine manually to get it working correctly.

derivedStateOf

Not sure why this is categorized under side effects, but what derivedStateOf() does, is combining multiple states into a single State.

@Composable
fun DemoDerivedStateOf() {
    var value1 by remember { mutableStateOf(true) }
    var value2 by remember { mutableStateOf(false) }

    val derivedValue by remember(value1) {
        derivedStateOf {
            "value1: $value1 + value2: $value2"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When value1 or value2 is changed, derivedValue is updated. The remember(value1) is needed so that, during recomposition, derivedStateOf() is skipped.

If you remove the remember(value1) as the following, everything still works correctly.

@Composable
fun DemoDerivedStateOf() {
    var value1 by remember { mutableStateOf(true) }
    var value2 by remember { mutableStateOf(false) }

    val derivedValue by derivedStateOf {
        "value1: $value1 + value2: $value2"        
    }
}
Enter fullscreen mode Exit fullscreen mode

However, during every recomposition, this line "value1: $value1 + value2: $value2" is executed. Thus, it simply wastes unnecessary resources here.

snapShotFlow

snapShotFlow converts State<T> to Flow<T>. Here is an example:

@Composable
fun DemoSnapShotFlow(){
    var textState =  remember { mutableStateOf("") }

    LaunchedEffect(textState) {
        // Convert State<T> to Flow<T>
        val flow = snapshotFlow { textState.value } 
        // Ensure flow doesn't emit the same value twise
        flow.distinctUntilChanged()
        // Collect the flow
        flow.collect { text ->
            Log.d("[SnapShotFlow]", "collecting flow value: $text")  
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For more information about collecting flow, refer to the following article:

Conclusion

I just merely document these side effect handlers' usages and their behaviors. However, I haven't known how to use them effectively yet. Sometimes I do find it confusing which one to use. :)

Source Code

This is my experimental code of playing around with side effects in Jetpack Compose. So it might be a bit messy here.

GitHub Repository: Demo_ComposeSideEffects


Originally published at https://vtsen.hashnode.dev.

Latest comments (0)