androidkotlinandroid-jetpack-composeside-effects

When is a value captured by a LaunchedEffect?


I don't understand why the following two methods give different results:

@Composable
fun MainScreen2() {
    var counter by remember { mutableIntStateOf(0) }

    LaunchedEffect(key1 = Unit, block = {
        delay(5000)
        Log.i("Launcher1", "1: $counter")
    })

    Column {
        Log.i("Launcher", "column compose")
        Text(text = "Count: $counter")
        Button(onClick = { counter++ }) {
            Text(text = "ADD")
        }
        test2(count = counter)
    }
}

@Composable
fun test2(count: Int) {
    LaunchedEffect(key1 = true, block = {
        delay(5000)
        Log.i("Launcher", "2: $count")
    })
}

When I click the "ADD" button three times within one seconds, after 5 seconds Log 1 prints: 1: 3 and Log 2 prints 2: 0.


Solution

  • When MainScreen2 enters the composition the LaunchedEffect is executed. The block you provide captures all objects that are used in that block in a closure. It's like a static snapshot that won't be affected by changes that will happen while the block executes. In this case the only object captured is counter. When 5 seconds have passed it will print its content to the log.

    Now, by this reasoning it actuallly should always print 0: After all, counter is captured right on the first composition, before any buttons were pressed. And at that time it is always 0.

    What's going on here is that counter isn't actually an Int, it is a delegate to a MutableState that contains an Int. You created that delegate by using the by keyword in the declaration. What this means is that, although it looks like an Int, it is actually the State object itself, only that everytime you access the "Int", the getValue function is called on the State when you want to read it, and the setValue function is called on write access. Delegates are just synatax sugar provided by Kotlin to make your code look nicer.

    The LaunchedEffect actually captures the MutableState in its closure, and since you remembered that MutableState, it will always be the same on all recompositions. So when you click the button, the same MutableState that the LaunchedEffect captured is modified, so when 5 seconds have passed, the LaunchedEffect just reads the current value from that state and prints it.

    That's the reason why this even works in the first place.

    But that also explains why your second LaunchedEffect only prints 0: It doesn't capture a delegate to a MutableState, it captures the parameter count. And that is really and Int, not just a delegate to a MutableState that contains an Int. Since the second LaunchedEffect is never recomposed because you provided true as its key, it will only ever have the count parameter in its closure from when test2 entered the composition. At that time it was 0, so 0 is printed.

    If you would provide the second LaunchedEffect with count as its key, then every time that changes the block of the LaunchedEffect would be restarted and the then current count would be printed to the log. The 5 second delay will be also executed again, so you have to wait for 5 seconds after each change of count to see the log entry.