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.53k stars 1.13k forks source link

Swing/JavaFx interop, container has not been initialized when added later #4642

Open jbruchanov opened 3 months ago

jbruchanov commented 3 months ago

Describe the bug App crash due to

UncaughtException: lateinit property container has not been initialized -> kotlin.UninitializedPropertyAccessException: lateinit property container has not been initialized

Affected platforms

Versions kotlin = "1.9.23" jetbrains-compose-ui = "1.6.1", "1.6.2", "1.6.10-dev1584 jvmtarget = "17" os: windows11

run following code on jvmDesktop

fun testStart() = application {
    val screenSize = Toolkit.getDefaultToolkit().screenSize
    val dpSize = (screenSize.height.dp * 0.8f)
        .let { h -> DpSize(h * (21 / 9f), h) }

    Window(
        onCloseRequest = ::exitApplication,
        title = "Sample App",
        state = rememberWindowState(size = dpSize),
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            var visible by remember { mutableStateOf(false) }
            Checkbox(checked = visible, onCheckedChange = { visible = it })
            if (visible) {
                SwingPanel(
                    modifier = Modifier.fillMaxWidth().weight(1f),
                    factory = {
                        JFXPanel().apply {
                            val stackPane = StackPane()
                            val scene = Scene(stackPane)
                            stackPane.children.add(Label("Test"))
                            setScene(scene)
                        }
                    }
                )
            }
        }
    }
}

having visible true by default (on start) works fine and the swing panel is rendered as expected, then uncheck -> check -> crash again

Problem starts here SwingPanel.desktop.kt#L165 when calling factory() Simple stack

factory()
-> calls my factory -> JFXPanel() 
-> then SwingPanel.desktop.kt in modifier.onGloballyPositione is accessing `componentInfo.container`, but that is not assigned yet, because we are still at point of calling/getting `factory()` result

might be related/similar to: https://github.com/JetBrains/compose-multiplatform/issues/519

compose_swing_stack.txt

Mett-Barr commented 2 weeks ago

I found a solution:

val fakeJFXPanel = JFXPanel()
fun main() = application {
    ...
}

I used a global variable to ensure that JFXPanel is initialized at the very beginning. This prevents the subsequent calls in the @Composable function from encountering the lateinit property component has not been initialized issue.

fun ComposeSwing(modifier: Modifier = Modifier) {
    SwingPanel(factory = { JFXPanel() })
}

This is definitely not a standard solution; it's quite a hack and there is certainly room for optimization. However, it solved my current issue and I hope it helps you too.

okushnikov commented 1 week ago

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