CLOVIS
10/28/2023, 7:19 PMState
object changes, the runtime is notified that a recomposition may be needed,
• blocks which read the state are recomposed.
This would mean that changes to state happening in non-composable blocks are "invisible" to Compose: it knows the state has changed, but it doesn't know where. Compose cannot recompose non-composable blocks of code, anyway.
To illustrate this, consider a simple non-composable DSL that lets the user create elements, which are later displayed.
class Elements {
val elements = ArrayList<String>()
fun element(title: String) {
elements += title
}
}
@Composable
fun Foo(block: Elements.() -> Unit) {
println("Foo recomposes")
val elements = remember(block) { Elements().apply(block) }.elements
for (element in elements) {
Text("Element: $element")
}
}
At first, this looks normal:
@Composable
fun Main() {
println("Main recomposes")
Foo {
println("Foo's block is executed")
element("First element")
element("Second element")
}
}
Both items are displayed, and the output is as we expect:
Main recomposes
Foo recomposes
Foo's block is executed
Of course, no state changed, so whether or not Compose detects changes is moot. Let's change the callsite:
@Composable
fun Main() {
println("Main recomposes")
val elements = remember { mutableStateListOf<String>() }
TextButton({ elements += List(Random.nextInt(1, 10)) { "Value: ${Random.nextUInt()}" } }) {
Text("Generate new elements")
}
Foo {
println("Foo's block is executed")
for (element in elements) {
element(element)
}
}
}
When pressing the button, Main recomposes
is printed again, but nothing else happens. In my mental model, this is easily explained:
• elements
has changed, so Compose knows it must recompose Main
• The lambda passed to Foo
captures elements
. Compose compares it with the lambda passed during the previous composition; elements
is referentially-equal to the previous value (it's a mutable list, the reference doesn't change).
• Compose decides that Foo
doesn't need recomposing, since its inputs are the same.
So far, so good. However, this is not how LazyColumn
behaves:
@Composable
fun Main() {
val elements = remember { mutableStateListOf<String>() }
TextButton({ elements += List(Random.nextInt(1, 10)) { "Value: ${Random.nextUInt()}" } }) {
Text("Generate new elements")
}
LazyColumn {
items(elements) {
Text(it)
}
}
}
Now, pressing the button does update the view.
How does LazyColumn
manage to recompose, here? I can't figure out the difference:
• both are @Composable
• both have a single lambda parameter that is not @Composable
• both only read the State value in the non-recomposable lambda
• both capture a reference which doesn't change, so an equality check on the lambda cannot consider it to have changedhfhbd
10/28/2023, 7:45 PMhfhbd
10/28/2023, 7:47 PMCLOVIS
10/28/2023, 7:49 PMWhy not elements = remember ( .elements }
It doesn't make a difference,
block
never changes so the remember won't ever be triggered anyway
It will work if Elements.elements is MutableStateList
Try it, it won't.
Foo
doesn't get recomposed, so the lambda isn't called. What the lambda does is irrelevant if it doesn't run in the first place.shikasd
10/29/2023, 1:17 AMderivedStateOf
, which should be close to what you need to get updatesCLOVIS
10/29/2023, 10:43 AMAs block changes, remember is not executed again, so you get the old elements list instead.I thought remember would recompute its result when its dependencies change? That's why I wrote
remember(block) { … }
.
You can wrap creating Elements object withI doesn't change anything. I think the problem happens before that, though, because the println at the start of the, which should be close to what you need to get updatesderivedStateOf
Foo
never prints after the first composition, so I don't think Compose even reaches the remember
/`derivedStateOf` .CLOVIS
10/29/2023, 10:55 AMmutableStateListOf
…
@Composable
fun Main() {
val elements = remember { mutableStateListOf<String>() }
…
}
…by a mutableStateOf<List>
:
@Composable
fun Main() {
val elements = remember { mutableStateOf(emptyList<String>()) }
…
}
then Foo
is recomposed, and everything updates correctly (presumably because Foo
's lambda captures elements
, which gets a new identity once we assign it again).
This is even weirder, though, because LazyColumn
does get recomposed by a mutableStateListOf
.CLOVIS
10/29/2023, 11:00 AM@Stable
annotation:
> • All Function types (lambdas)
This explains why Foo
doesn't recompose. However, by that logic, LazyColumn
cannot recompose either.
I'm starting to wonder if the compiler has a case specifically for LazyColumn
and LazyRow
? All the rules seem to imply that it really shouldn't behave like this.shikasd
10/29/2023, 2:20 PMLazyColumn
just uses the derived state to trigger updates. You just need to do val elements by remember(block) { derivedStateOf { Elements().apply(block).elements } }
shikasd
10/29/2023, 2:23 PMI thought remember would recompute its result when its dependencies change? That's why I wrote remember(block) { … } .
My bad, should have clarified this better. The state is read inside the block and that changes, so you want to subscribe to updates somehow. Block instance is the same on recomposition because you get memoized lambda (I think it is memoized keyed with state instance)
shikasd
10/29/2023, 2:24 PMAlbert Chang
10/29/2023, 2:26 PMderivedStateOf
does work.
Also,
changes to state happening in non-composable blocks are "invisible" to Compose: it knows the state has changed, but it doesn't know where. Compose cannot recompose non-composable blocks of code, anyway.This is not correct. As long as the block containing state reads is invoked in a scope that is observed by compose, the state reads will be recorded. There are three kinds of scopes that are observed in compose: composition scope, layout scope and draw scope, of which layout scope and draw scope are both non-composable scope.
Box(
modifier = Modifier
.layout { measurable, constraints ->
// Layout scope
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
.drawBehind {
// Draw scope
}
) {
// Composition scope
}
Changes of the states read (directly or indirectly) in these scopes will cause the rerun of the corresponding phase (recomposition / relayout / redraw).shikasd
10/29/2023, 2:30 PMshikasd
10/29/2023, 2:33 PMZach Klippenstein (he/him) [MOD]
10/29/2023, 5:11 PMCLOVIS
10/29/2023, 9:08 PMYou just need to do val elements by remember(block) { derivedStateOf { Elements().apply(block).elements } }
Ah, sorry. I thought you meant using derivedStateOf by itself, without remember. I'll try that tomorrow.
CLOVIS
10/31/2023, 7:03 PMremember(block) { derivedStateOf(…) }
does not work when the call-site uses MutableStateList
, but it does work when the callsite uses State<List<>>
. Are you sure this isn't caused by the lambda being stable because it captures no mutable references?shikasd
10/31/2023, 9:08 PMCLOVIS
11/02/2023, 9:42 PM@Composable
fun Foo(block: () -> Unit) {
println("Foo recomposes")
block()
}
@Composable
fun Main() {
val elements = remember { mutableStateListOf<Int>() }
Foo {
for (element in elements)
println("Element: " + element)
}
Button(onClick = { elements.add(Random.nextInt()) }) {
Text("Generate elements")
}
}
This does print more and more elements the more times the button is pressed. I don't understand what the difference between this and my code is.
Even if I call the lambda directly as the first line in my composable, it doesn't recompose when the list changes.shikasd
11/02/2023, 11:52 PMderivedStateOf
) I suggested earlier and it works as expected:
class Elements {
val elements = ArrayList<String>()
fun element(title: String) {
elements += title
}
}
@Composable
fun Foo(block: Elements.() -> Unit) {
println("Foo recomposes")
val elements by remember(block) {
derivedStateOf { Elements().apply(block).elements }
}
for (element in elements) {
Text("Element: $element")
}
}
@Composable
fun Main() {
println("Main recomposes")
val elements = remember { mutableStateListOf<String>() }
TextButton({ elements += List(Random.nextInt(1, 10)) { "Value: ${Random.nextUInt()}" } }) {
Text("Generate new elements")
}
Foo {
println("Foo's block is executed")
for (element in elements) {
element(element)
}
}
}
Albert Chang
11/03/2023, 7:55 AMremember
, which is not observed.