I have the following code: ```class Model { v...
# compose-desktop
y
I have the following code:
Copy code
class 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)
a
1. Is it valid? Yes, and often encouraged. It's often good practice to "hoist" the remembered state of a composable function into an object that looks a lot like this, and then pass it as a parameter with a default value, so that callers that want to manipulate it can do so, and callers that don't care don't need to see it. The default state parameter style generally looks like this:
Copy code
@Composable
fun MyComposable(
  state: MyState = remember { MyState() }
2. This is the "snapshots" system that is included with the compose runtime. You can take a snapshot and track reads and writes to any snapshot-backed records. Both compose-runtime and compose-ui do this to track invalidations of composition, layout, and drawing in the same way. See the
Snapshot
API for more info on it.
3. No, snapshots do not involve any compiler magic. It's all just run-time kotlin code.
The
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.
You can also use
snapshotFlow
to observe changes to your own snapshot-backed objects outside of any sort of compose-related context. If you were to write
Copy code
snapshotFlow { model.value1 }
  .collect { println("new value: $it" }
you would be able to see the values changing
y
I guess I don't understand how snapshotFlow knows that value1 is being accessed. where is the "magic"?
the magic is
Snapshot.takeSnapshot
and then the call to
enter
that runs the expression block
y
I guess the
mutableStateOf
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...
a
more or less.
Snapshot.enter
sets up threadlocals for the current snapshot and that forms the communication path
in effect, the current thread local snapshot is like a process-global redux store of sorts, consisting of all snapshot state objects in the process, keeping them all internally consistent with each other
y
ok. I will have to dig deeper if I want to go down this route. But clearly calling
mutableStateOf
or (
flow.collectAsState
) is required for the system to work
a
snapshot observation is the primary/preferred mechanism of invalidating compose-runtime/compose-ui componentry, yes
(
Flow.collectAsState
just uses a
LaunchedEffect
to collect the flow and write into a
mutableStateOf
object, so it's the same thing)
y
thanks for the help
👍 1
it's easy to find tutorial/examples that tells you to do this, but it doesn't explain why...
a
yeah. What kind of docs on this would you have wanted to find in some official materials? Pointers to the source as I linked above? something else?
we'd like to make sure we don't scare folks off who would be overwhelmed by the underlying mechanics for fear that they have to understand all of it in order to use the system at all
y
Even with your explanations, it is still very vague in my mind how the "magic" works. There is clearly an interaction between what
mutableStateOf
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).
👍 1
3
a
the side effect happens when
get
or
set
is called on
SnapshotMutableState.value
c
It is not reaching outside your class so much as it is reaching outside the instance created by
mutableStateOf
. 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.