Thread
#compose
    Rick Regan

    Rick Regan

    1 year ago
    I have a
    Text
    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 😃 )
    If I call
    DiscreteSlider()
    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]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    Each composable function gets a recompose scope – the scope begins when the function’s body starts executing, and ends when it stops. When function reads a snapshot state, the recompose scope of the function that the read is executed in is the one that gets marked as dirty and will be recomposed for the next frame. When a recompose scope gets recomposed, it just gets its function called again – and the entire function body runs (although if it calls other composable functions, they may just return cached values). In this case, the.
    DiscreteSlider
    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

    Rick Regan

    1 year ago
    Well I think it's "off by two" now because I'm more confused 😃. So basically, any value that changes (mutable state or not) will trigger recomposition if the composable that reads it happens to be in the same scope as another composable that is updated by mutable state?
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    Ugh, slack is just destroying my messages today
    No, the fact that
    sliderValueInt
    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
    .
    If you remove
    sliderValueFloat = it
    from your event handler, or pass a different (non-mutable) value of
    sliderValueFloat
    to
    Slider(value =
    , the recomposing should stop.
    Rick Regan

    Rick Regan

    1 year ago
    I meant that, in this case, the mutable state
    sliderValueFloat
    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]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    I mean, most code will execute differently depending on how it’s written 😛 Reading a state causes the entire composable function or lambda in which the state was read to recompose. Your
    Text
    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).
    It 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

    Rick Regan

    1 year ago
    I had always taken that message to mean things can be recomposed n times, so you should not have side effects. In this case, n is zero if the function is in the "wrong" scope.
    There seems to be more to this. This code does not cause a recompose of the
    Text
    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]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    Yes, because the lambda
    { 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

    Rick Regan

    1 year ago
    Thanks for explaining the role of scope. However, I'm going to assume that "piggybacking" on some other recomposition as a by-product of scope is not something we can rely on, and that anything we want updated in the UI should observable (mutable state). Now of course that was my assumption until this morning, when I changed the scope in my code and broke it due to the unintentional omission of
    mutableStateOf
    .
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    I think that’s a pretty safe assumption 😉