Yan Pujante
03/20/2021, 5:03 PMclass Model {
var value1 by mutableStateOf(0)
private set
var value2 = 0
private set
fun onClick1() { value1++ }
fun onClick2() { value2++ }
}
val model = Model()
@Composable
fun TestModel() {
val m = Modifier.padding(10.dp)
Column {
Row { Text(model.value1.toString(), modifier = m); Button(onClick = {model.onClick1()}) { Text("Val1++")} }
Row { Text(model.value2.toString(), modifier = m); Button(onClick = {model.onClick2()}) { Text("Val2++")} }
}
}
which produces the result in the attached screenshot.
I have a few questions:
1) Is it valid to have the state in an external object (like) Model (as opposed to have it directly inside the composable function)? Are there caveats?
2) I don't understand how Compose recognizes that value1 has changed since its underlying representation is a State
but it is exposed as a simple value. How does compose know this? How is value2 different from value1 from a user point of view (IntelliJ tells me that value1 is public final var value1: Int
when using Ctrl-Q)?
3) if I understand correctly (some of) the magic of compose comes from the compiler. Is there a way to "see" this magic in action? Like a flag? (something like compiling .cpp
to .s
to "see" the assembly code)Adam Powell
03/20/2021, 5:06 PM@Composable
fun MyComposable(
state: MyState = remember { MyState() }
Snapshot
API for more info on it.snapshotFlow {}
API is a high-level way to see all of the machinery in action; you can put any code you want into the snapshotFlow
block, and when you .collect
the flow, it will emit a new value returned by that block whenever any of the snapshot state that was read inside the block changes.snapshotFlow
to observe changes to your own snapshot-backed objects outside of any sort of compose-related context. If you were to write
snapshotFlow { model.value1 }
.collect { println("new value: $it" }
you would be able to see the values changingYan Pujante
03/20/2021, 5:14 PMSnapshot.takeSnapshot
and then the call to enter
that runs the expression blockYan Pujante
03/20/2021, 5:18 PMmutableStateOf
call must somehow registers itself somewhere otherwise there is no way that an opaque block of code could be analyzed by snapshotFlow
to determine what is going on...Adam Powell
03/20/2021, 5:21 PMSnapshot.enter
sets up threadlocals for the current snapshot and that forms the communication pathYan Pujante
03/20/2021, 5:23 PMmutableStateOf
or (flow.collectAsState
) is required for the system to workAdam Powell
03/20/2021, 5:24 PMFlow.collectAsState
just uses a LaunchedEffect
to collect the flow and write into a mutableStateOf
object, so it's the same thing)Yan Pujante
03/20/2021, 5:29 PMAdam Powell
03/20/2021, 6:21 PMYan Pujante
03/20/2021, 6:34 PMmutableStateOf
does and the compose/snapshot system. The proof being that if you just declare a "regular" variable it doesn't work. I really thought that the compiler was doing some kind of trick (I have seen in some places reference to the compose compiler, so the compose compiler is doing something) but I guess I was wrong. At the end of the day I understand it's not magic 😉 and I would like to see some paper or presentation, describing how when I write mutableStateOf
somehow it "notifies" (probably not the right word) that whenever a snapshot is taken then this "state" needs to be part of it (ThreadLocal? Global Objects?). There IS a side effect happening when I call mutableStateOf
that reaches OUTSIDE of my class and that is the magic that scares me (scares is probably too strong of a word 😉) . Yes I can look at the source code but a high level explanation/architecture would be super useful (at least to me).Adam Powell
03/20/2021, 6:41 PMget
or set
is called on SnapshotMutableState.value
Chuck Jazdzewski [G]
03/22/2021, 6:40 PMmutableStateOf
. This instance is observable (https://en.wikipedia.org/wiki/Observer_pattern). Whenever the object's value property is written to the snapshot mechanism will inform any listeners that the object was changed (through a global snapshot apply observer). Composition is one such observer (as is layout and draw).
During composition, a snapshot is created with a read observer which will be informed whenever a read of a observable object is performed (even transitively). It records an association with the part of the composition that is being either produced or updated (called a recompose scope) and when it is informed later that the object was changed it will schedule recompose scope to be recomposed.
In the above example, reading value1
in TestModel
will cause TestModel
to recompose whenever value1
changes. Incrementing value1
causes an apply notification to be sent to the composer which will invalidate TestModel
and schedule a recompose to happen in the next choreographer frame. TestModel
is re-invoked by the composer and the new value is updated in the Text
. Changing value2
updates the value but does not schedule a recompose as nothing tells the composer that it has changed.