In the official google code, there is an example of some buggy Compos code, which is buggy because it assumes a sequential execution model:
@Composable
fun Buggy(
items: List<String>
) {
// Composabe functions can:
// Execute in any order because they are assumed to be "pure" functions
// Run in parallel
// Whether or not you observe this happening is dependant on
// your runtime environment and the compose runtime code
// But *DO NOT* assume that because you have written code that
// appears sequential that it will be executed sequentially...
// For instance, the following code is not correct:
var itemCount = 0
Row {
Column {
items.forEach { item ->
Text("Item: $item")
itemCount++ // Avoid! Side-effect of the column recomposing.
}
}
// Inuitively, we expect that this will render the length of myList
// But in reality, this code may run during (or even before!) the execution Column
// Therefore, relying on side effects such as read/writes to some external state
// And expecting execution to be sequential is a bug waiting to happen
Text("Count: $itemCount")
}
}
However, they do not show an example of how to make this code not buggy.
How should we go about making this code safe for a parallel, non sequential execution model without simple using items.size in the Text function call? I know this is contrived, but I want to understand the "correct" way to do a side effect like shown in the buggy example, that will cause Text to always show the "correct" value.
Should I use LaunchedEffect to update the itemCount like:
@Composable
fun NotBuggy(
items: List<String>
) {
var itemCount = 0
Row {
Column {
items.forEach { item ->
Text("Item: $item")
LaunchedEffect(Unit) {
itemCount++
}
}
}
Text("Count: $itemCount")
}
}
Surely this would still have the same bug?
>Solution :
I believe that purpose of this example is to encourage you to completely avoid side effects. All calculation should happen beforehand and your composable function should receive everything required to render UI.
With count it’s easy to avoid, because we can use items.count() and avoid any manual calculation.
Assume you there is something more complex and you want to display a summary that somehow depend on the list of items. In that case summary should be calculated beforehand and function receives it as an input.
@Composable
fun NotBuggy(
items: List<String>,
summary: String
) {
Row {
Column {
items.forEach { item ->
Text("Item: $item")
}
}
Text(summary)
}
}
LaunchedEffect is useful when you can’t avoid side effect because this side effect is not for calculation, but directly affects UI state. For example, show a snackback or scroll the list.
However, sometimes side-effects are necessary, for example, to trigger a one-off event such as showing a snackbar or navigate to another screen given a certain state condition. These actions should be called from a controlled environment that is aware of the lifecycle of the composable.
You can find valid examples in documentation: https://developer.android.com/jetpack/compose/side-effects#launchedeffect