https://kotlinlang.org logo
#compose
Title
# compose
p

Piotr Prus

07/30/2022, 8:37 PM
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

saket

07/31/2022, 2:14 AM
r

Rick Regan

07/31/2022, 3:38 AM
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

Piotr Prus

08/02/2022, 9:42 AM
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

Rick Regan

08/02/2022, 1:28 PM
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

Piotr Prus

08/02/2022, 1:30 PM
that is my use case as well. It just work as expected with destructuring declaration.
r

Rick Regan

08/02/2022, 1:32 PM
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

Zach Klippenstein (he/him) [MOD]

08/03/2022, 7:00 PM
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

Rick Regan

08/03/2022, 8:29 PM
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
25 Views