I have a problem with slider and its `onValueChang...
# compose
p
I have a problem with slider and its
onValueChangedFinished
when use on slider with steps and snapping. It do not have a value parameter, so I am taking the parameter from my remembered field. The value that is stored in the moment of
onValueChangedFinished
is not the value that slider will snap to. It is the value from the moment I released the finger. I do not understand the point of this lambda and I do not know how to properly update the slider value, since I allow only these values from step point, not float from the middle. Code and example in the thread 🧵 👇
Copy code
@Composable
fun SliderTest() {
    val (sliderValue, setSliderValue) = remember { mutableStateOf(0f) }
    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Slider(
        modifier = Modifier.testTag(SLIDER_TAG).fillMaxWidth(),
        value = sliderValue,
        valueRange = 0f..list.size.minus(1).toFloat(),
        steps = list.size.minus(2),
        onValueChange = {
            println("AAAA, value change: $it")
            setSliderValue(it)
        },
        onValueChangeFinished = {
            println("AAAA, onFinished: $sliderValue")
        }
    )
}
Output:
Copy code
AAAA, value change: 2.1914978
AAAA, value change: 2.1914978
AAAA, value change: 2.1767893
AAAA, value change: 2.1058307
AAAA, value change: 2.0159974
AAAA, value change: 2.0033238
AAAA, value change: 2.0
AAAA, onFinished: 2.1914978
I released a finger at 2.19. The value is snapped to 2.0, but the moment lambda is invoked is giving me the “wrong” value.
s
r
When I try it with
var sliderValue by remember { mutableStateOf(0f) }
instead of
val (sliderValue, setSliderValue) = remember { mutableStateOf(0f) }
(and in onValueChange do
sliderValue = it
instead of
setSliderValue(it)
it works as expected.
https://developer.android.com/jetpack/compose/state says the three ways to declare
MutableState
are equivalent, but that appears not to be the case here: 1.
val sliderValue = remember { mutableStateOf(0f) }
(with
println("AAAA, onFinished: ${sliderValue.value}")
) works. 2.
var sliderValue by remember { mutableStateOf(0f) }
(with
println("AAAA, onFinished: $sliderValue")
) works 3.
val (sliderValue, setSliderValue) = remember { mutableStateOf(0f) }
(with
println("AAAA, onFinished: $sliderValue")
) doesn’t work. For #3, the onFinished prints a previous value of the Slider, which is easiest to see when you tap the slider instead of dragging it. I guess the destructuring declaration “captures” an earlier value? I’ve never used this form before -- is that expected?
p
great finding @Rick Regan! Thank you for that. I think this should be reported on issue tracker, if it is not expected behaviour. @jim can you take a look?
r
To answer your second question about the use of `onValueChangeFinished`: I use it in my app’s settings. I use
onValueChange
to continuously set the slider value in a mutable state variable but then use
onValueChangeFinished
to write that value to a preferences DataStore. For other uses of Sliders I don’t have an
onValueChangeFinished
at all.
p
that is my use case as well. It just work as expected with destructuring declaration.
r
It will be interesting to hear a Compose expert’s opinion on that ...
This takes
Slider
out of the picture and still demonstrates the same phenomenon:
Copy code
@Composable
fun TwoLambdaTest() {
  val (value, setValue) = remember { mutableStateOf(0) }
  TwoLambdaComposable(
    value = value,
    onValueChange = {
      println("AAAA, onValueChange, value: $value")
      println("AAAA, onValueChange, it: $it")
      setValue(it)
    },
    onValueChangeFinished = {
      println("AAAA, onValueChangeFinished: $value")
    },
  )
}

@Composable
fun TwoLambdaComposable(
  value: Int,
  onValueChange: (Int) -> Unit,
  onValueChangeFinished: () -> Unit,
) {
  Button(
    onClick = {
      onValueChange(Random.nextInt(0, 10))
      onValueChangeFinished()
    }
  ) {
    Text(text = value.toString())
  }
}
This prints
Copy code
AAAA, onValueChange, value: 0
AAAA, onValueChange, it: 4
AAAA, onValueChangeFinished: 0
AAAA, onValueChange, value: 4
AAAA, onValueChange, it: 8
AAAA, onValueChangeFinished: 4
If I use the two other forms of mutable state it works as expected:
Copy code
AAAA, onValueChange, value: 0
AAAA, onValueChange, it: 4
AAAA, onValueChangeFinished: 4
AAAA, onValueChange, value: 4
AAAA, onValueChange, it: 8
AAAA, onValueChangeFinished: 8
@Zach Klippenstein (he/him) [MOD] Zach, since you’re the author of two good articles on scoped recomposition and
remember
, I wonder if you could explain why the destructuring declaration form of mutable state behaves differently than the other two “equivalent” forms. Thanks.
z
Because of when the read of the MutableState happens. The destructuring is just syntactic sugar for:
Copy code
val myStateHolder = remember { mutableStateOf(…) }
val myState = myStateHolder.value
val changeMyState = { myStateHolder.value = it }
Look at the implementation – that’s literally exactly what the component functions are doing. That
.value
is the thing to look for – that’s the “read” that will trigger whatever scope it executes in to restart when the value changes. Because the read here happens in the composition directly, it will always trigger recomposition when it changes, and anything capturing
myState
will capture the value read from the MutableState at the time it was captured.
r
I tried to convince myself that that’s how it worked (I had taken an amateur look at SnapshotState.kt) but then I was confused by why, with each click, the lambda keeps getting a new (albeit prior) value. Well maybe I should first make sure I understood what you said: The read at
val (value, setValue) ...
does happen again (because
setValue(it)
causes recomposition of
TwoLambdaTest()
) but since the actual read of the State is not done in the lambda, the initial value it captured from initial composition is all it’s going to see. But then why, with each click, does the lambda recapture a new value?
Wait I think I get it now: the lambda does recapture with each recomposition, but the value is old with respect to the next time it runs.
FYI, here’s a similar discussion from this channel in January, which I now recall seeing, but obviously didn’t digest until I tried it for myself: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1643654501452869