State of Compose 2023 results are in! Click here to learn more
Published on

Everything you need to know about Side Effects in Jetpack Compose with examples

Authors

Most composable functions such as Buttons and Text render parts of your UI. There are cases where composables need to execute code that is not part of the screen's UI state. That could be sending analytics events or displaying Toasts.

As we do not control when composable functions are going to be executed or how many times, this makes composable functions unreliable to call such code. Because of this, Jetpack Compose provides specific composable functions that make it safe to call any code that is unrelated to rendering your UI.

After reading this article you will become confident in using side effects in your composable functions. We will see why side effect composables are important, what happens if you don't use them and what options there are so that you know what to use for each use case.

Featured in Android Weekly #555

Never run non-composable functions within your composable functions

We do not have control over how many times composable functions are going to run. We also do not have control over the order in which composable functions are going to run on.

🌋 Composable functions are lava

Think of Jetpack Compose side effect functions as safe spaces where you can run your non-composable code. In other words, everything in a composable function is lava and side effects composables are the safe spot you can run your code in:

@Composable
function LavaComposable() {

  val context = LocalContext.current

  // 🚫 don't call makeToast() here
  // you will end up with multiple Toasts being displayed
  // in undefinded moments
  Toast.makeToast(context, "💀", Toast.LENGTH_SHORT).show()

  LaunchedEffect(Unit) {
    // ✅ safe to run any non-composable code here
    // You will end up with one Toast when the LavaComposable
    // enters the composition
    Toast.makeToast(context, "🏝️", Toast.LENGTH_SHORT).show()
  }
}

Which side effect composable to use for each use case

There are multiple side effect composables you might want to use. Let's go through all of them so that you are aware of what is available and when to use each one:

SideEffect() on every recomposition

A SideEffect will run its lambda on every composition. This includes the first time the composable will be added to the composition, plus on every other recomposition.

I haven't had the need to use such side effect function in my apps, so here is the example from the official documentation:

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

LaunchedEffect() on enter composition and when keys are updated

The LaunchedEffect() will run its lambda as soon as it enters the composition. It will run its lambda again whenever one of the given keys change.

One nice thing about LaunchedEffect() is that it provides access to a coroutine scope, so you can run suspend functions.

The following example will display "Wait for a surprise on the screen" and uses delay() to display a toast after 2 seconds:

@Composable
fun SurpriseComposable() {
    Text("Wait for a surprise ⏰")

    LaunchedEffect(Unit) {
        delay(2000)
        Toast.makeToast(context, "🎊 Surprise 🎊", Toast.LENGTH_SHORT).show()
    }
}

In the previous sample, the Toast is displayed only once as the key passed to the LaunchedEffect is always the same (Unit is a singleton). LaunchEffect will run its lamda once again whenever the keys passed into it gets updated.

In the following example, we observe the size of the list and print it when it gets changed. When the list is empty, the onListEmptied function is called:

@Composable
fun PoppingList(onListEmptied : () -> Unit) {
    val list = remember { mutableStateListOf(1, 2, 3, 4, 5) }
    LaunchedEffect(list.size) {
        println("${list.size} items left")
        if (list.isEmpty()) {
            println("No more left! Bail")
            onListEmptied()
        }
    }
    Button(onClick = { list.removeLast() }) {
        Text("Pop")
    }
}

You can pass as many keys into the LaunchedEffect() composable as you like.

As a LaunchedEffect() provide a coroutine scope, it is a good candidate to collect your Kotlin Flows from.

What I find myself doing often is collecting one-time events from my View Models. Those are events that are emitted from my ViewModels but are not part of the state:

@Composable
fun CollectFlow() {
    val viewModel = viewModel { MyViewModel() }

    LaunchedEffect(Unit) {
        viewModel.sideEffects.collect { effect ->
            // TODO handle the effect
        }
    }
}

Speaking of View Models, if you ever need to run non-compose code as a result of a state change (such as navigating to a new screen) you would have to use a LaunchedEffect():

@Composable
fun NavigateOnStateChange(onNavigateAway : () -> Unit) {
    val viewModel = viewModel { MyViewModel() }

    val state = viewModel.state
    when(state) {
        UserJourneyCompleted -> LaunchedEffect(Unit) {
            // make sure the navigation happens in a launched effect
            // otherwise you risk having it called multiple times
            onNavigateAway()
        }
        // .. your other states here
    }
}

DiposableEffect(): like LaunchedEffect() but with clean-up

DisposableEffect() works very similarly like LaunchedEffect(). They will both run their side effect as soon as they enter the composition. They will both re-run the side-effect as soon as the passed keys are changed.

The unique thing about DisposableEffect() is the onDispose {} callback. As soon as the passed keys change or the composable leaves the composition, the onDispose {} callback is executed. This is great if you need to perform some cleanup such as freeing resources. Keep in mind that DisposableEffect() does not provide a coroutine scope to run your suspend functions from.

Here is an example of how you would register a BroadcastReceiver from a composable function. The receiver is unregistered when the composable leaves the composition:

@Composable
fun BroadcastReceiver(intentFilter: IntentFilter, onReceive: (Intent) -> Unit) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val broadcastReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                onReceive(intent)
            }
        }
        context.registerReceiver(broadcastReceiver, intentFilter)
        onDispose {
            context.unregisterReceiver(broadcastReceiver)
        }
    }
}

Consider if you really need any effect functions

There is a big chance that you might not need any of the composable functions mentioned above. Most of the times, you might end up running some non-composable function as a result of a user input (ie when the user clicks on a button).

Callbacks provided by existing composables give you a safe spot to run your non-composable code from.

The earlier LaunchedEffect() example can be rewritten by writing the checking logic in the buttons' onClick:

@Composable
fun ButtonClick() {
    val list = remember { mutableStateListOf(1, 2, 3, 4, 5) }
    val context = LocalContext.current

    Button(onClick = {
        // this callback will run once. safe to run your non-composable code

        if (list.isEmpty()) {
            Toast.makeText(context, "List is empty", Toast.LENGTH_SHORT).show()
        } else {
            list.removeLast()
        }
    }) {
        Text("Pop: ${list.size} left")
    }
}

As a rule of thumb, if the code you are trying to run is part of a user action's callback then high chance is you don't need an effect composable to run your code from.

🎁 BONUS: How to launch coroutines from your composable functions

Even though LaunchedEffect() allows you to run your code within a coroutine scope, you will find yourself in need of a coroutine scope instance while using Jetpack Compose.

There is this common pattern in Jetpack Compose that some animation related API require to be launched from a coroutine scope. This is nice as you get notified immediately as soon as the animation ends. An example of this is the SnackbarHostState.showSnackbar().

Luckily, you can use rememberCoroutineScope() in order to get a reference to a coroutine instance and use it to launch your coroutines:

@Composable
fun ShowSnackbar() {
    Box(Modifier.fillMaxSize()) {
        val snackbarHostState = remember { SnackbarHostState() }
        val scope = rememberCoroutineScope()
        Button(onClick = {
            scope.launch {
                val result = snackbarHostState.showSnackbar(
                    "Message deleted", actionLabel = "Undo"
                )
                when (result) {
                    SnackbarResult.Dismissed -> {
                        // nothing to do
                    }
                    SnackbarResult.ActionPerformed -> {
                        // TODO perform undo
                    }
                }
            }
        }
        ) {
            Text("Delete")
        }
        SnackbarHost(
            hostState = snackbarHostState,
            modifier = Modifier.align(Alignment.BottomCenter),
        )
    }
}

You still need some safe space to run your coroutines from. You do not want to launch a coroutine outside a side-effect composable or a callback. If you launch your coroutines outside of such safe space, you risk launching new coroutines in undefined moments (as each new composition might rerun the coroutine launching code).


In this article you learnt everything you need to know about side effects in Jetpack Compose. You learnt why you should not run non-composable code from composable functions. You are now aware which effect composable function to use in order to call code safely from your composable functions.


Further reading


Here is how I can help you