When mutableStateOf is updated recomposition does not get occurred

722 Views Asked by At

I have basic composable as in the following:

var timer by remember { mutableStateOf(0) }
        Column {
            Text(text = timer.toString())
            Button(onClick = { timer++ }) {
                Text(text = "Add")
            }
        }

When button is clicked, internal state which is timer will be increased by one and since it has been used in the Text component, recomposition will be occurred. But if I have LazyColumn instead of Column as in the following:

var timer by remember { mutableStateOf(0) }
        LazyColumn {
            item(contentType = "timer") {
                Text(text = timer.toString())
            }
            item {
                Button(onClick = { timer++ }) {
                    Text(text = "Add")
                }
            }
        }

Recomposition does not occur, Do not we expect it to be recomposed since the state has been changed?

2

There are 2 best solutions below

0
On BEST ANSWER

When a State is read it triggers recomposition in nearest scope.

First example:

    var timer by remember { mutableStateOf(0) }
    SideEffect {
        Log.i("info", "xxxx")
    }

    LogCompositions("JetpackCompose.app", "Composable scope")

    LazyColumn {

        item(contentType = "timer") {
            LogCompositions("JetpackCompose.app", "Item Text Scope")
            Text(text = timer.toString())
        }
        item {
            LogCompositions("JetpackCompose.app", "Item Button Scope")
            Button(onClick = { timer++ }) {
                LogCompositions("JetpackCompose.app", "Button Scope")
                Text(text = "Add")
            }
        }
    }

When the screen is displayed you can find:

Compositions: Composable scope 0
Compositions: Item Text Scope 0
Compositions: Item Button Scope 0
Compositions: Button Scope 0
I  xxxx

When the Button "Add" is clicked only the item with the Text is recomposed.

Compositions: Item Text Scope 1

It happens because it recompose the scope that are reading the values that changes: Text(text = timer.toString()).

Using a Column instead of a LazyColumn:

    var timer by remember { mutableStateOf(0) }

    SideEffect {
        Log.i("info", "xxxx")
    }
    LogCompositions("JetpackCompose.app", "Composable scope")
    Column {

        LogCompositions("JetpackCompose.app", "Column Scope")
        Text(text = timer.toString())

        Button(onClick = { timer++ }) {
            LogCompositions("JetpackCompose.app", "Button Scope")
            Text(text = "Add")
        }
    }

When the screen is diplayed:

Compositions: Composable scope 0
Compositions: Column Scope 0
Compositions: Button Scope 0
I  xxxx

When the Button "Add" is clicked all the composable is recomposed:

Compositions: Composable scope 1
Compositions: Column Scope 1
I  xxxx

Also in this case it happens because it recompose the scope that are reading the values that changes: Text(text = timer.toString()).
The scope is a function that is not marked with inline and returns Unit. The Column is an inline function and it means that Column doesn't have an own recompose scopes.


To log composition use this composable:

class Ref(var value: Int)

// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
inline fun LogCompositions(tag: String, msg: String) {
    if (BuildConfig.DEBUG) {
        val ref = remember { Ref(0) }
        SideEffect { ref.value++ }
        Log.d(tag, "Compositions: $msg ${ref.value}")
    }
}
1
On

I do not know what do you mean by saying Recomposition does not occur. The text is updating in both peace of code you have in the question