julioromano
01/21/2021, 12:42 PMdata class State(
val firstCounter: Int = 0,
val secondCounter: Int = 0
)
@Composable
fun MyScreen(
stateFlow: StateFlow<State>,
onClick1: () -> Unit,
onClick2: () -> Unit,
) {
val state by stateFlow.collectAsState()
Column {
Button(onClick = onClick1) {
Text(text = "One ${state.firstCounter}")
}
Button(onClick = onClick2) {
Text(text = "Two ${state.secondCounter}")
}
}
}
If only one field of the state
object changes will it trigger recomposition of only one (the one that’s reading that field) or both buttons ?
Can someone point to some docs or examples to learn a bit more how recomposition actually works?
🙏KamilH
01/21/2021, 1:02 PMMyButton
like that:
@Composable
fun MyButton(text: String, number: Int, onClick: () -> Unit) {
onCommit(callback = {
Log.i("MyButton", "committed: $text, $number")
})
Button(onClick = { onClick() }) {
Text(text = "$text value: $number")
}
}
you can discover it by yourself 🙂
From the documentation:
The onCommit effect is a lifecycle effect that will execute [callback] every time the composition commits.
So, if I’m not mistaken this callback is called every time recomposition happens 🙂@Immutable
annotation to achieve that)julioromano
01/21/2021, 2:30 PMclass MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val stateFlow = MutableStateFlow(State())
setContent {
MyApplicationTheme {
MyScreen(
stateFlow = stateFlow,
onClick1 = {
stateFlow.value = stateFlow.value.copy(
firstCounter = stateFlow.value.firstCounter + 1
)
},
onClick2 = {
stateFlow.value = stateFlow.value.copy(
secondCounter = stateFlow.value.secondCounter + 1
)
}
)
}
}
}
}
MyScreen
composable and throwing everything into setContent {}
):
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val stateFlow = MutableStateFlow(State())
setContent {
MyApplicationTheme {
val state by stateFlow.collectAsState()
Column {
MyButton(
text = "One ${state.firstCounter}",
onClick = {
stateFlow.value = stateFlow.value.copy(
firstCounter = stateFlow.value.firstCounter + 1
)
}
)
MyButton(
text = "Two ${state.secondCounter}",
onClick = {
stateFlow.value = stateFlow.value.copy(
secondCounter = stateFlow.value.secondCounter + 1
)
}
)
}
}
}
}
}
In this case changing just one field triggers recompose of both buttons.
Why is that?Adam Powell
01/21/2021, 3:21 PMinline
or use some special compose compiler directive annotations, a recompose scope is created by each composable function body. This is both for composables declared with fun
as well as composable lambda blockscollectAsState
. Your data class State
object is just a plain data class that involves no snapshot state properties.state
in two places: each one of the string interpolations to create the text for each Text
composable. If state
changes, each of these must recompose to create new input strings for the `Text`s.Button
composable is not declared inline
, so that means that the current recompose scope for those two reads of state
that create new strings will be the content body of each respective Button
call.Button
content body blocks recompose doesn't mean they'll have a lot of work to do. They'll create new strings from the new value of state
, but if those resulting strings match their old values, the calls to Text
may skip, since there isn't new work to do.julioromano
01/21/2021, 4:57 PMval state by stateFlow.collectAsState()
Column {
// Scope A
Button(onClick = onClick1) {
// Scope B
Text(text = "One ${state.firstCounter}")
}
Button(onClick = onClick2) {
// Scope C
Text(text = "Two ${state.secondCounter}")
}
}
In example 1 state
is read twice, one from within Scope B and one from within Scope C.
Example 2:
val state by stateFlow.collectAsState()
Column {
// Scope A
MyButton(
text = "One ${state.firstCounter}",
onClick = onClick1
)
MyButton(
text = "Two ${state.secondCounter}",
onClick = onClick2
)
}
In example 2 state
is still read twice, but both reads happen within Scope A.
Therefore when state
changes:
- In example 1 both `Button`s are always invalidated and re-run, but any Text
composable whose input text
param doesn’t change is skipped.
- In example 2 it’s actually Column
that gets invalidated and re-run, but any MyButton
composable whose input text
param doesn’t change is skipped.
Can this “skipping” be regarded as some form of memoization?
Thanks a lot for the explanation!Adam Powell
01/21/2021, 5:01 PMthis
and receivers!) then we can't know it's safe to skip, so we won't@Stable
contract, which these days we also infer through static analysis at compilation timejulioromano
01/21/2021, 5:08 PMStability determination throws a bit of a wrench into when we can skip and still be correct occasionally, so I’m deliberately hand-waving a bit hereIn fact while writing I wasn’t sure whether the
onClick
param could actually prevent the skipping for MyButton
: one thing is to check equality on a string, but checking it on a lambda doesn’t seem equally simple.Adam Powell
01/21/2021, 7:43 PMonClick
lambda. Setting a StateFlow
value in that lambda as opposed to setting a mutableStateOf()
snapshot state object (which we know is stable) likely behaves differently here.Chuck Jazdzewski [G]
01/21/2021, 9:53 PMCan this “skipping” be regarded as some form of memoization?This is exactly what it is but we call it Positional Memoization because we consider the "position" of the call in the call graph as an implicit memoization parameter.
onClick
we use referential equality for lambdas. For lambda expressions in composable functions we will remember
the lambda instance based on the values it captures.@Immutable
the compiler will now infer @Stable
(the more general annotation) for types such as State
class at the beginning of the thread. In most cases, you no longer need to mark a class as @Stable
as we will infer it. The composable function includes checks for stability of inferred types crossing module boundaries but in such a way that R8 can remove the checks for the application using the modules.