JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.24k stars 1.11k forks source link

On JS and Wasm (and maybe iOS), when depending a `val` storing the state value outside in a `NavHost` `composable` lambda, the updated state is not propagated #4966

Open ShreckYe opened 1 week ago

ShreckYe commented 1 week ago

Describe the bug

See the code below:

val (count, setCount) = remember { mutableStateOf(0) }
val navController = rememberNavController()
val clickToIncCountModifier = Modifier.onClick {
    setCount(count + 1)
}

Column {
    BasicText("Count outside: $count", clickToIncCountModifier)
    NavHost(navController, "0") {
        repeat(4) { i ->
            composable(i.toString()) {
                Column {
                    BasicText("Screen $i")
                    BasicText("Count in `NavHost`: $count", clickToIncCountModifier)
                }
            }
        }
    }
}

Run the app on Wasm, and click the "count" texts to increment the text. If you click the text outside the NavHost, the value is correctly incremented; if you click the text inside, the value is always reset to 1 which is the original value 0 incremented. However, If I use MutableState.value directly in the lambda or use delegated properties for the state instead, it works. Also both texts increment and show the count correctly on JVM desktop.

After going through a bunch of Jetpack Compose docs and experimenting with the code myself, I suspect that this is a bug with the Compose compiler and the navigation library combined, firstly because it works on JVM desktop but not on JS and Wasm, and secondly because I see that lambdas are stable, so they are immutable and essentially closures wrapping val count and their recomposition is skipped, and while the compiler may have adopted some mechanism similar to rememberUpdatedState on desktop, it doesn't do it on JS or Wasm. However, LazyColumn has a similar style of adding composable lambdas in a non-composable lambda, but doesn't have this bug as I have tested. Nevertheless, These are merely my thoughts and of course you guys are the experts, so correct me if I am wrong.

I came across this problem when I was trying to port a small portion of Compose navigation to Compose HTML. And I spent quite some time debugging this. If you are interested in more details, see the commit messages of the latest 4 commits here.

Affected platforms

Versions

To Reproduce

See the code and instructions above.