Side-effects in jetpack compose
đ This Side Aditya,
In my previous article i discussed about basic advanced jetpack compose layout for dynamic UI, if you havenât seen it you can find here Advanced Jetpack Compose Layout For Dynamic UI.
Now itâs time to dive into side-effects, generally we android dev have habit of performing operation without thinking of itâs effect on the performance. but know if you are working on jetpack compose you have to follow jetpack compose guidelines, and in this artical will discuss about side-effects.
What Is Side-Effects :
Whenever we tries to change the state of the composable function it should be done inside an controlled environment, because composable function has property like recomposition any time, recomposition in any order. if we not properly control how and when our state state changes it leads to unpredictable output.
but now you may have a question like how should we control this ?
answer is you should use Effect APIâs so that side effect should run in predictable manner. letâs see them one by one.
1. LaunchEffect :
LaunchEffect is basically composable function which contains coroutine block in it. it is attached with the lifecycle of composable function and it take âkeyâ if key value change it will remove current coroutine and relaunch it self by launching new coroutine and LaunchEffect also get cancel is composition leaves . so whenever you want to execute any suspend function or you want to perform any database or networking or any other operationâs you can opt this. let see quick example of this
// Allow the pulse rate to be configured, so it can be speed up if the user is running
// out of time
var pulseRateMs by remember { mutableStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
while (isActive) {
delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
alpha.animateTo(0f)
alpha.animateTo(1f)
}
In above example we are trying to show animation âpulseRateMsâ is acting as a key whenever its value change it trigger LaunchEffect to run again inside the body we have a while loop which has âisActiveâ you can consider it as âtrueâ always inside while we have âdelay (suspendable function)â with has âpulseRateMsâ as a value means it will hold the coroutine some âpulseRateMsâ time and then execute the animation.
so basically whenever you want to perform any task either it could be database , network or anything else operation you can consider LaunchEffect.
2. DisposableEffect :
This is same as LaunchEffect , it also take key and whenever key change it remove the coroutine and create new one and use it and whenever Composable remove it cancel the DisposableEffect, but it has one feature âcleanupâ basically when the coroutine remove it will cleanup the resource which he occupied and make it available for otherâs . it does it with the help of observer called âonDisposeâ. let see example
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
/* Home screen content */
}
In above example , we are trying to send the analytics event to server consider it. âlifecycleOwnerâ will help us to know the state of the HomeScreen like is it in START state or STOP state. based on itâs state we will send the analytics data to server, here we are attaching an âobserverâ to âlifecycleOwnerâ and in âonDisposeâ we will clear it . so you might have a question when will this âonDisposeâ will get call , it will get call in 2 case first when the âlifecycleOwnerâ change and when this âHomeScreenâ composable will no longer visible to user .
3. rememberCoroutineScope :
if you think that LaunchEffect and DispposableEffect is only way to execute the coroutineScope then you have not seen this rememberCoroutineScope. where LaunchEffect and DispposableEffect are it self a composable function thatâs why you canât use them outside composable function, rememberCoroutineScope is not a composable function yet is it strictly attach with composable function so that whenever composable leave the composition then this rememberCoroutineScope will also get canceled. so you might ask when to user it so whenever to want to perform the task on any activity done by user like button click , scroll etc.. then you can use this inside there callback function directly. let see an example
@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
The above example is self explanatory , the rememberCoroutineScope âscopeâ is attach to âMoviesScreenâ lifecycle basically so that whenever MoviesScreenâ is not available to user it will remove all the coroutine launch by ârememberCoroutineScopeâ.
4. rememberUpdatedState :
LaunchEffect basically relaunch whenever the key change but it is not necessary that every time you want to relaunch the effect if something change sometime you might require an feature where you want to change the state but that change shouldnât relaunch the side effect again in that case you can ârememberUpdatedStateâ. basically ârememberUpdatedStateâ create a refference to value which can directly updated or capture without relaunching the effect. let see the example
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
/* Landing screen content */
}
in above example âonTimeoutâ is the value and we are referencing it with currentOnTimeout using ârememberUpdatedStateâ so for example is you pass âfunctionOne()â as an argument to âLandingScreenâ composable and âSplashWaitTimeMillisâ is 5 secondâs and after 2 second you changed âLandingScreenâ parameter with âfunctionOne()â with âfunctionTwo()â then âfunctionTwo()â will get call in LaunchedEffect without relaunching of LaunchedEffect again.
5. produceState :
the produceState launch the coroutine and return the coroutine with a state basically it manages to production on state value for there consumer, in simple language it contains coroutine and inside the coroutine you will create a value and then value will be return to consumer. it has a very important use case with a reactive programming library like Flow, LiveData, Rxjava, and you can also use this for other cases like whenever any other state change we want to update state which create with produceState. let see 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)
}
}
}
In above example if you see we are making api request with âimageRepositoryâ and then in the end we are returning the result so that we can consume itâs image response wherever we are using it.
6. derivedStateOf :
derivedStateOf help use to create a state from another state, so when to use it a when you have a input which leads to recompose of your UI more often then it actually required that time you can use the derivedStateOf.
letâs take example we have a floating action button on right bottom and i want to show it when user not seeing the 1st item on the list that time you can use derivedStateOf with the condition like what is the current top fully viisble item > 0 . let see the code.
@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
Box {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
}
If you see above example we have full screen list âlistStateâ and we have a âshowButtonâ state with âderivedStateOfâ which has condition with the state âlistStateâ if âfirstVisibleItemIndexâ is more than 0 then we make âshowButtonâ true and it will show button .
7. snapshotFlow :
you can use snapshotFlow to convert compose state to cold flow, it works same as kotlin cold flow here you also can emit and collect the value in collector and this is cold flow so if there is not collector it wonât emit any value the behaviour of emitting value is same as âFlow.distinctUntilChanged()â means if there is no change in value it will not emit the same data once it is emittied (we will more discuess this in coroutine article).
let see example
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
the above example is same as previous scroll example here if you see we are using âsnapshotFlowâ with âlistState.firstVisibleItemIndexâ as the index of âlistState.firstVisibleItemIndexâ change it with relaunch the LaunchEffect and emit and collect the value in â.collect{}â callback.
Side-effect is really interesting topic in jetpack compose and if you readed this article till here you might have more interest in knowing this in more deeply. so if you want to read more about this you caan follow an offical documentation below created by google.
Thank you,
Next article will be on State Of Jetpack Compose. Stay Tune.