Jetpack Compose Effect Handlers

Jetpack Compose Effect Handlers

Introduction

When developers start learning Jetpack compose and starts building projects, they generally do not consider Side effects happening on their composables. They simply make it very clean and simple and keep on creating stunning UIs. But its very important to keep the side effects in mind while writing composables. These can avoid unnecessary resource usage, memory leaks, enhancing performance, etc.

So without wasting time anymore lets understand these terms step by step

What is Side Effect

A side-effect is a change of the state in the app that happens outside the scope of composable functions. Sometimes we ignore them but there happens some unwanted recompositions or executions that we don't want as they affect the performance of the application. So we target out codebase to be side-effect free.

But sometimes, in Compose, we need to perform certain actions that have side effects, such as displaying a snackbar or navigating to another screen based on a specific condition. To handle such cases, Jetpack Compose provides various side-effect APIs that allow us to execute these actions from a controlled environment that is aware of the composable's lifecycle.

Types of Side Effects:

  • LaunchedEffect

  • rememberCoroutineScope()

  • rememberUpdatedState()

  • DisposableEffect

  • SideEffect

  • ProduceState

  • DerivedStateOf

  • snapShotFlow()

LaunchedEffect

In Jetpack Compose, LaunchedEffect is a side-effect API that allows you to perform asynchronous operations within a composable function. It is particularly useful when you need to trigger an effect, such as making a network request or performing a time-consuming computation, and update the UI based on the result.

example:

@Composable
fun MyComposableExample() {
    val dataState by remember { mutableStateOf(DataState.Loading) }
    LaunchedEffect(Unit) {
        delay(2000)
        dataState = DataState.Success
    }
    when (dataState) {
        DataState.Loading -> Text(text = "Loading...")
        DataState.Success -> Text(text = "Data Loaded!")
    }
}

Now, as you can see we have Unit inside LaunchedEffect block, so in place of that we can use key to ensure when it should recompose, like if we give dataStore as key, it will recompose whenever the dataStore variable will change. If you want to recompose it only once then pass LaunchedEffect(key1 = true)

rememberCoroutineScope()

This is used when we want to use a CoroutineScope inside a composable.

example:

@Composable
fun MyComposableExample() {
    val scope = rememberCoroutineScope()
    val countDown by remember { mutableStateOf(1000) }

    Button(
        onClick = {
            scope.launch {
                delay(1000)
                countDown--
            }
        }
    ) {
        Text(text = countDown.toString())
    }
}

Note: This scope can only be used inside a callback of a composable

rememberUpdatedState()

In simple words, the rememberUpdatedState() function in Jetpack Compose is used to remember a value that can be updated during the composition of a composable. It's useful when you have long-lived operations or expensive calculations that you don't want to recreate every time the composable is recomposed.

To use rememberUpdatedState(), you provide a lambda function that calculates the value you want to remember. The value returned by the lambda will be stored and updated whenever the composable is recomposed.

example:

@composable
fun MyComposableExample(onTimeout: () -> Unit) {
  val timer = rememberUpdatedState(0)

  LaunchedEffect(timer) {
    delay(5000)
    onTimeout()
  }
  Text(text = "The timer is at ${timer.value}")
}

DisposableEffect

Disposable effect refers to a mechanism provided by Compose to handle the lifecycle and cleanup of resources that are acquired during the execution of a composable function. It ensures that resources are properly released when they are no longer needed, preventing leaks and improving memory management.

example:

@Composable
fun MyComposableExample() {
    DisposableEffect(key1 = "resourceKey") {
        //use the resource
        val resource = acquireResource()

       //remove block
        onDispose {
            //remove the resources after used
            releaseResource(resource)
        }
    }

    // ...
}

SideEffect

The SideEffect composable in Jetpack Compose is used to perform side-effects, which are changes to the state of the app that happen outside the scope of a composable function. For example, you might use a SideEffect to update a data store, or to make an HTTP request based on state.

example:

@Composable
fun MyComposableExample() {
 val count by remember{mutableStateOf(0)}
  SideEffect {
    // This block will run whenever the count state changes.
    updateSharedPreference(count.value)
  }
 Button(onClick={count++}){
      Text(text = "The count is ${count}") 
   } 
}

ProduceState

The produceState function in Jetpack Compose is used to create a state object that is backed by a coroutine. This means that the state object can be updated from within the coroutine, and the changes will be reflected in the composable that uses the state object.

The produceState function takes a lambda as its argument, which is used to initialize the state object and to define the coroutine that updates the state object. The lambda must return a State object.

This is an alternative for Flows as it also returns the state using collectAsState(initialValue = value) . Using this makes the code more cleaner.

example:

@Composable
fun MyComposableExample(countUpTo:Int):State<Int> {
  return produceState(initialValue = 0) {
  // also this is a coroutine scope
    while (value<=countUpTo) {
      delay(1000)
      value ++
    }
  }
}

DerivedStateOf

The derivedStateOf function in Jetpack Compose is used to create a new state object that is derived from an existing state object.

In Simple words:

Assume there is a complex calculation which depends on the state i.e. whenever the state changes the calculation will happen again or when that value will be used by another composable, it will calculate again and again. To avoid this we cache the calculated data and until it changes, we use the cached value to avoid unnecessary evaluations.

example:

@Composable
fun MyComposeExample() {
 val highPriorityTasks = derivedStateOf(count) {
    // It only updates the state object when the count state object changes.
    count.value.filter { it > 10 }
  }
  Text(text = "The high priority tasks are ${highPriorityTasks.value}")
}

snapShotFlow()

The snapshotFlow() function in Jetpack Compose is used to convert a State object into a Flow. This means that the Flow will emit the current value of the State object whenever the State object changes.

The snapshotFlow() function takes a lambda as its argument, which is used to extract the State object from the composable. The lambda must return a State object.

example:

@Composable
fun MyComposableExample(count: MutableStateOf<Int>) {
  val countFlow = snapshotFlow { count }

  LaunchedEffect(key1 = countFlow) {
    // will be executed whenever the count state changes
    // prints the current value of the count state
    println(countFlow.value)
  }
  Text(text = "The count is ${count.value}")
}

That was all about the Effect Handlers in Jetpack compose, hope this helps to get an idea and basic understanding on this. Now whenever you will code in compose, you will definitely find the use cases.

Thanks for reading ๐Ÿš€.

Did you find this article valuable?

Support Subhadip Das by becoming a sponsor. Any amount is appreciated!

ย