Side Effects in Jetpack Compose : A Beginner’s Guide
Understanding the Role of Side Effect Handlers
Jetpack Compose is powerful tool that enables developers to build UIs in a declarative way. By focusing on what your UI should look like, it simplies the android development process.
However, when buiding an application, there are moments when you need to perform tasks that go beyond just rending UI components on the screen. For example, your app needs to retrieve data from a server, store information in a local database, or respond to user interactions by initiating network requests. These require interactions with external systems, data manipulation, and responses to asynchronous events. This is where the concept of side effect comes into the picture.
What is Side Effect?
Side effects refers to any action that happens beyond the scope of the function. Using side effects in Jetpack Compose involves handling tasks that occur outside of the normal UI rendering process.
For example, opening a new screen when the user taps on a button is a side effect. This is because the new screen is opened by the operating system, outside of the scope of the composable function that displays the button.
When to use Side Effect?
- Making network requests: If your app needs to retrieve data from a server, you can use a side effect to make the network request. This will ensure that the network request is only made once, even if the composable function is recomposed multiple times.
- Updating shared state: If your app has shared state that needs to be updated, you can use a side effect to update the state. This will ensure that the state is updated in a consistent way, even if the composable function is recomposed multiple times.
- Launching coroutines: If your app needs to launch a coroutine, you can use a side effect to do so. This will ensure that the coroutine is launched in a consistent way, even if the composable function is recomposed multiple times.
- Handling callbacks: If your app needs to handle callbacks, you can use a side effect to do so. This will ensure that the callbacks are handled in a consistent way, even if the composable function is recomposed multiple times.
How to use Side Effects?
Android recommends that composable should be side effects free. Side effects are required while bulding an application so we have to run it in controlled environment and for that Side Effect Handlers are used. Let’s understand each handler one by one.
SideEffect :
The SideEffect
composable takes a block of code as its parameter. This block of code will be executed whenever the composable is composed, even if the state of the composable does not change.
Here is the example of sideEffect
composable to make a network request:
@Composable
fun MyComposable(state: MyState) {
SideEffect {
// Make a network request to get the latest data.
val data = fetchData()
// Update the state with the latest data.
state.data = data
}
// Display the UI that depends on the state.
}
In this example, the SideEffect
composable makes a network request to get the latest data. The data is then stored in the state of the composable. The UI that depends on the state will be updated whenever the data changes.
SideEffect
composable only executes once and so fetchData()
function is only called once. Whenever the state is changed, MyComposable
will be recomposed, but the SideEffect
composable will not be executed again because it is idempotent. This ensures that the network request is only made once, even if the state of the composable changes multiple times. This increases your app performance.
Idempotent - calling the same function n times, providing the same input, should result in same output.
The
SideEffect
composable is idempotent, which means that it will always produce the same results, even if it is executed multiple times.
LaunchedEffect :
The LaunchedEffect
composable takes a block of code as its parameter, as well as a key. The block of code will only be executed once, when the composable is first composed. If the composable is recomposed with the same key, the block of code will not be executed again.
Here is another example of how to use the LaunchedEffect
composable to launch a coroutine:
fun MyComposable(state: MyState) {
LaunchedEffect(key1 = "myKey") {
// Launch a coroutine to do some work.
val result = doWork()
// Update the state with the result of the coroutine.
state.result = result
}
// Display the UI that depends on the state.
}
In this example, the LaunchedEffect
composable launches a coroutine to do some work. This ensures that the coroutine is only executed once, even if the composable is recomposed multiple times.
If the key1
parameter changes, the LaunchedEffect
composable will be executed again, and the doWork()
coroutine will be restarted.
LaunchedEffect
composable is not idempotent. This means that it can produce different results if it is executed multiple times.
In the example above, the doWork()
coroutine may produce different results if it is executed multiple times. This is because the coroutine may access different data from the network or from the database.
SideEffect VS LaunchedEffect :
You should use the SideEffect
composable if you need to perform a side effect that is idempotent and that should only be executed once, even if the composable is recomposed multiple times. You should use the LaunchedEffect
composable if you need to perform a side effect that is not idempotent or that needs to be executed again if the key
parameter changes.
rememberCoroutineScope :
LaunchedEffect
is not scoped to the composable function. This means that the coroutine scope will not be cancelled when the composable function is removed from the composition hierarchy. This could lead to a memory leak, as the coroutine would continue to run in the background, even though it is no longer needed. Therefore, rememberCoroutineScope
composable is needed because it provides a way to create a coroutine scope that is scoped to the composable function.
Here is an example of how we can avoid memory leak with rememberCoroutineScope
composable:
fun MyComposable(state: MyState) {
val scope = rememberCoroutineScope()
scope.launch {
// Fetch data from the network.
val data = fetchData()
// Update the state with the data.
state.data = data
}
// Display the UI that depends on the state.
}
In this example, the scope
variable is created using the rememberCoroutineScope
composable. This ensures that the coroutine scope will be cancelled when the MyComposable
composable function is removed from the composition hierarchy. This prevents the memory leak from occurring.
rememberUpdatedState :
The LaunchedEffect
composable is called during the initial composition only or whenever the key is changed. Whenever the key is changed, it cancels the current execution and restarts the LaunchedEffect
composable. However, there might comes a need to change the state of the value inside the LaunchedEffect during recomposition. At that time RememberUpdatedState
comes into the picture.
The RememberUpdatedState
composable is used to create a state that is updated whenever the composable is recomposed. This means that you can use it to update the state of the value inside a LaunchedEffect
composable during recomposition.
Here is an example of how to use the RememberUpdatedState
composable:
fun MyComposable(value:Int) {
val state = rememberUpdatedState(newValue = value)
LaunchedEffect(key1 = "myKey") {
//Long-running task
delay(5000)
Log.d("Inside LaunchedEffect", "Current value: ${state.value}")
}
Text(text = value.toString())
}
Here There is some long task in launchedEffect. During recomposition, I don’t want that this task exectus again but I want uptodate value in the text, so rememebr
is used. Here Log will always display up-to-date value even if it is in LaunchedEffect.
DisposableEffect :
DisposableEffect
is a composable that executes a side effect and then disposes of it when it is no longer needed. This is useful for side effects that consume resources, such as making network requests or opening files. It is used to ensure that resources are properly cleaned up when they are no longer needed. This helps to prevent memory leaks and other performance problems.
DisposableEffect
takes key and block of code. Key is used to uniquely identify the DisposableEffect. Block is a lambda that defines the side effect that will be executed. The lambda will be executed when the DisposableEffect is first composed. onDispose
block is used inside this and it will be executed when the DisposableEffect is no longer needed.
Here is an example of how DisposableEffect
can be used:
fun MyComposable(value: Int) {
//DisposableEffect that will make a network request.
DisposableEffect(key1 = "myKey") {
// Make a network request.
val response = fetchData()
// Do something with the response.
Log.d("MyComposable", response.toString())
onDispose{
// Close any open resources.
// Cancel any pending network requests.
}
}
}
In this example, the DisposableEffect
composable is used to make a network request. The network request is made when the MyComposable
composable is first composed. The response from the network request is then logged to the console. onDispose
block is executed when the composable is no longer needed. In this block, you will specify any cleanup that is necessary to release resources or any pending request that needs to be cancelled.
ProduceState :
produceState
is a simple concept that combines the functionality of remember
and LaunchedEffect
. It creates a state variable and then updates the value of the state variable based on the results of a side effect. This can be used to ensure that your composables are always displaying the latest data.
The produce state composable takes initialValue and producer. InitialValue is the initial value of the state variable. Producer is a lambda that defines the side effect that will be executed to update the value of the state variable. This lambda will be executed when the produce state composable is first composed, and it will be executed again whenever the value of the state variable changes.
Here is the example of produceState
:
//Composable with remember and LaunchedEffect
fun MyComposable(value: Int) {
val state = remember { mutableStateOf("") }
// Update the state variable with the user's name.
LaunchedEffect(key1 = "keyName") {
value = fetchUserName()
}
// Display the user's name.
Text(text = state.value)
}
//Composable with produceState
fun MyComposableWithProduceState(value: Int) {
val state = produceState(initialValue = "") {
// Update the state variable with the user's name.
value = fetchUserName()
}
// Display the user's name.
Text(text = state.value)
}
In this example, MyComposable
and MyComposableWithProduceState
will give the same results. produceState
composable is used to create a state variable that will store the latest value of the user's name. The produceState
composable will first fetch the user's name from a database. The user's name will then be stored in the state variable. The produceState
composable will also be executed whenever the user's name changes in the database.
derivedStateOf :
Whenever we want to get new state value from existing state values, derivedStateOf
is used. The derivedStateOf
composable is used to create new state variables that are computed from the values of other state variables. This can be useful for a variety of tasks, such as formatting data, calculating derived values, and caching data.
The derivedStateOf
composable takes block that is lambda which defines the calculation that will be used to derive the new state variable. The lambda will be executed whenever the value of the state variable changes.
Here is the example:
fun MyComposable(value: Int) {
// Create a state variable that stores the user's name.
val nameState = remember { mutableStateOf("") }
// Create a new state variable that stores the user's name in uppercase.
val nameInUpperCaseState = remember { derivedStateOf {
// Convert the user's name to uppercase.
return it.value.uppercase()
}
}
// Display the user's name in uppercase.
Text(text = nameInUpperCaseState.value)
}
In this example, the derivedStateOf
composable is used to create a new state variable that stores the user's name in uppercase. The new state variable is derived from the value of the nameState
state variable. The getter
lambda converts the value of the nameState
state variable to uppercase and returns the result.
Conclusion
To conclude, let’s summarize the key points about side effect handlers.
- SideEffect : Control side effects that are idempotent and executed during composition.
- LaunchedEffect : Executes only once during the initial composition, or when the specified keys change.
- rememberCoroutineScope : Coroutine scope that is scoped to the composable function.
- rememberUpdatedState : Maintain a stable reference to a value, ensuring accurate updates during recomposition.
- DisposableEffect : Handle the side effect that required clean up.
- ProduceState : Combine state and side effects, ensuring that UI display latest data.
- DerivedStateOf : Derived the state value from another state values.
Side Effects are helpful tools when used in a controlled environment. However, relying too much on side effects can make your app less predictable and cause performance issues. If possible, it’s recommended to avoid using side effects.