Side-Effects and Effect-Handlers In Jetpack Compose šŸ˜

Jayant KumaršŸ‡®šŸ‡³
9 min readApr 18

--

Photo by Alexander Grey on Unsplash

The code that will run outside the scope of any composable function is called side-effects.

Why we write any code outside the scope of any composable function?

it is because Due to composables’ lifecycle and properties such as unpredictable recompositions, executing recompositions of composables.

Let’s understand through an example why we need to use side-effects in compose project.

@Composable
fun WithoutSideEffectExample() {

var counter by remember { mutableStateOf(0) }
val context = LocalContext.current

// on every recomposition , this toast will show
context.showToast("Hello")

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { counter++ }) {
Text(text = "Click here")
}
SpacerHeight()
Text(text = "$counter")
}

}

As you can see in the above example , I have written code without using side-effects. This code have a button and text , and on every button clicks , a counter variable increases and show into the Text composable function. Also we are showing a toast message when our app launches first time.

As it works fine when we launch the application first time but when we click on the button , counter variable will increase and also toast message will show again , again we click on the button , toast will show again. See in the below gif.

without using side-effects

Why? We all know that on state changes (here counter variable changes , because it’s a mutableState) recomposition will occur(this composable function rebuild again).

In that case we have to write our toast code inside side-effect so that it will run only once and we can control over this code.

Note :- Never run any non-composable code inside composable function, always use side-effect for that.

To handle these side effects we have various effect-handlers and side-effect states.

There are two types of Effect-Handlers

Suspended effect handler

This effect-handler is for suspending functions

  • LaunchEffect
  • rememberCoroutineScope

Non-suspended effect handler

This effect-handler is for non-suspending functions

  • DisposableEffect
  • SideEffect

There are four types of Side-Effect States

  • rememberUpdateState
  • produceState
  • derivedStateOf
  • snapShotFlow

Let’s understand all these one by one

LaunchEffect

LaunchEffect is a composable function that is used to execute side-effect when a component is launched. it takes two parameter key and coroutineScope block.

  • In key parameter you can pass any state because it’s a type of Any .
  • In the coroutineScope block you can pass any suspended or non-suspended function.
  • LaunchEffect will always run once in the composable function.
  • If you want to run the LaunchEffect block again then you have to pass any state (mutableStateOf , StateFlow) which is changing over a time in the key parameter.

Loads of theory let’s understand through an example

Example — 1

Lets overcome the above toast issue with LaunchEffect

@Composable
fun WithLaunchEffect() {

var counter by remember { mutableStateOf(0) }
val context = LocalContext.current


LaunchedEffect(key1 = true) {
context.showToast("Hello")
}

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { counter++ }) {
Text(text = "Click here")
}
SpacerHeight()
Text(text = "$counter")
}

}

As you can see in the above code we shift the showToast code in the launch effect . It means that the launch effect block will called once in the composable function when you launched the app first time.

Now toast is showing only one time, there is no impact on the toast code after clicking on the button.

Suppose your toast code in the launch effect and you want to show toast message while clicking on the button. Let’s see how we can do this ?

it’s a very simple thing pass your counter variable in the key parameter.

@Composable
fun WithLaunchEffect() {

var counter by remember { mutableStateOf(0) }
val context = LocalContext.current


LaunchedEffect(key1 = counter) {
context.showToast("Hello")
}

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { counter++ }) {
Text(text = "Click here")
}
SpacerHeight()
Text(text = "$counter")
}

}

when counter variable changes , then launch effect block call and show the toast message.

Example — 2

Let’s see another example with api calling.

sealed class ApiState<out T> {
data class Success<T>(val data: String) : ApiState<T>()
object Loading : ApiState<Nothing>()
object Empty : ApiState<Nothing>()
}

class MainViewModel : ViewModel() {
private val _apiState: MutableState<ApiState<String>> = mutableStateOf(ApiState.Empty)
var apiState: State<ApiState<String>> = _apiState
private set

fun getApiData() = viewModelScope.launch {
_apiState.value = ApiState.Loading
delay(2000)
_apiState.value = ApiState.Success("Data loaded successfully..")
}

}

@Composable
fun LaunchEffectExample() {
val viewModel: MainViewModel = viewModel()
var call by remember { mutableStateOf(false) }

LaunchedEffect(key1 = call) {
viewModel.getApiData()
}
// never call this function here as whenever recomposition occurs this function will call again
// viewModel.getApiData()

when (val res = viewModel.apiState.value) {
is ApiState.Success -> {
Log.d("main", "Success")
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = res.data, fontSize = 25.sp)
SpacerHeight()
Button(onClick = {
call = !call
}) {
Text(text = "Call Api again !")
}
}
}
ApiState.Loading -> {
Log.d("main", "Loading")
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
ApiState.Empty -> {}
}

}

As you can see in the above code we are doing fake api calling , from the viewModel we are getting success and failed states. As you notice in the composable function , we pass getApiData function in the launch effect . if we don’t do this thing our api will call again and again because as our viewModel.apiState value’s change , our composable function recompose then getApiData function will call again hence this cycle will continue…. and never ends.

Remember what i said above , Never run any non-composable code inside composable function, always use side-effect for that.

And if you want to call this api again , for that we make call mutableState variable that i passed in the key parameter and whenever the value of this variable changes , getApiData function will call.

rememberCoroutineScope()

it is a composable function in jetpack compose that will create a coroutine scope associated with the current composition, where we can call any suspended function inside it.

  • This coroutine scope can be used to launch new coroutines that are automatically cancelled when the composition (composable function) is no longer active.
  • The CoroutineScope object created by rememberCoroutineScope() is a singleton for each composition. This means that if the function is called multiple times within the same composition, it will return the same coroutine scope object.

Let’s understand through an example , Do you know how to create snackbar in jetpack compose? No ? Let’s build this.

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun CoroutineScopeExample() {
val state = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = state,
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(onClick = {
scope.launch {
state.snackbarHostState.showSnackbar("Hey How are you?")
}
}) {
Text(text = "Show Snackbar")
}
}
}

}

In the above code when we click on the button , it will show a snackbar .

scope.launch {
state.snackbarHostState.showSnackbar("Hey How are you?")
}

here .showSnackbar is a suspend function that means we have to call in a coroutine scope that’s why we created a rememberCoroutineScope composable function.

scope.cancel()

If the above coroutine scope is used multiple places and when you write this code scope.cancel() all the coroutine scope will be canceled .

val job =  scope.launch {
state.snackbarHostState.showSnackbar("Hey How are you?")
}
job.cancel()

If you want to cancel the current scope then simply write the above code. Alright !

DisposableEffect

DisposableEffect is a Jetpack Compose function that allows you to create side effects that need to be executed when a Composable is first rendered or disposed.

This function takes two parameters, the first parameter is the side effect that needs to be executed, and the second parameter is a list of dependencies that trigger the side effect to run.

Let’s understand through an example , Basically on button clicks we will send a broadcast event to check whether the device is in airplane mode or not? and see how we can use disposable effect here.

@Composable
fun AirplaneModeScreen() {

var data by remember{ mutableStateOf("No State") }
val context = LocalContext.current
val broadcastReceiver = remember {
object : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val bundle = intent?.getBooleanExtra("state",false) ?: return
data = if(bundle)
"Airplane mode enabled"
else
"Airplane mode disabled"
}

}
}

DisposableEffect(key1 = true){
val intentFilter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
context.applicationContext.registerReceiver(broadcastReceiver,intentFilter)
onDispose {
context.applicationContext.unregisterReceiver(broadcastReceiver)
}
}

Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
Text(text = data)
}

}

As you can see in the above code we are sending a broadcast event to check airplane mode.

But when you focused on the DisposableEffect code it looks strange. So here in the disposable effect function we are registering the broadcast receiver and inside it, it also contain one more function onDispose where we can dispose or unregister the broadcast receiver after no use (when this composable function destroys).

Note :- Whenever you get any case where you have to dispose or unregister something after no use , without thinking about anything just use DisposableEffect . Hope you got it !

SideEffect

SideEffect is a function in jetpack compose which is used to do side work without affecting the ui performance.

Let’s understand through an example

without SideEffect function

@Composable
fun WithOutSideEffectExample() {
val count = remember { mutableStateOf(0) }

Log.d("sideeffect", "Count is ${count.value}")

Button(onClick = { count.value++ }) {
Text(text = "Click here!")
}
}

As you will see the above code , you will notice , when we click on the button count variable increase and recomposition happend and we will see a logcat message. This code works fine but it may impact on the performance.

Remembered ? Never run any non-composable code inside composable function, always use side-effect for that.

with SideEffect function

@Composable
fun WithOutSideEffectExample() {
val count = remember { mutableStateOf(0) }

SideEffect{
Log.d("sideeffect", "Count is ${count.value}")
}

Button(onClick = { count.value++ }) {
Text(text = "Click here!")
}
}

Now the above code looks fine, as we shift the logcat code in the SideEffect block.

derivedStateOf()

derivedStateOf is a function in jetpack compose that is used to compute value based on the value of other states or derivedState .

In other words , derivedStateOf is a function that allows you to create a state that depends on one or more other states.

Let’s see an example to understand in a better way

Example 1

fun DerivedStateExample() {

var counter by remember { mutableStateOf(0) }

val evenOdd by remember {
derivedStateOf {
if (counter % 2 == 0) "even"
else "odd"
}
}

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {

Text(text = "$counter", fontSize = 30.sp)
SpacerHeight()
Text(text = "count is $evenOdd", fontSize = 30.sp)
SpacerHeight()
Button(onClick = {
counter++
}) {
Text(text = "Counter")
}

}

}

As you can see in the above code, we are increasing the counter variable on button clicks. Here counter is a mutable state variable , and based on this counter variable we are finding (computing) odd or even number and showing in the Text.

val oddEvent by remember {
mutableStateOf(
if (counter % 2 == 0)
"even"
else "odd"
)
}

If we try to use mutableStateOf to compute the value , it will never works.

Example — 2

@Composable
fun DerivedStateOfExample() {

var numberOne by remember { mutableStateOf(0) }
var numberTwo by remember { mutableStateOf(0) }
val result by remember { derivedStateOf { numberOne + numberTwo } }

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {

TextField(value = "$numberOne", onValueChange = { numberOne = it.toIntOrNull() ?: 0 })
SpacerHeight()
TextField(value = "$numberTwo", onValueChange = { numberTwo = it.toIntOrNull() ?: 0 })
SpacerHeight()
Text(text = "Result is : $result", fontSize = 30.sp)
}

}

As you can see in the above example we have two TextField and their mutableState variables (numberOne , numberTwo). Now with the help of derivedStateOf we are computing (adding) both the variables and showing in the Text .

That’s all for today my friends , Hope you enjoyed and learnt something new from this article.

--

--

Jayant KumaršŸ‡®šŸ‡³

My name is jayant, i am a youtuber and software Engineer from India šŸ‡®šŸ‡³