So, yesterday, I was tracking down what I am refer...
# compose
n
So, yesterday, I was tracking down what I am referring to as unexpected recomposition issues in an app I'm working on that deals with images from the CameraX API. Content Redacted for brevity, see thread.
j
This question scares me a little, because it sounds like you are depending on something not recomposing. Compose reserves the right to recompose at any time for any reason at its sole discretion. A Composable function should be a functional transform from input data->UI, so you should not know or care when extra recompositions happen. If an extra recomposition would cause a problem of any kind (other than just the performance/CPU cost of recomposing) then you should rethink your composable.
n
Okay, yeah, I was afraid of that too. So I'll rethink my composables. Mixing the CameraX API and Compose is tricky for me to wrap my head around and there aren't many examples of how to do this properly that have been updated along with the changes in the beta release of compose. I've developed a feedback loop and need to break out of it.
Moving the original content here for completeness: I had a scaffold with the following content:
Copy code
Scaffold(
        content = { padding ->
            Column(horizontalAlignment = Alignment.End, modifier = Modifier.padding(padding)) {
                ProcessedImage()

                var sliderValue by rememberSaveable { mutableStateOf(75.0f) }
                Slider(
                    valueRange = 8f..100f,
                    steps = 12,
                    value = sliderValue,
                    onValueChange = {
                        sliderValue = it
                        onSliderValueChanged(it)
                    })

                PreviewRow(backCameraToggled, onImage)
            }
        }
The PreviewRow here, was being recomposed every time the slider moved, so I decided to refactor the slider into its own Composable function, which ended up looking like:
Copy code
@Composable
private fun PixelSizeSlider(onSliderValueChanged: (Float) -> Unit) {
    var sliderValue by rememberSaveable { mutableStateOf(75.0f) }

    Slider(
        valueRange = 8f..100f,
        steps = 12,
        value = sliderValue,
        onValueChange = {
            sliderValue = it
            onSliderValueChanged(it)
        })
}
And which in turn made the scaffold content change to:
Copy code
content = { padding ->
            Column(horizontalAlignment = Alignment.End, modifier = Modifier.padding(padding)) {
                ProcessedImage()

                PixelSizeSlider(onSliderValueChanged)

                PreviewRow(backCameraToggled, onImage)
            }
        }
After doing this,
PreviewRow
was no longer getting recomposed whenever the slider was modified. When I look at the code, I can't figure out why this was happening.
PreviewRow
doesn't depend on the
sliderValue
so shouldn't it be skipped when being composed? Some may ask how I'm testing this. I'm using the debugger and putting debug logging breakpoints into my composables to see which ones get hit, not very scientific, but I would hope that it would work correctly.
r
I understand that under the covers there can be extra recompositions (for internal reasons we should not need to know or care about) but aren't there some basic "external" rules for recomposition that we should know? We (those new to Compose specifically) couldn't use those rules to depend on recomposition, but we could structure our state and composables (for example, to know how granular to make our state and which composables should see it). I do understand why you'd want to make it a blanket statement -- so no one writes code dependent on recomposition -- but I think we have to distinguish some of the basic tenets of recomposition.
j
@Rick Regan I'm not sure I understand your question, but maybe the following will be helpful to you: • Your composable functions should be pure stateless functional transforms of data to UI. They should not have any side effects and should never mutate any data. • Your composable functions should depend only upon the data passed directly to them. Avoid depending on globals, ThreadLocals, CompositionLocals, filesystem, network, or any other sideways data loading techniques. • Your composable functions should never make any assumptions about how often they will be run, when they will be run, on what thread they will be run, etc. • Your composable functions should be independent of your application code, and should not access anything android-specific. In particular, this means you should avoid using AAC view models, avoid using Android Context, and avoid using any other androidisms. Similarly, you should avoid using any data structure that is intimately tied to your application. A good rule of thumb is to ask yourself the question "what would happen if I copy-pasted this composable function into some random application like the 'United Airlines' app; would it still work as defined? Generally, all the other bullet points are really just implications of the first bullet point. If your composable is a pure stateless functional transform of data to UI, everything else is mostly automatic. These constraints exist for a couple of reasons. (1) they make the data flow in your application MUCH more managable and maintainable (2) they ensure your composables won't break when Compose does things like multithreading and reprioritization and other clever optimizations.
💯 3
r
I was thinking of how you would convey a level of understanding of recomposition that informs decisions like "should I use a mutable array of state or an array of mutable state" but doesn't make anyone dependent on it for correctness. In other words, performance (and maybe aesthetics) reasons only. I suppose we could just view it as a black box, but I assume that you could really mess up performance if you didn't know something about how recomposition works. Maybe a specific example of the type of question would clarify: If I had a matrix of 25 buttons, each with text that may change, should each text value be its own mutable state (and passed individually to each corresponding button), or should the entire 5 x 5 array be the mutable state (entire new array passed to each button)? (I suppose it may depend on what percentage of values may change at a time, but that's the idea.) I assume if one button changes, only that button is recomposed in the former, but all buttons are recomposed in the latter (subject to other reasons why internally you may decide to compose).
j
The answer to that question depends largely on how much someone wants to lean into immutable vs. observable-mutatable data. As a general rule of thumb, it is premature to optimize performance before you are able to measure a problem, and then you can start optimizing the measurable thing.
r
I guess I was smart then to go with the full mutable array for starters :)
I will certainly look into measuring it at some point. What I have done in my app for now does not seem to be a problem, probably to your point.
c
how much someone wants to lean into immutable vs. observable-mutatable data
@jim This is something that I still need to get better about. I originally thought with compose that my approach of having everything be immutable would work really well, but the two approaches available are interesting. My team lead pushed really hard on immutability a year or so ago and so I'm in this constant mode of trying to make everything immutable.