Jetpack Compose:对滑块更改值做出反应

Jon*_*nas 10 android kotlin android-jetpack-compose

我想显示一个滑块,该滑块可以由用户使用拖放进行更新,也可以从服务器实时更新。

当用户完成拖动手势时,我想将最终值发送到服务器。

我最初的尝试是:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(value = sliderValue, onValueChange = onDimmerChanged)
}
Run Code Online (Sandbox Code Playgroud)

onDimmerChanged方法来自我的 ViewModel,它更新服务器值。它工作得很好,但是onValueChange每次移动都会被调用,这会用不需要的请求轰炸服务器。

我尝试创建一个自定义滑块:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}
Run Code Online (Sandbox Code Playgroud)

它在应用程序端运行良好,并且该值仅在用户停止拖动时发送一次。

但是,当服务器有更新时,它无法更新视图。我猜这与remember. 所以我尝试不使用remember

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by mutableStateOf(initialValue)
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}
Run Code Online (Sandbox Code Playgroud)

这次,当服务器更新值时,视图会正确更新,但当用户拖动滑块时,视图不再移动。

我确信我错过了一些与状态提升有关的东西,但我不知道是什么。

所以我的最后一个问题是:如何创建一个可由 ViewModel 或用户更新的 Slider,并仅在用户完成拖动时通知 ViewModel 新值?

编辑:

我还尝试了@CommonsWare 的建议:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    val sliderState = mutableStateOf(sliderValue)
    Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}

@Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
    Slider(
        value = state.value,
        onValueChange = { state.value = it },
        onValueChangeFinished = valueSet
    )
}
Run Code Online (Sandbox Code Playgroud)

而且它也不起作用。使用拖放时,sliderState会正确更新,并onDimmerChanged()使用正确的值进行调用。但由于某种原因,当点击滑动(而不是滑动)时,valueSet被调用并且sliderState.value不包含正确的值。我不明白这个值从哪里来。

Ste*_*nke 9

关于本地和服务器(或视图模型)状态相互冲突的原始问题:

我通过检测我们是否正在与滑块交互来解决这个问题:

  • 如果是,则显示并更新本地状态值
  • 或者如果没有 - 则显示 viewmodels 值

正如您所说,我们永远不应该从 - 更新视图模型onValueChange,因为这仅用于本地更新滑块值(文档)。相反,一旦我们完成交互,onValueChangeFinished就用于将当前本地状态发送到视图模型。

关于当前交互的检测,我们可以利用InteractionSource.

工作示例:

@Composable
fun SliderDemo() {
    // In this demo, ViewModel updates its progress periodically from 0f..1f
    val viewModel by remember { mutableStateOf(SliderDemoViewModel()) }


    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {

        // local slider value state
        var sliderValueRaw by remember { mutableStateOf(viewModel.progress) }

        // getting current interaction with slider - are we pressing or dragging?
        val interactionSource = remember { MutableInteractionSource() }
        val isPressed by interactionSource.collectIsPressedAsState()
        val isDragged by interactionSource.collectIsDraggedAsState()
        val isInteracting = isPressed || isDragged

        // calculating actual slider value to display
        // depending on wether we are interacting or not
        // using either the local value, or the ViewModels / server one
        val sliderValue by derivedStateOf {
            if (isInteracting) {
                sliderValueRaw
            } else {
                viewModel.progress
            }
        }


        Slider(
            value = sliderValue, // using calculated sliderValue here from above
            onValueChange = {
                sliderValueRaw = it
            },
            onValueChangeFinished = {
                viewModel.updateProgress(sliderValue)
            },
            interactionSource = interactionSource
        )

        // Debug interaction info
        Text("isPressed: ${isPressed} | isDragged: ${isDragged}")
    }
}
Run Code Online (Sandbox Code Playgroud)

希望有帮助。


jos*_*olf 7

这里发生了一些事情,所以让我们尝试将其分解。

每次值更改都被要求的最初尝试onDimmerChanged看起来很棒!

看看第二次尝试,创建自定义Slider组件是可行的,但存在一些问题。

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}
Run Code Online (Sandbox Code Playgroud)

让我们讨论一下Slider可组合项中发生的情况:

  1. 我们记住的状态是initialValue
  2. channel被更新、LightView被重组,也是如此Slider
  3. 由于我们记住了状态,因此它仍然设置为之前的状态initialValue

remember你认为自己是这里的罪魁祸首的想法是正确的。当记住一个值时,除非我们告诉 Compose,否则在重组时它不会被更新。但是如果没有记忆(如您的第三次尝试所示),每次var value by mutableStateOf(initialValue)使用initialValue. 由于每次更改都会触发重组value,因此我们始终将 而不是更新后的值传递initialValue给 Slider,导致它永远不会通过此可组合项中的更改进行更新。相反,我们希望将initialValue其作为键传递给remember,告诉 Compose 在键发生变化时重新计算该值。

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}
Run Code Online (Sandbox Code Playgroud)

您也可以将其拉Slider入您的LightView

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    var value by remember(channel.sliderValue) { mutableStateOf(channel.sliderValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { onDimmerChanged(value) }
    )
}
Run Code Online (Sandbox Code Playgroud)

最后,关于@commonsware 建议的尝试。

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    val sliderState = mutableStateOf(sliderValue)
    Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}

@Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
    Slider(
        value = state.value,
        onValueChange = { state.value = it },
        onValueChangeFinished = valueSet
    )
}
Run Code Online (Sandbox Code Playgroud)

这是做同样事情的另一种方式,但传递MutableStates 是一种反模式,如果可能的话应该避免。这就是状态提升有帮助的地方(您在之前的尝试中尝试做的事情:))


最后,关于Slider'sonValueChangeFinished使用错误的值。

而且它也不起作用。使用拖放时,sliderState 会正确更新,并且使用正确的值调用 onDimmerChanged()。但由于某种原因,当点击滑动(而不是滑动)时,调用 valueSet 并且 sliderState.value 不包含正确的值。我不明白这个值从哪里来。

这是组件中的一个错误Slider。您可以通过查看此代码的输出来检查这一点:

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    val key = Random.nextInt()
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { 
            valueSetter(value)
            println("Callback invoked. Current key: $key")
        }
    )
}
Run Code Online (Sandbox Code Playgroud)

由于key每次重新组合都会发生变化,因此您可以看到回调onValueChangeFinished包含对旧组合的引用(希望我说得对)。所以你没有发疯,这不是你的错:)

希望这有助于澄清一些事情!