In the Test
Composable, action
is a variable of type mutableStateOf, initialized with ::a
. Then, in the LaunchedEffect, action is changed to ::b
.
However, because LaunchedEffect(Unit)
is by default bound to the Unit key in Compose, it means that it triggers when the Composable is first executed, and during this process, it captures a reference to perform the specified action
. As a result, changes to action
do not immediately reflect in ExecuteInLongLambda
because action was captured by Compose during the initial suspension.
After the delay
in LaunchedEffect
completes, even though action has been updated to ::b
, ExecuteInLongLambda
still executes the initially captured action (i.e., ::a
) from the first render, so ::a
is executed in the end. I know this should be solved using rememberUpdatedState()
, My doubt is that I created a lambda which capture State (i.e counter
) and pass the lambda as action2
to ExecuteInLongLiveLambda
. When counter changes, the lambda that captures counter should be recreated. Why does it behave differently from action1
? Shouldn't the printed counter be 0? Can someone explain in detail what is happening?
fun a() {
Log.e("MainActivity", "a")
}
fun b() {
Log.e("MainActivity", "b")
}
@Composable
fun Test() {
var action by remember { mutableStateOf(::a) }
var counter by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
delay(1000)
action = ::b
counter++
}
ExecuteInLongLiveLambda(
action1 = action,
action2 = {
// read State<Int>
Log.e("MainActivity", "counter = $counter")
}
)
}
@Composable
fun ExecuteInLongLiveLambda(
action1: () -> Unit,
action2: () -> Unit,
) {
LaunchedEffect(Unit) {
delay(2000)
action1() // a
action2() // counter = 1
}
}
Due to the delegation you only capture the underlying MutableState, not the Int. And this always stays the same object, only its content is changed. When you actually access the content (after two seconds), the value will be 1
, not 0
.
First composition
action
is set to ::a and counter
is set to 0.action1
: ::aaction2
: Captures counter
which is a MutableState with value 0So far so good. Now, after a second the LaunchedEffect from 2. sets action
to ::b and counter
to 1. Since both are backed by a MutableState that is accessed by Test
, a recomposition is triggered.
Second composition
action
and counter
are remembered they keep their current value (i.e. ::b and 1).Unit
) didn't change.action1
: ::baction2
: Captures counter
which is a MutableState with value 1This LaunchedEffect (the one from ExecuteInLongLiveLambda) never sees the two updated parameters. The coroutine that was internally launched on first composition is still running (still waiting for two seconds where only one has passed by now).
After another second passes the two seconds this LaunchedEffect was waiting are over and the rest from the lambda is executed. Keep in mind, this is the lambda with the parameters 3. from the first composition.
action1()
is executed. Since that was ::a when the LaunchedEffect was created, that is executed which logs a
.action2()
is executed. It didn't change during recomposition, so even when the LaunchedEffect hadn't captured it it would behave the same. The counter
may have changed, but the lambda is still the same.counter
was captured. And this is where it gets interesting: Due to the by
delegation this looks like an Int, but it actually is a MutableState. Under the hood the delegation calls getValue()
and setValue()
respectively everytime you access it. This means, what was really captured was the MutableState and that did never change. Only its value changed. But the first time that is accessed is when the lambda actually is executed. And the then current value is retrieved. At this point of time it is 1, not 0.Alternatives
This behavior changes if you actually capture the Int
. Let's assume you have an additional variable like this in Test
:
val counterValue = counter
When you access this counterValue
in the action2 lambda, then you will actually capture the Int
0 (not a MutableState with value 0), and that is also what will be logged later on.
Now, you might say that action
is also a delegate, like counter
, so that should behave the same way. That's not the case, though, because you never access action
in a lambda. You just pass it as a parameter to ExecuteInLongLiveLambda, and only that parameter is then captured in the ExecuteInLongLiveLambda's LaunchedEffect. ExecuteInLongLiveLambda never sees a MutableState, only ::a and ::b directly.