My `Slider` is very laggy. I have an `onValueChang...
# compose
l
My
Slider
is very laggy. I have an
onValueChange
callback from Slider to my ViewModel which updates the number using a simple calculation. This is release build.
Copy code
var thresholdProgress: Float by rememberSaveable(uiState.thresholdProgress) {
        mutableStateOf(uiState.thresholdProgress)
    }
            Slider(
                value = thresholdProgress,
                onValueChange = {
                    thresholdProgress = it
                    viewModel.onThresholdProgressChanged(it)
                },
            )
Copy code
fun onThresholdProgressChanged(progress: Float) {
        val progressStep = (progress * 100).roundToInt()
        val lux = if (progressStep <= 50) {
            progressStep
        } else {
            50 + (progressStep - 50) * 5
        }
        _uiState.update { it.copy(lux = lux) }
    }
uiState.lux
is used for continuously updating the lux number in the UI as user drags the slider.
When I remove the calculation from
onThresholdProgressChanged()
then the slider becomes responsive again. Should I move the calculation to another dispatcher? Is it blocking the main thread?
f
So you observe the
uiState.lux
in UI and update the
thresholdProgress
MutableState?
z
Could it be that when uiState updates, a larger portion of the screen is recomposed as well?
l
From
uiState.lux
I only update the LUX value in the text field
underneath the slider
thresholdProgress
is remembered directly in the composable
I have the impression that the
onValueChanged
callback is not throttled in any way
I.e. it is probably bombarded with the user movement events and I have to throttle somehow my UI updates in
onThresholdProgressChanged()
The LUX text field looks like this in the composable:
Copy code
Text(
                text = stringResource(id = R.string.light_sensor_settings_threshold_lux, uiState.lux),
                style = BegaTheme.typography.captionDynamic,
            )
Maybe it loads the string resource every time?
z
Can you share the entire code for the (what looks like) bottom sheet?
l
It would be a lot of copy paste 😉 Yes, it is a
ModalBottomSheetLayout
But the
sheetContent
is just a simple
Column
with a few Rows containing the Slider and the Text and Icon elements
z
Is uiState only observed in the sheetContent?
l
Yes
For the screen in the background there is a separate viewmodel and separate uiState
z
Can you temporarily remove all the other composables from the sheet besides the slider? Does it still lag?
l
It doesn't lag when I outcomment the line updating the
uiState.lux
from the callback
(See in the code snippet at the beginning of the thread in the
onThresholdProgressChanged()
I suspect I have to throttle the
uiState
updates somehow
z
Hmm, in your
rememberSaveable(uiState.thresholdProgress)
I think youre creating a new mutableState everytime the progress changes
Is that intentional?
l
Yes, but the
thresholdProgress
in the ViewModel is only updated when I load the data from API when creating the bottom sheet for the first time. The value in the ViewModel is not updated while dragging the slider.
z
You could try using this on the other composables in the sheet, it will highlight whenever they recompose.. and if stuff is flashing like crazy, thats probably it 🙂
l
Hmm, I think Dolphin also had something like this built in...
z
I worked with a pretty similar case recently where a slider in my app lagged behind. I eventually solved it by really limiting the recomposition scope of it, now its the only thing that recomposes, and its super snappy!
Dolphin? 🐬
l
Android Studio Dolphin, sorry
l
There is a similar problem mentioned on Stack Overflow (without an answer) https://stackoverflow.com/questions/69394091/jetpack-compose-slider-performance
z
I have zero experience with LiveData unfortunately! If its not too much work, you could try replacing it with MutableState just to verify whether or not thats where the bottleneck is 🙂
l
This is someone else's issue, I use StateFlow observed as MutableState
z
Gotcha, thats good 👍🏽 Id say try the recomposition tracking I mentioned earlier then, I still believe thats the root cause. You can also set the slider increments to a larger number to get a little fewer updates, but thats probably not what you really want?
l
I just checked recomposition tracking and there's no flashing at all.
I will try with a custom number of steps
Because I only need 100 steps from 1-100
r
So try
valueRange = 0f..100f
and
steps = 99
. (Endpoints would be 0, 100 which I think matches what you have now.) If you use the Material3 version of slider you’ll get fewer callbacks (only on the “integer” ticks...I say “integer” because they’re floats and may be ever so slightly off the integer marks).
l
I just tried it. I think the Slider was not designed for such a high number of steps. It makes tiny "jumps" between each step. The movement is snappier, but it's jerky.
Also it now paints the part behind the slider marker black... but not completely, but leaving just a thin orange line on top.
r
I just tried a Material 3 Slider with 100 steps and it seems to move OK, even in a debug build (of course I only have a Slider and not your entire screen). With a discrete Material 3 Slider there will be little discrete jumps, which may not be what you want. My colors are also OK. FWIW, even the Material 2 version moves OK, though I see it has more callbacks.
l
I use Material 2
When I don't update the
uiState.lux
(which only influences the
Text
displaying the LUX value) then the Slider moves super fluidly.
Maybe that's a problem that I have it in the
ModalBottomSheetLayout
?
z
If you add this right next to the slider, how many calls do you get if you move the slider one step?
Copy code
SideEffect { 
    Log.d("TAG", "Recomp")
}
l
It won't log the slider recomposition, only of the containing composable...
z
I think they share the same recomposition scope, no?
l
Slider recomposition would have to be logged inside the
Slider
I'm not sure
I thought only the composables where the state changes get recomposed
But maybe I'm wrong
I'm testing this
So it gets called a lot indeed when I drag the slider
How can I limit the recomposition scope of the Slider?
z
Can you please share the code that surrounds the slider, including where uiState is collected?
l
That's really a lot of code if you want the whole context
There are three components:
SystemFragment
where the composables are inflated and the state from viewmodels gets collected
SystemScreen
which is a
ModalBottomSheetLayout
where the
sheetContent
is the
SensorSettingsSheet
containing the `Slider`; the `SystemScreen`'s content is the screen visible in the background
SensorSettingsSheet
which is the actual composable where the
Slider
is
There is also a ViewModel, relevant part of which I posted at the beginning of the thread
z
Thank you! I think the problem is that youre collecting the uiState at the root of the composables, so everytime it updates, pretty much everything else is forced to recompose as well. As a rather quick experiment, you could try passing the viewmodel to SystemScreen, then to SensorSettingsSheet, and collect the value there instead - that should help a lot!
l
I thought only the parts of the composable where the
uiState
value actually changes get recomposed...
I cannot pass viewmodels to all the composables, especially that I have custom composables - what viewmodels should they accept?:
Isn't Compose smart enough to calculate the diff between the old and new
uiState
and only recompose what's needed?
z
Another thing that comes to mind, if uiState is immutable you could annotate it with @Immutable as well, that will help compose be smarter about what it recomposes too 🙂
l
I tested
@Immutable
and the slider is still as laggy as it was
z
I cant say that I fully understand how compose handles recompositions, but in most cases you dont need to think too much about these things, it just works. This is a great article on the subject.
l
Yes, in my experience it also just works usually, apart from cases like this...
Thanks for all your helpful suggestions!
z
Im confident that the viewmodel solution I mentioned will work wonders. Im just about out of time now, good luck - and Ill respond again later if you have any further questions 🙂
l
Thank you again, that's very kind of you! 🙏
Ok, I tested passing the viewmodel directly to the composable and it indeed works super fluid!
But how do I build my previews now?
And what about state hoisting, which is officially recommended by Google?
I'm confused, because it should work the same way with state hoisting, but clearly isn't.
s
If you really need to defer state reads to a composable down the line (and don’t want to pass viewmodels like you’re saying here) you’ll have to pass down lambdas that read the state, something like here. If you call a composable which takes in a parameter, even if you’ve got it backed inside a State object, in order to call that function you’re implicitly calling .value on it, therefore invalidating the current scope. If you instead pass it inside a lambda you’re then not reading it in that scope and can defer the reading into as small scope as possible.
z
Ah, sorry I dont use viewmodels so I didnt know that was a thing. Id say you can do one of two things; 1. Do the
uiState.collectAsState()
like you currently are, but replace 'by' with '=' so that you get a State<UiState>. Pass it to the inner composables, like I mentioned with viewmodel, but only read (state.value) its value where youre also using it. 2. Pass uiState (the stateflow) down the composables instead, and collectAsState() where you also read the value 🙂
l
Oh yes, both your and @Stylianos Gakis’s solutions seem like exactly what I need! This way I can indeed defer reading the state to the actual composable that uses it and probably still easily create previews by just creating
mutableStateOf(MyUiState(...))
. BTW if you don't use viewmodels, what do you use? Do you just keep the whole state in the composable itself?
z
👍🏽 I use something completely custom 😄 The same principles apply where composables just get some state that they render. If you want, you can take a look at this library; its not what I use but the concept is similar.
l
Awesome, thank you both for the time and help, and explaining everything very clearly along the way!
@Zoltan Demant I went with your solution 1. - replace
by
with
=
in
collectAsState()
and only reading the
value
inside the actual composable that's interested in it.
I suspect part of the lagging might have also been caused by the fact that I was constantly passing the collected state to the "big" screen, including the
ModalBottomSheetLayout
. That might have caused too many calculations, even though in the end the state of the bottom sheet didn't influence the state of the screen visible in the background.
s
Just before you invest too much into passing such parameters, take a look at Adam’s suggestions here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1650307495136109?thread_ts=1650289906.051689&amp;cid=CJLTWPH7S and strongly consider what it is that he is suggesting here.
l
Oh yes, the deferred read via lambda looks indeed much better
I didn't know Compose gets so complicated when you get only 2 levels of composables, and try to improve performance.
This is really actually much harder to grasp initially than just writing XML. You have to get deep into many Kotlin language concepts and what their effects are.
s
Yeah but the idea is that you don't need to do that most of the time. And always this isn't about improving correctness, just performance. You can get correct output without knowing any of this, which is the important bit imo.
l
Yes, true. But if you don't understand very deeply how it works, then such performance issues are a brick wall. If it weren't for you and @Zoltan Demant I wouldn't figure it out on my own (unless I would spend a week reading Compose documentation and experimenting with sample apps).
s
To be fair however, if one hits a performance road block, I'd guess they'd go to the official docs about how to improve performance, on the doc part named exactly "performance" https://developer.android.com/jetpack/compose/performance. And said documentation explains with an imo quite clear example how you can defer those reads https://developer.android.com/jetpack/compose/performance#defer-reads to improve performance. I agree that the community here is amazing and I've gotten an immense amount of help myself too, but the more I read the docs the more I realize they're in general incredibly well written.
l
Definitely, I must admit I was amazed by the relatively easy start with Compose and skipped reading most of the documentation in the beginning because the learning curve was so easy.
But now I get to the tough nuts and it's time to dig into the docs. They are indeed very clearly written as you say.
m
ya. cases like this, if you get the input from the viewmodel and then send the output to the viewmodel which in turn feeds back in the composable, you will have an infinite recomposition chain. Just observe the writes without feeding it back in.
l
In this particular case there is no infinite recomposition. I was not sending the slider progress from viewmodel back to the composable. Just the recomposition scope was too big (the screen in the background got unnecessarily recomposed every time when dragging the slider).