【问题标题】:Callback function can be changed in Compose?可以在 Compose 中更改回调函数吗?
【发布时间】:2022-07-06 04:21:56
【问题描述】:

当我在 Android 开发者网站上做this codelab(第 4 步)时,我注意到据说回调函数即使在传递给 Composable 之后也可以更改,并且代码需要保护它不被更改。如下:

LaunchedEffect 这样的一些副作用 API 将可变数量的键作为参数,用于在其中一个键更改时重新启动效果。你发现错误了吗?如果 onTimeout 发生变化,我们不想重新启动效果!

要在此可组合项的生命周期内仅触发一次副作用,请使用常量作为键,例如 LaunchedEffect(true) { ... }。但是,我们现在不保护 onTimeout 的更改!

如果在副作用进行时 onTimeout 发生了变化,则不能保证最后一个 onTimeout 在效果结束时被调用。要通过捕获和更新到新值来保证这一点,请使用rememberUpdatedState API:

代码:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState

@Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        // This will always refer to the latest onTimeout function that
        // LandingScreen was recomposed with
        val currentOnTimeout by rememberUpdatedState(onTimeout)

        // Create an effect that matches the lifecycle of LandingScreen.
        // If LandingScreen recomposes or onTimeout changes, 
        // the delay shouldn't start again.
        LaunchedEffect(true) {
            delay(SplashWaitTime)
            currentOnTimeout()
        }

        Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
    }
}

我对如何更改回调函数(在本例中为 onTimeout)感到困惑,因为代码没有对其进行任何修改。我的理解是,onTimeout 回调作为状态保存在内存中,当 Composable 退出组合时被忘记/删除,并在重组期间重新初始化,这意味着 change。因此,我们必须使用 rememberUpdatedState 来确保最后使用的 onTimeout(而不是一个空的 lambda,因为 Composable 不关心执行顺序)在重组期间传递给 LaunchedEffect 范围

不过,以上只是我的假设,因为我对这个话题还是新手。我已经阅读了一些文档,但仍然没有完全理解。如果我错了,请纠正我或帮助我以更平易近人的方式理解它。

提前致谢

【问题讨论】:

    标签: android kotlin callback state android-jetpack-compose


    【解决方案1】:

    在 Codelab 的示例中,提供的超时不会改变,但它是虚构的,因为它可能会改变许多默认的可组合使用 rememberUpdatedState这是

    @Composable
    fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
        mutableStateOf(newValue)
    }.apply { value = newValue }
    

    正如您在下面的问题和评论中一样,它也可以用作

        var currentOnTimeout by remember(mutableStateOf(onTimeout))
        currentTimeout = onTimeout
    

    它看起来不如 rememberUpdatedState 但两者都有效。这是您可以为同一目的选择的偏好或选项。

    Slider 和许多其他 Composable 也使用 rememberUpdatedState

    @Composable
    fun Slider(
        value: Float,
        onValueChange: (Float) -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
        /*@IntRange(from = 0)*/
        steps: Int = 0,
        onValueChangeFinished: (() -> Unit)? = null,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        colors: SliderColors = SliderDefaults.colors()
    ) {
        require(steps >= 0) { "steps should be >= 0" }
        val onValueChangeState = rememberUpdatedState(onValueChange)
    
    }
    

    即使您提供的onValueChange 函数在大多数情况下都不会更改,可能在少数情况下它可能需要更改,使用rememberUpdatedState 以确保将回调的最新值传递给Slider

    Slider(
        value = value,
        onValueChange = {
            value it
        }
    )
    

    在下面的示例中,当 showCalculation 为真时,Calculation2 Composable 进入组合并传递一个回调命名操作。如果您更改使用新操作回调重组的选择 Calculation2 函数,则会进行计算。如果您记住旧值而不是在延迟结束后更新它 LaunchedEffect 调用过时的操作,这就是您使用 rememberUpdatedState 的原因。因此,虽然函数重组它们接收到的回调可能会发生变化,但您可能需要使用最新的。

    /**
     * In this example we set a lambda to be invoked after a calculation that takes time to complete
     * while calculation running if our lambda gets updated `rememberUpdatedState` makes sure
     * that latest lambda is invoked
     */
    @Composable
    private fun RememberUpdatedStateSample2() {
    
        val context = LocalContext.current
    
        var showCalculation by remember { mutableStateOf(true) }
        val radioOptions = listOf("Option?", "Option?", "Option?")
    
        val (selectedOption: String, onOptionsSelected: (String) -> Unit) = remember {
            mutableStateOf(radioOptions[0])
        }
    
        Column {
    
            radioOptions.forEach { text ->
                Column(
                    modifier = Modifier.selectableGroup()
                ) {
                    Row(
                        Modifier
                            .selectable(
                                selected = (text == selectedOption),
                                onClick = {
                                    if (!showCalculation) {
                                        showCalculation = true
                                    }
                                    onOptionsSelected(text)
                                },
                                role = Role.RadioButton
                            )
                            .fillMaxWidth()
                            .padding(vertical = 4.dp)
                    ) {
                        RadioButton(selected = (text == selectedOption), onClick = null)
                        Spacer(modifier = Modifier.width(16.dp))
                        Text(text = text)
                    }
                }
            }
    
            Spacer(modifier = Modifier.height(12.dp))
    
            if (showCalculation) {
    
                println("? Invoking calculation2 with option: $selectedOption")
    
                Calculation2 {
                    showCalculation = false
                    Toast.makeText(
                        context,
                        "Calculation2 $it result: $selectedOption",
                        Toast.LENGTH_SHORT
                    )
                        .show()
                }
            }
        }
    }
    
    
    /**
     * LaunchedEffect restarts when one of the key parameters changes.
     * However, in some situations you might want to capture a value in your effect that,
     * if it changes, you do not want the effect to restart.
     * In order to do this, it is required to use rememberUpdatedState to create a reference
     * to this value which can be captured and updated. This approach is helpful for effects that
     * contain long-lived operations that may be expensive or prohibitive to recreate and restart.
     */
    @Composable
    private fun Calculation2(operation: (String) -> Unit) {
    
        println("? Calculation2(): operation: $operation")
        // This returns the updated operation if we recompose with new operation
        val currentOperation by rememberUpdatedState(newValue = operation)
        // This one returns the initial operation this Composable enters composition
        val rememberedOperation = remember { operation }
    
        // ? This LaunchedEffect block only gets called once, not called on each recomposition
        LaunchedEffect(key1 = true, block = {
            delay(4000)
            currentOperation("rememberUpdatedState")
            rememberedOperation("remember")
        })
    
        Row(verticalAlignment = Alignment.CenterVertically) {
            CircularProgressIndicator(color = getRandomColor())
        }
    }
    

    您还可以考虑更实际的情况,当您从远程服务器加载数据时,用户可能会像上面的示例那样与您的应用交互,并且您可能需要在加载完成后根据用户的最新交互进行导航。

    【讨论】:

    • 在您的第一个示例的计算中,我们为什么不将rememberSaveable 用于第一个变量,而对于第二个变量,我们为什么不能将input 作为@ 的键传递987654334@ 或在remember 内使用mutableStateOf(input)
    • rememberSavable 用于跨配置更改保留状态,而remember 用于跨重组保留。对于我所说的第二个问题,您可以使用 mutableState.value = newValue 或 rememberUpdatedState
    • 去掉了第一个例子以防止混淆,并添加了更多的实际场景和rememberUpdatedState的源代码
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-14
    • 2016-03-26
    • 1970-01-01
    • 1970-01-01
    • 2020-09-02
    • 2011-05-20
    相关资源
    最近更新 更多