One Off the Slack: Why Do We Need rememberUpdateState()?

theapache64 asked:

Can anyone give me an example of rememberUpdateState? 🤔 Maybe “with and without rememberUpdateState”

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 the onTimeOut lambda. Without the rememberUpdatedState, 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 the delay ?

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.

Halil attempted an explanation:

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.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!