Rick Regan
03/15/2021, 2:07 PMText
that gets recomposed even though its value is not based on mutable state. In particular, this happens when I include it in another composable that has a Slider
with its own mutable state, and it doesn't happen when I put it in a separate composable (see thread for code). I know we're not supposed to depend on when recomposition happens or doesn't happen; I'm just trying to understand how Compose works (and avoid bugs that are hidden until code is refactored :) )Rick Regan
03/15/2021, 2:08 PMDiscreteSlider()
the Text
updates; if I call DiscreteSliderInParts()
it does not.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(null) {
Column {
DiscreteSlider() // Text gets recomposed
//DiscreteSliderInParts() // Text does not get recomposed
}
}
}
}
var sliderValueFloat by mutableStateOf(0f)
var sliderValueInt: Int = 0
@Composable
fun DiscreteSlider() {
Slider(
value = sliderValueFloat,
onValueChange = {
sliderValueInt = round(it).toInt()
sliderValueFloat = it
},
valueRange = 0f..10f,
steps = 9
)
Text(text = sliderValueInt.toString())
}
@Composable
fun DiscreteSliderInParts() {
DiscreteSliderSlider()
DiscreteSliderText()
}
@Composable
fun DiscreteSliderSlider() {
Slider(
value = sliderValueFloat,
onValueChange = {
sliderValueInt = round(it).toInt()
sliderValueFloat = it
},
valueRange = 0f..10f,
steps = 9
)
}
@Composable
fun DiscreteSliderText() {
Text(text = sliderValueInt.toString())
}
(I'm on beta02.)Zach Klippenstein (he/him) [MOD]
03/15/2021, 2:40 PMDiscreteSlider
is reading sliderValueFloat
. So when your onValueChange
mutates this value, it marks DiscreteSlider
as dirty, and the entire DiscreteSlider
function will be called again for the next frame. The fact that you’re then passing the value of sliderValueFloat
to Slider
doesn’t affect `DiscreteSlider`’s recomposition.Rick Regan
03/15/2021, 2:59 PMZach Klippenstein (he/him) [MOD]
03/15/2021, 3:00 PMZach Klippenstein (he/him) [MOD]
03/15/2021, 3:01 PMsliderValueInt
changes is irrelevant. As long as at least one value that is read in a recompose scope changes, that entire recompose scope needs to be recomposed. So in this case that’s triggered by sliderValueFloat
.Zach Klippenstein (he/him) [MOD]
03/15/2021, 3:03 PMsliderValueFloat = it
from your event handler, or pass a different (non-mutable) value of sliderValueFloat
to Slider(value =
, the recomposing should stop.Rick Regan
03/15/2021, 3:19 PMsliderValueFloat
causes the Text
to recompose, even though sliderValueInt
changed but is not mutable. It still seems strange that we can get different behavior based upon how things are packaged. But so it is. Thanks for the explanation.Zach Klippenstein (he/him) [MOD]
03/15/2021, 3:33 PMText
could use a hard-coded value and the enclosing function would still execute when any states it reads changes.
One thing that I found helpful to build my intuition around this is to think of every state read as being moved to the top of the function by the compiler. sliderValueFloat
has to be read before the Slider
function can be called, so the read is associated with the outer function, not the one it calls. I suppose the compose compiler could potentially try to be extra clever and realize that sliderValueFloat
was only read in order to pass it to Slider
, and thus only associate its read with `Slider`’s recompose scope. I don’t know the actual reasons for not doing this, but I’m guessing it would make the whole mechanism an order of magnitude more complex, and probably take more memory (if you performed some operation on the value before passing it to Slider
, the call to Slider
would effectively need to be wrapped in an anonymous function to perform the operation since the Slider
bytecode can’t be modified for each caller).Zach Klippenstein (he/him) [MOD]
03/15/2021, 3:34 PMIt still seems strange that we can get different behavior based upon how things are packaged.This is probably why the compose docs and team all stress so heavily that it is bad to rely on how many times any given function is actually recomposed at runtime.
Rick Regan
03/15/2021, 4:00 PMRick Regan
03/15/2021, 4:41 PMText
when it is not based on mutable state:
var outputText = "123"
//var outputText by mutableStateOf( "123")
var buttonText by mutableStateOf("Toggle")
@Composable
fun RecomposeTest() {
Column {
Button(
onClick = {
buttonText = if (buttonText == "Toggle") "Toggled" else "Toggle"
outputText += "${kotlin.random.Random.nextInt(48, 58).toChar()}"
}
) {
Text(text = buttonText)
}
Text(text = outputText)
}
}
Zach Klippenstein (he/him) [MOD]
03/15/2021, 4:49 PM{ Text(text = buttonText) }
has its own recompose scope. The buttonText
state is only read from that inner scope, it’s not read by RecomposeTest
directly, so `RecomposeTest`’s recompose scope isn’t invalidated.Rick Regan
03/15/2021, 6:05 PMmutableStateOf
.Zach Klippenstein (he/him) [MOD]
03/15/2021, 6:41 PM