我在viewmodel中存储了一个可变状态:

val state: MutableState<Int?> = mutableStateOf(null)

在我的compose UI中,我有一个LaunchedEffect监听状态变化:

LaunchedEffect(state.value) {
    while(state.value != null && state.value != 0) {
        ...do stuff
    }
}

当我用不同的valye更新状态值时,它会触发LaunchedEffect.

我试图实现的是用repeated状态值重新启动效果.

但这并不引起效果.

state.value = null
state.value = 30

而这将触发效果.

state.value = null
delay(100L)
state.value = 30

为什么我必须在这里延迟LaunchedEffect才能检测到变化?


编辑:

谢谢利维坦和查克的回答.现在更清楚了.

至于为什么我需要LaunchedEffect,我有一个可滚动的列表,每当pendingScrollState(来自业务逻辑)发生变化时,我需要以编程方式滚动:

LaunchedEffect(pendingScrollPosition.value) {
    while(pendingScrollPosition.value != null && pendingScrollPosition.value != 0) {
        when(pendingScrollPosition.value) {
            currentPosition.value -> {
                pendingScrollPosition.value // pending scroll finishes
            }

            else -> {
                lazyListState.animateScrollBy(  // animate scroll
                    value = calculatePendingScrollAmount(pendingScrollPosition.value)
                )
            }
        }
    }
}

有时动画滚动被用户手势取消,我需要恢复滚动.

因此,我试图重新启动效果,

val pendingScrollToResume = pendingScrollPosition.value
pendingScrollPosition.value = null
//delay(100L)
pendingScrollPosition.value = pendingScrollToResume 

它不会在没有延迟的情况下重新启动.

现在我对快照系统有了更好的了解,我知道我需要一个更可靠的解决方案.

我可以通过在LaunchedEffect中添加另一个键来解决这个问题,以判断滚动何时完成:

val isScrolling = remember {
    derivedStateOf {
        lazyListState.isScrollInProgress
    }
}
LaunchedEffect(key1 = pendingScrollPosition.value, key2 = isScrolling.value ) {
    while(!isScrolling.value && pendingScrollPosition.value != null && pendingScrollPosition.value != 0) {
        when(pendingScrollPosition.value) {
            currentPosition.value -> {
                pendingScrollPosition.value = null // pending scroll finishes
            }

            else -> {
                lazyListState.animateScrollBy(  // animate scroll
                    value = calculatePendingScrollAmount(pendingScrollPosition.value)
                )
            }
        }
    }
}

推荐答案

利维坦的回答比这个更好,你应该遵循其中的建议.然而,利维坦的回答并没有解释你所看到的行为的根本原因.这个答案试图更直接地回答你的问题(但不太有用).

首先,把key改为LaunchedEffect会"触发"emits 的影响的前提是错误的.该键旨在传达,如果该键改变,则应取消并重新启动LaunchedEffect.这应该用来反映效果捕获的状态,当捕获的状态不同时,重新启动效果.

一个很好的例子是material 3的date picker:

LaunchedEffect(lazyListState) {
    updateDisplayedMonth(
        lazyListState = lazyListState,
        ...
    )
}

lazyListState的值被lambda捕获,如果使用了新的lazyListState,则使用旧值的协程应该被取消,并且新的协程应该用新值启动.这是在合成中执行的,通常,每帧只执行一次,并且在帧的开始.

其次,快照不记录值序列,因此不应用于此.如果需要一个值序列,请使用Flow而不是快照状态.

快照,顾名思义,是特定时间点状态值的"快照".密码,

state.value = null
state.value = 30

在同一个快照中写入state.value两次.null立即被覆盖,而没有任何记录已被执行,并且没有通知发生了这种情况.就好像它从未发生过一样.

然而,写入state最终将触发另一个全局快照的创建,并通知Recomposer,任何读取值state的组合都需要重新组合.在重组开始时,将对该值和状态进行快照,在重组期间使用该点的值state.此时,它只看到值30,永远不会知道该值暂时是null.

delay(100L)似乎工作的原因是它足够长,以创建一个新的帧并完成重组(大约每16ms).然而,这并不能保证发生,因为系统中的帧速率可能不同(例如,可以是60Hz,120Hz或1Hz),并且根本不能保证发生(例如当电话Hibernate 时).例如,当具有可变刷新率的设备被降低到其通常为1Hz的"UI空闲率"时,delay(100L)就不够长.这意味着这个代码在Pixel Fold上无法正常工作.

合成的目的是将应用程序模型的当前状态转换为UI的当前状态.鉴于此,composition不应该写回用户模型,它应该只读取它.

任何使用composition来观察应用程序模型,而不是将模型转换为UI的当前状态,都会遇到摩擦,因为这不是composition的预期用途,因为框架所做的假设将不适合于更新UI以外的任何用途.

Kotlin相关问答推荐

用浮点数或十进制数给出错误答案的阶乘计算

如何访问方法引用的接收者?

关键字';在When Kotlin When-语句中

为什么Kotlin不用static inner class来实现带有object关键字的单例呢?

Jetpack BottomNavigation - java.lang.IllegalStateException:Already attached to lifecycleOwner

如何在 kotlin 中通过反射设置委托属性值?

从列表中的每个对象中 Select 属性

kotlin,如何从函数返回类类型

`this@classname` 在 Kotlin 中是什么意思?

如何在顶级函数中使用 koin 注入依赖项

在Kotlin中传递并使用函数作为构造函数参数

Kotlin:子构造函数如何使用其父构造函数的辅助构造函数?

如何解决:将Java类转换为Kotlin后出现error: cannot find symbol class ...?

编译错误:-Xcoroutines has no effect: coroutines are enabled anyway in 1.3 and beyond

Android Jetpack导航,另一个主机片段中的主机片段

类型不匹配推断类型为单位,但应为空

Mocked suspend函数在Mockito中返回null

@StringRes、@DrawableRes、@LayoutRes等android注释使用kotlin参数进行判断

用 kotlin 学习 Android MVVM 架构组件

任务':app:kaptDebugKotlin'的Kotlin执行失败