:question: Can anyone give me an example of `remem...
# compose
t
❓ Can anyone give me an example of
rememberUpdateState
? πŸ€” Maybe β€œwith and without
rememberUpdateState
”
πŸ‘ 5
a
There's this example in the docs: https://developer.android.com/jetpack/compose/interop/interop-apis#case-study-broadcastreceivers Ironically it's used wrong (reported it). In
onReceive
it should call
currentOnSystemEvent
instead of
onSystemEvent
.
In the example mentioned above
rememberUpdatedState
should be used because the parameter
onSystemEvent
can change after recomposition and you want the BroadcastReceiver to use the latest value without recreating it on recomposition
t
Yeah, i can understand the purpose of
rememberUpdateState
, but what I can’t understand is β€œhow would be the behaviour without it”. for eg. here’s a sample from official docs. Code A (official)
Copy code
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }
}
Code B
Copy code
@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?
a
I just saw a tweet explaining a particular use-case, lemme find it real quick
t
It must be from @Halil Ozercan
a
Yep
d
LaunchedEffect
does an implicit capture of the
onTimeOut
lambda. Without the
rememberUpdatedState
, it'll be invoking the potentially outdated lambda.
☝️ 4
☝🏻 1
a
It's because compose can't observe state changes inside a lambda, right?
h
@Doris Liu that's what I thought too but it apperantly works in some experiments πŸ˜„
d
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. πŸ™‚
t
@Doris Liu So Code A and B meant to produce different result if
onTimeout
changed during the
delay
?
d
The result can be different. Or rather Code B's correctness isn't guaranteed.
t
@Doris Liu I don’t get the β€œcan” be part. Would be very helpful if you can give a sample code with and without
rememberUpdatedState
that produce different output for each πŸ˜„
βž• 1
a
@Doris Liu That would a difficult thing to debug, is it possible to warn for incorrect behavior via lint checks?
h
Copy code
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()
        }
    }
}
πŸ‘ 3
result logs:
Copy code
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
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
Copy code
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
πŸ‘ 1
d
Nice! @Halil Ozercan A conditional lambda is what I had in mind as an example as well. πŸ™‚
h
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 πŸ˜„
just like in this example, β€’ condition wrapping the lambda: 🚫 β€’ lambda wrapping the condition: πŸ‘Œ
r
Here is a modified version of the
LandingScreen
demo where the lambda is never changed without the
rememberUpdatedState
call. Thanks @Halil Ozercan I think i'm getting the hang of it now.
Copy code
val SplashWaitTimeMillis = 5000L

@Composable
fun RememberUpdatedState() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        var callback by remember {
            mutableStateOf({
                println("Hello from initial callback")
            })
        }
        LaunchedEffect(Unit) {
            delay(2000)
            callback = {
                println("Hello from new callback")
            }
        }

        LandingScreen(callback)
    }
}

@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    // 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, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }
    /* Landing screen content */
}
βœ… 3
d
Another example would be that the lambda is capturing an object that becomes stale in the new composition:
Copy code
val textToDisplay = counter.toString()
val myLambda: () -> Unit = { textToDisplay }
Child(myLambda)
βœ… 1
a
Thanks for all the examples πŸ˜„
d
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.
βœ… 2
t
@Halil Ozercan @Doris Liu @Rafs @Aditya Wasan @Alex Gabor Thank you guys for the all the wonderful examples πŸ™‡ I was expecting the same output in my code, but I was always getting the latest lambda. possibly some bug in my code. The examples are working perfectly fine. thanks again guys
h
Hello again @theapache64 πŸ™‚ I just found a bug in RichText that's fixed with
rememberUpdatedState
. It's rather complicated but I'll try to simplify it as much as I can: β€’ Inline content composables in a
Text
needs to get their constraints before the whole text is measured. β€’ We pass these constraints using a lambda with a signature of
() -> Constraints
. β€’ This has never been a problem because constraints for text almost never change in Android. Even if they do, inline content is rarely used. β€’ Image is an inline content. Desktop windows are easily resizable. Images never get the updated constraints because lambda goes stale β€’ Adding rememberUpdatedState solves this problem.
this is the buggy one
and this one uses
rememberUpdatedState
t
@Halil Ozercan wow! makes sense πŸ‘
k
Copy code
@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:
Copy code
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?
h
@Kefas My understanding is as follows:
number
is a state in
Screen
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 of
number
which is
currentNumber
.
currentNumber
is not a state in the
Foo
composable. It resets at each recomposition but those recompositions doesn't really change anything for the scheduled
LaunchedEffect
. Once that
LaunchedEffect
runs, it reads the value from first composition. On the other hand, when you use
rememberUpdatedState
,
number
becomes a state in
Foo
composable context. Any effect that is scheduled from
Foo
will read the correct state value when it's accessed.
πŸ™ 2
@Kefas can you also send a State<Int> to
Foo
composable and read the currentNumber as
state.value
?
k
send a State<Int> to
Foo
composable and read the currentNumber as
state.value
?
@Halil Ozercan The result is 1 (the latest value)
h
https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn this post might be helpful to put everything in context
k
Thanks will definitely take a look. So that's why I see
State
objects are passed into parameters instead of its value.
h
I wouldn't recommend that approach tho. Try to not pass State objects from one place to another, state hoisting usually favors your first code example. In a way, that's why rememberUpdatedState exists. Be careful while reading values in Effects and passing around lambdas. That's it πŸ™‚