I'm looking at this working code:
class MainActivity : AppCompatActivity() {
var progress by mutableStateOf(0f)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val composeView = findViewById<ComposeView>(R.id.composeView)
composeView.setContent {
ComposeProgressThingy(progress = progress)
}
thread {
while (progress < 1f) {
Thread.sleep(500) // Simulate work
progress += 0.1f // Update progress
}
}
}
}
What I don't understand is how ComposeProgressThingy
is aware of changes to progress. I never explicitly registered ComposeProgressThingy
as a listener or observer of progress.
It feels like some kind of "black magic" is happening here. Does the lambda passed to setContent{}
somehow analyze its references—at compile time or runtime—and automatically subscribe to any mutableStateOf()
variables it uses?
Can someone explain what mechanism is at work here?
progress
is backed by mutableStateOf(0f)
, which creates a Compose State
object. That is an observable container that holds a value (which you unwrap using Kotlin's by
delegate). This takes care of tracking changes happening to progress
.
Now, reacting to these changes is more complicated. That is handled by Compose's Snapshot system. The link explains a lot of the technical details, but it basically boils down to the Compose compiler generating code to re-execute functions annotated with @Composable
and the Compose runtime to determine when that is necessary (i.e. observing which State variables changed).
So, the "black magic" you talk about is actually just some elaborate code, hidden in the Compose compiler plugin and the Compose libraries you use.
In conclusion, when you use a State
you sign up to observe it for changes, and by annotating a function with @Composable
you kind of register an observer to listen to changes of any State that it uses to re-execute the function. You just don't need to do any of the dirty work yourself.