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.63k stars 1.14k forks source link

Desktop: Updating composable lambda from compose scope throw CompletionHandlerException #3715

Open G0xilla opened 10 months ago

G0xilla commented 10 months ago

Describe the bug When I update composable lambda from compose scope I get CompletionHandlerException

Affected platforms Select one of the platforms below:

Versions

To Reproduce Steps and/or the code snippet to reproduce the behavior:

fun main() {
    val content = MutableStateFlow<(@Composable () -> Unit)?>(null)

    fun updateContent() {
        // Simulate some time to update content
        Thread.sleep(1000)
        content.value = {
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier.fillMaxSize()
            ) {
                Text("Second Screen")
            }
        }
    }

    // init content
    content.value = {
        val scope = rememberCoroutineScope()

        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxSize()
        ) {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("First Screen")
                Button(
                    onClick = {
                        scope.launch { updateContent() }
                    }
                ) {
                    Text("Update Content")
                }
            }
        }
    }

    application {
        Window(onCloseRequest = ::exitApplication) {
            val contentState = content.collectAsState()
            contentState.value!!.invoke()
        }
    }
}

To reproduce behavior click Upadate Content several times in a row.

Expected behavior Normally open Second Screen without exception.

Screenshots

https://github.com/JetBrains/compose-multiplatform/assets/62667055/a9c6ebb4-140f-48ea-8cbf-57b797b0168f

dima-avdeev-jb commented 10 months ago

Also reproduces on MacOS: https://github.com/dima-avdeev-jb/issue-3715-exception-desktop and JDK 17

eymar commented 10 months ago

rememberCoroutineScope is tied to the particular point of composition.

Return a CoroutineScope bound to this point in the composition using the optional CoroutineContext provided by getContext. getContext will only be called once and the same CoroutineScope instance will be returned across recompositions. This scope will be cancelled when this call leaves the composition.

This scope will be cancelled

it can't be cancelled in your case because you use blocking Thread.sleep():

Running updateContent in scope.launch { updateContent() } still blocks the UI because rememberCoroutineScope will by default use the dispatcher of Recomposer.applier.

if you want to run a heavy task in onClick callback, you can use a different dispatcher:

val scope = rememberCoroutineScope {
            Dispatchers.IO // just an example
        }

or if you want to have a simple delay:


suspend fun updateContent() {
        // Simulate some time to update content
       delay(1000)
       ....
}

making any of these changes makes your code snippet work. (Prefer using suspend function if possible)

__

please let us know if this helps and if you have any more questions.

G0xilla commented 10 months ago

Yes, I should not do heavy task in Recomposer.applier, but if i do it should not end up with Exception. If you run same project on android, only UI will block and exception will not throw. Here is a example project for android. This issue can be duplicate of other issue.

eymar commented 10 months ago

@Gorry077 thank you!

okushnikov commented 3 weeks ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.