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 🙂KamilH
01/21/2021, 1:06 PM@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
)
}
)
}
}
}
}julioromano
01/21/2021, 2:31 PMMyScreen 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 PMAdam Powell
01/21/2021, 3:21 PMAdam Powell
01/21/2021, 3:22 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 blocksAdam Powell
01/21/2021, 3:26 PMAdam Powell
01/21/2021, 3:28 PMAdam Powell
01/21/2021, 3:29 PMAdam Powell
01/21/2021, 3:32 PMAdam Powell
01/21/2021, 3:34 PMAdam Powell
01/21/2021, 3:36 PMcollectAsState. Your data class State object is just a plain data class that involves no snapshot state properties.Adam Powell
01/21/2021, 3:37 PMstate 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.Adam Powell
01/21/2021, 3:40 PMButton 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.Adam Powell
01/21/2021, 3:42 PMButton 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 PMAdam Powell
01/21/2021, 5:01 PMthis and receivers!) then we can't know it's safe to skip, so we won'tAdam Powell
01/21/2021, 5:02 PM@Stable contract, which these days we also infer through static analysis at compilation timeAdam Powell
01/21/2021, 5:03 PMAdam Powell
01/21/2021, 5:04 PMAdam Powell
01/21/2021, 5:04 PMjulioromano
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.julioromano
01/21/2021, 5:09 PMAdam 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.
Chuck Jazdzewski [G]
01/21/2021, 10:12 PMonClick we use referential equality for lambdas. For lambda expressions in composable functions we will remember the lambda instance based on the values it captures.Chuck Jazdzewski [G]
01/21/2021, 10:20 PM@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.