Effect Handlers in Jetpack Compose: A Complete Guide
Sagar Malhotra
Android dev by day, Creator by night.???? Sharing my passion through videos, blogs and conferences.??
Effect Handlers: as the name suggests, they are used to handle the “side”-effects in Jetpack-Compose. But, what exactly is a side-effect?
According to docs: A side-effect is a change to the state of the app that happens outside the scope of a composable function.
This explains it all, but if you still didn’t get it, let’s take an Example…
class MainActivity : ComponentActivity() {
var i=0 //Outside of compose
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Button(onClick = {}){
i++//SIDE EFFECT
Text(i.toString())
}
}
}
}
}
Here, our variable will increment every time the re-composition occurs, and it will affect the state of our app, but from outside of our COMPOSITION.
Suppose, in any other complex scenario, we perform some heavy operation, we should be able to handle the action/event/operation efficiently and independently of the Compose Lifecycle.
Effect-Handlers are on then rescue?;)
1. LaunchedEffect:
A compose function that allows you to pass 1 or more keys and a code that you want to execute every time our key changes.
fun LaunchedEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
)
Key: Generally a composed state that acts as a trigger for executing the block?
Block: Provides a coroutine scope to run a (suspend)block of code based on a key change.
Example:?
var text = remember{
mutableStateOf("")
}
LaunchedEffect(key1 = text){//A compose state key
delay(100)//Supports suspend function
print("Printing $text")//Everytime text changes
}
Uses-Cases:
2. rememberCoroutineScope:
A composable function that gives reference to a composition-aware coroutine scope.
Example:
@Composable
fun SomeCompose() {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
delay(300)
}
}
) {
Text(text = "")
}
}
As soon as the compose function leaves the composition, all coroutines in the scope will also get canceled.
This is a side-effect because it is only used for events like in the onClick which is targetted to some outside state change.
Uses-cases:
Prevent using it as much as possible. (Personal preferences)
3. rememberUpdatedState:
remember a mutableStateOf and update its value to newValue on each recomposition of the rememberUpdatedState call.
Using an example will give more clarity:
@Composable
fun SomeCompose(
onEvent:()->Unit
) {
LaunchedEffect(Unit){
delay(3000)
onEvent()
}
}
@Composable
fun SomeCompose(
onEvent:()->Unit
) {
val onUpdatedEvent by rememberUpdatedState(newValue = onEvent)
LaunchedEffect(Unit){
delay(3000)
onUpdatedEvent()
}
}
We can use the rememberUpdatedState side-effect to store the latest state of any value, and only the updated value will be passed on to further uses.
4. DisposableEffect:
A compose function that allows you to pass keys and a block of code(similar to the LaunchedEffect) but with extra functionality to write a cleanup block of code.?
Difference from LaunchedEffect: Here, the working of the Disposable effect is exactly similar to LaunchedEffect, it will also relaunch every time the key change. Differences are:
Example:
val context = LocalContext.current
DisposableEffect(context){
register()//Registering a callback is a side-effect and should be done one time
onDispose {
unregister()//Any Callback needs to be unregistered to prevent memory leaks
}
}
领英推荐
5. SideEffect:
Reminding you of the working of compose, it will ONLY update the code/composable which are getting changes/affected with the compose state objects(eg. mutableStateOf). If any non-compose values are changed, then recomposition will not occur.?
So, SideEffect is used to share the compose state with objects not managed by compose.
Let’s dive deeper into the explanation with an example:
var notComposeState = ""https://Outside compose
Column {
var composeState by remember {
mutableStateOf("")
}
val notComposeUpdatedValue = someCompose(composeState,notComposeState)
Text(text = "ComposeStateValue = $composeState")
Button(onClick = { composeState+="*" }) {
Text(text = "Update ComposeState")
}
Text(text = "NotComposeState = $notComposeUpdatedValue")
Button(onClick = { notComposeState+="*" }) {
Text(text = "Update NotComposeState")
}
}
Use-Case: To update any values from non-compose states in case of recompositions(from any other compose state).
On the other hand, it is incorrect to perform an effect before a successful recomposition is guaranteed, which is the case when writing the effect directly in a composable.
6. produceState:
It is a compose function that we can use to convert a non-compose state into a composed state.
I have not personally used this anywhere as it is just similar to generating a flow and then using collectAsState(). Also, this is built on top of other handlers and we can create our own handlers like this.
Example:
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
// Creates a State<T> with Result.Loading as initial value
// If either `url` or `imageRepository` changes, the running producer
// will cancel and will be re-launched with the new inputs.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// In a coroutine, can make suspend calls
val image = imageRepository.load(url)
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
This is a generic function that we can use to pass the URL and load images and return the result as a state. Creating a flow here can be an alternative but this is more of a compose way of doing that.
7. derivedStateOf:
This compose function is used to optimize the performance by helping in reducing the recompositions. We can use it to convert one or multiple state objects into another state.?
In simpler words, one value depends on another state, but the other state changes multiple times resulting in unnecessary recompositions. We can use derivedStateOf to create a cache and avoid recompositions when not required, given conditions.
If you check the internal implementation, its working is very different, you’ll find that it will only respond to the changes(given conditions) and update all the observers while maintaining the cache. If we are not using it then every time the calculations and recompositions occur.
Example:
var counter by remember {
mutableIntStateOf(0)
}
val string by remember {
derivedStateOf {
if(counter>10){
"The high counter values is $counter"
}else{
"Low counter"
}
}
}
Text(text = string)
Button(onClick = { counter++ }) {
Text(text = "Add")
}
The string value is cached till 10 and after that, the counter is changed and only that integer value is getting updated in all observers.
If we don't use this, after every change recomposition occurs.
it acts similarly to the Kotlin Flows distinctUntilChanged() operator.
8. snapshotFlow:
It works exact opposite of collectAsState.
This compose function is used to convert a compose state into a flow that emits values whenever a compose state changes.
Pretty simple, right? Yes, it is!
Example:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
It is generally used to benefit from the power of flow operators.
Resources:?
and My superpowers?;]
I hope you find this knowledge of SideEffect handlers helpful in your code. If yes, please make sure to clap clap clap and hit the follow button, for more such helpful content.?
Computer Engineer | Android Developer | Research Assistant at Duzce University
6 个月Good article and it is very instructive Sagar Malhotra