英文:
Android compose - Side effects confusion
问题
In the official Google code, there is an example of some buggy Compos code that assumes a sequential execution model. They do not show an example of how to make this code not buggy.
To make this code safe for parallel, non-sequential execution without simply using items.size
in the Text
function call, you can indeed use LaunchedEffect
to update itemCount
. However, as you mentioned, the provided code still has the same bug. To fix it, you can use a mutableStateOf
to ensure that the itemCount
value is updated correctly and the Text
function always shows the correct value. Here's an improved version:
@Composable
fun NotBuggy(
items: List<String>
) {
var itemCount by remember { mutableStateOf(0) }
Row {
Column {
items.forEach { item ->
Text("Item: $item")
LaunchedEffect(Unit) {
itemCount++
}
}
}
Text("Count: $itemCount")
}
}
By using mutableStateOf
, you ensure that changes to itemCount
trigger recomposition, and the Text
function will always display the up-to-date count.
英文:
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?
答案1
得分: 1
I believe that the purpose of this example is to encourage you to completely avoid side effects. All calculations 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 there is something more complex, and you want to display a summary that somehow depends on the list of items. In that case, the summary should be calculated beforehand, and the 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 effects because this side effect is not for calculation but directly affects UI state. For example, showing a snackbar or scrolling the list.
However, sometimes side-effects are necessary, for example, to trigger a one-off event such as showing a snackbar or navigating 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 the documentation: https://developer.android.com/jetpack/compose/side-effects#launchedeffect
英文:
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论