One Off the Slack: Why Do We Need rememberUpdateState()?
theapache64 asked:
Can anyone give me an example of
rememberUpdateState
? đ¤ Maybe âwith and withoutrememberUpdateState
â
what I canât understand is âhow would be the behaviour without [
rememberUpdateState()
]â.
for eg. hereâs a sample from official docs. Code A (official)
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
}
Code B
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
onTimeout()
}
}
both A and B produce the same output even if the LandingScreen recomposed more than one time, right?
Googleâs Doris Liu provided the answer:
LaunchedEffect
does an implicit capture of theonTimeOut
lambda. Without therememberUpdatedState
, itâll be invoking the potentially outdated lambda.
When Halil Ozercan pointed out that in some cases the one without rememberUpdateState()
seems to work, Doris replied:
It would work as long as thereâs no recomposition to the outer composable. That recomposition is when a new lambda may be recreated. But itâs best not to count on that. đ
theapache64 asked:
So Code A and B meant to produce different result if
onTimeout
changed during thedelay
?
Doris replied:
The result can be different. Or rather Code Bâs correctness isnât guaranteed.
theapache64 remained unconvinced and wanted an example that demonstrated an actual difference, so Halil offered:
fun main() = singleWindowApplication {
Top()
}
@Composable
fun Top() {
val counter by produceState(0) {
while(isActive) {
delay(10)
value += 1
}
}
val myLambda: () -> Unit = if (counter % 2 == 0) {
{ println("This is even: $counter") }
} else {
{ println("This is odd: $counter") }
}
Child(myLambda)
}
@Composable
fun Child(block: () -> Unit) {
LaunchedEffect(Unit) {
while (isActive) {
delay(100)
block.invoke()
}
}
}
âŚwhich results in:
This is even: 4
This is even: 12
This is even: 22
This is even: 29
This is even: 39
This is even: 48
This is even: 58
This is even: 67
This is even: 77
This is even: 87
This is even: 97
Halil elaborated:
Although I specifically change the lambda, it never registers the second one. Itâs always stuck with the first one.
When I use the
rememberUpdatedState
, these are the logs
This is odd: 3
This is even: 12
This is even: 22
This is even: 32
This is even: 42
This is even: 52
This is odd: 61
This is odd: 71
This is odd: 81
This is odd: 91
This is odd: 101
This is odd: 111
I also want to mention an additional case which opened my eyes about compose a little bit better. Letâs say you are passing a lambda downstream in your compose tree to fetch the latest state (never do this). If you are relying on the latest lambda, rather than the latest values that lambda reads in it; you are gonna get in trouble đ
Doris then tried to wrap up:
A summary of this: when you capture a lambda, you are also transitively capture everything that is in the lambda, including potentially stale states and whatever that is derived from those stale states.
Kefas remained confused:
@Composable
fun Screen() {
var number by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
delay(100L)
number = 1
}
Foo(number)
}
@Composable
fun Foo(number: Int) {
println("Recomposed with $number")
val currentNumber = number
Text(text = "$currentNumber")
LaunchedEffect(Unit) {
println("Start effect")
delay(200L)
println("currentNumber = $currentNumber")
}
}
Result
Recomposed with 0
Start effect
Recomposed with 1
currentNumber = 0
I still donât get it.
- Why the Text can get the latest value whereas the effect canât?
- If I use
rememberUpdatedState
instead, the effect can get the latest value. Why is that? Isnât the value itself already a state?
Halil attempted an explanation:
My understanding is as follows:
number
is a state inScreen
composable and when it changes, compose runtime knows exactly where to update. However, Effects are scheduled when composition happens for the first time. As your effect never gets invalidated, it has a stale version ofnumber
which iscurrentNumber
.currentNumber
is not a state in theFoo
composable. It resets at each recomposition but those recompositions doesnât really change anything for the scheduledLaunchedEffect
. Once thatLaunchedEffect
runs, it reads the value from first composition.
On the other hand, when you use
rememberUpdatedState
,number
becomes a state inFoo
composable context. Any effect that is scheduled fromFoo
will read the correct state value when itâs accessed.
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!