Side-Effects and Effect-Handlers In Jetpack Compose š
--
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.
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 useside-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 ofAny
. - 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 thekey
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 useside-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 useside-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.