Jetpack Compose Side Effects – With Examples

Ali Eren ŞenAli Eren Şen
4 min read

Recompositions and states are the main parts of Jetpack Compose. Here’s how to manage states without getting affected by recompositions. We’ll use Jetpack Compose Side Effects API’s and inspect each method one by one.

Why manage states outside composables?

In our Jetpack Compose State Management article we explained:

  1. What is a state
  2. Manage state in Android
  3. Manage state in Compose

However, states can trigger in every recomposition and this might cause unexpected application behavior such as:

Showing a snackbar more than once Sending heavy load of network requests

Let’s give an example scenario:

In a login screen composable function implementation, a snackbar message is showing if the network state is an error and a login request is made when the boolean state is true.

If the boolean state triggers true and the request returns error application stucks in making a request and showing a snackbar.

To avoid such situations, we must manage states in Jetpack Compose’s side-effect APIs.

Jetpack Compose Side Effects APIs

As Developers Android states, Composables should be side-effect free(State shouldn’t get affected directly in composable function scope). Side-effect APIs are comes to our aid just right here. Here are the side effects with example usages:

LaunchedEffect:

Running suspend functions in composable lifecycle scope. LaunchedEffect is one of the most used side effects. As the name suggests, it launches when composition starts for the first time and can call suspend functions. Moreover, we can relaunch it by giving a key parameter(It relaunches when given key changes).

Example:

LaunchedEffect(networkState) { //Relaunches if network state changes
  if(networkState.Success){
      //Navigate to Screen
  }
  if(networkState.Failure){
      //Show error message
  }
}

In the code above, composable screen only navigates or shows a message when networkState changes. Recompositions or changes on other states doesn’t affect it.

rememberCoroutineScope:

Coroutine scope to run functions outside of compose scope.

Since LaunchedEffect requires a composable scope to run, it can’t be used in cases such as onClick() of a button(onClick lambda function goes out of compose scope and doesn’t have coroutine scope). That is where rememberCoroutineScope comes into play. rememberCoroutineScope creates a coroutine scope to call our actions outside of compose. Moreover, this scope will be bound to composable’s lifecycle so that when composition leaves the screen(Navigating, app termination and etc.) scope will be canceled automatically.

Example:

// Creates a CoroutineScope bound to the screen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler 
                    scope.launch {
                    // getData is a suspend function and being called in coroutine
                        repository.getData()
                    }
                }
            ) {
                Text("Get data")
            }
        }
    }

In the code above, when “Get data” button is clicked repository’s getData() suspend function is called in a coroutine and since this coroutine is compose’s lifecycle aware, we make sure that coroutine won’t continue to run if screen’s lifecycle ends.

DisposableEffect:

Like LaunchedEffect but works while leaving the composition. While working with fragments, onDestroy life cycle event was where we do our cleanup before leaving the fragment. In Compose we use DisposableEffect scope for the same purpose. DisposableEffect triggers while leaving the composition or if the given key is changes (like in LaunchedEffect).

Example:

DisposableEffect(key1 = startStop) {
      //Sending start event when screen starts or key changes
      firebaseEventSender.sendStartEvent()
      onDispose {
      //Sending stop event before leaving the screen or before starting
       the new effect due to key changes
        firebaseEventSender.sendStartEvent()
      }
}

SideEffect:

Running non-suspend functions in every recomposition. SideEffect is basically, a kind of LaunchedEffect. It launches in every recomposition and saves us from creating a state for LaunchedEffect but it can’t run suspend functions directly.

Example:

var timer by remember { mutableStateOf(0) }

Box(modifier = Modifier.fillMaxSize(), 
  contentAlignment = Alignment.Center) {
  Text("Time $timer")
}

SideEffect {
  Thread.sleep(1000)
  timer++
}

In the code above, the timer state is changing in every second. Since it’s a state recomposition and SideEffect triggers in each change. However, we couldn’t use any suspend delay functions such as delay() because SideEffect can’t run suspend functions.

produceState:

LaunchedEffect with its own key. LaunchedEffect runs when the given key changes. So a key must be defined and has to change in composition. However, produceState comes with key, so it can run by itself.

Example:

val timer by produceState(initialValue = 0) {
  delay(1000)
  repository.sendSignal()
  value++
}

In the code above produceState sending a signal in every second without causing a recomposition to screen.

Conclusion

Firstly, we learned that side-effect APIs are here to help us connect our noncomposition aware code with our composable screens. Secondly, there are different type of side-effects to fit our cases. Finally, side-effects are not mandatory. For example, we can use viewmodels(init function) to have the same case that we have with LaunchedEffect.

Where to Go From Here

Try and be addicted to the Appcircle fastest Mobile CI/CD Platform

5
Subscribe to my newsletter

Read articles from Ali Eren Şen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Eren Şen
Ali Eren Şen

Creating Appcircle.io your helper in mobile CI/CD