compose-multiplatform: Crash when I use JFXPanel

I want to use JavaFX in Compose-jb, but Compose-jb only support swing, so I use JFXPanel

But, kotlin.UninitializedPropertyAccessException: lateinit property layout has not been initialized when I use JFXPanel

My Code

    @Composable
    private fun createMainSwingScreen() {
        SwingPanel(
            background = Color.White,
            modifier = Modifier.fillMaxSize(),
            factory = {
                JFXPanel().also { jfxPanel ->
                    PlatformImpl.startup {
                }
            }
        }
    )
}

Error log

    Exception in thread "AWT-EventQueue-0 @coroutine#5" kotlin.UninitializedPropertyAccessException: lateinit property layout has not been initialized
	at androidx.compose.desktop.ComponentInfo.getLayout(SwingPanel.desktop.kt:113)
	at androidx.compose.desktop.SwingPanel_desktopKt$SwingPanel-euL9pac$$inlined$onGloballyPositioned$1.onGloballyPositioned(OnGloballyPositionedModifier.kt:60)
	at androidx.compose.ui.node.LayoutNode.dispatchOnPositionedCallbacks$ui(LayoutNode.kt:1095)
	at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:51)
	at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
	at androidx.compose.ui.node.OnPositionedDispatcher.dispatch(OnPositionedDispatcher.kt:44)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks(MeasureAndLayoutDelegate.kt:253)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks$default(MeasureAndLayoutDelegate.kt:249)
	at androidx.compose.ui.platform.DesktopOwner.measureAndLayout(DesktopOwner.desktop.kt:227)
	at androidx.compose.ui.platform.DesktopOwner.render(DesktopOwner.desktop.kt:207)
	at androidx.compose.ui.platform.DesktopOwners.onFrame(DesktopOwners.desktop.kt:121)
	at androidx.compose.desktop.ComposeLayer$1.onRender(ComposeLayer.desktop.kt:123)
	at org.jetbrains.skiko.SkiaLayer.update$skiko(SkiaLayer.kt:117)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer.update(Direct3DRedrawer.kt:42)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer.redrawImmediately(Direct3DRedrawer.kt:37)
	at org.jetbrains.skiko.SkiaLayer$redraw$1.run(SkiaLayer.kt:82)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.desktop/java.awt.WaitDispatchSupport$2.run(WaitDispatchSupport.java:188)
	at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:235)
	at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:233)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:312)
	at java.desktop/java.awt.WaitDispatchSupport.enter(WaitDispatchSupport.java:233)
	at javafx.embed.swing.JFXPanel.initFx(JFXPanel.java:241)
	at javafx.embed.swing.JFXPanel.<init>(JFXPanel.java:267)
	at top.gtf35.nekohelper.ui.file_manager.FileManagerScreen$createMainSwingScreen$1.invoke(FileManagerScreen.kt:92)
	at top.gtf35.nekohelper.ui.file_manager.FileManagerScreen$createMainSwingScreen$1.invoke(FileManagerScreen.kt:91)
	at androidx.compose.desktop.SwingPanel_desktopKt$SwingPanel$4.invoke(SwingPanel.desktop.kt:84)
	at androidx.compose.desktop.SwingPanel_desktopKt$SwingPanel$4.invoke(SwingPanel.desktop.kt:83)
	at androidx.compose.runtime.DisposableEffectImpl.onRemembered(Effects.kt:81)
	at androidx.compose.runtime.ComposerImpl$RememberEventDispatcher.dispatchRememberObservers(Composer.kt:1344)
	at androidx.compose.runtime.ComposerImpl.applyChanges$runtime(Composer.kt:1409)
	at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:414)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:699)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:304)
	at androidx.compose.ui.platform.Wrapper_desktopKt.setContent(Wrapper.desktop.kt:39)
	at androidx.compose.desktop.ComposeLayer.initOwner(ComposeLayer.desktop.kt:257)
	at androidx.compose.desktop.ComposeLayer.access$initOwner(ComposeLayer.desktop.kt:50)
	at androidx.compose.desktop.ComposeLayer$Wrapped.init(ComposeLayer.desktop.kt:83)
	at org.jetbrains.skiko.HardwareLayer.checkIsShowing(HardwareLayer.kt:30)
	at org.jetbrains.skiko.HardwareLayer.access$checkIsShowing(HardwareLayer.kt:7)
	at org.jetbrains.skiko.HardwareLayer$1.hierarchyChanged(HardwareLayer.kt:22)
	at java.desktop/java.awt.Component.processHierarchyEvent(Component.java:6781)
	at java.desktop/java.awt.Component.processEvent(Component.java:6400)
	at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:4990)
	at java.desktop/java.awt.Component.dispatchEvent(Component.java:4822)
	at java.desktop/java.awt.Component.createHierarchyEvents(Component.java:5628)
	at java.desktop/java.awt.Container.createHierarchyEvents(Container.java:1466)
	at java.desktop/java.awt.Container.createHierarchyEvents(Container.java:1466)
	at java.desktop/java.awt.Container.createHierarchyEvents(Container.java:1466)
	at java.desktop/java.awt.Container.createHierarchyEvents(Container.java:1466)
	at java.desktop/java.awt.Container.createHierarchyEvents(Container.java:1466)
	at java.desktop/java.awt.Component.show(Component.java:1680)
	at java.desktop/java.awt.Window.show(Window.java:1056)
	at java.desktop/java.awt.Component.show(Component.java:1717)
	at java.desktop/java.awt.Component.setVisible(Component.java:1664)
	at java.desktop/java.awt.Window.setVisible(Window.java:1028)
	at androidx.compose.desktop.ComposeWindow.setVisible(ComposeWindow.desktop.kt:85)
	at androidx.compose.desktop.AppWindow.show(AppWindow.desktop.kt:446)
	at androidx.compose.desktop.AppWindow.show$default(AppWindow.desktop.kt:432)
	at top.gtf35.nekohelper.ui.file_manager.FileManagerScreen.dispatch(FileManagerScreen.kt:78)
	at top.gtf35.nekohelper.ui.file_manager.FileManagerScreen$dispatch$3.invoke(FileManagerScreen.kt)
	at top.gtf35.nekohelper.ui.file_manager.FileManagerScreen$dispatch$3.invoke(FileManagerScreen.kt)
	at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:91)
	at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2252)
	at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2499)
	at androidx.compose.runtime.ComposerImpl.recompose$runtime(Composer.kt:2625)
	at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:406)
	at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:724)
	at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:100)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:437)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:411)
	at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:41)
	at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:62)
	at androidx.compose.ui.platform.DesktopOwners.onFrame(DesktopOwners.desktop.kt:116)
	at androidx.compose.desktop.ComposeLayer$1.onRender(ComposeLayer.desktop.kt:123)
	at org.jetbrains.skiko.SkiaLayer.update$skiko(SkiaLayer.kt:117)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer.update(Direct3DRedrawer.kt:42)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer.access$update(Direct3DRedrawer.kt:11)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invokeSuspend(Direct3DRedrawer.kt:20)
	at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
	at org.jetbrains.skiko.FrameDispatcher$job$1.invokeSuspend(FrameDispatcher.kt:32)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 3
  • Comments: 15 (2 by maintainers)

Most upvoted comments

Hello! It looks like the initialization of the JFXPanel happens before the initialization of the SwingPanel, because both Swing and JavaFX have their own thread to dispatch events. So, if you try the following approach, it should solve your problem:

import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import java.awt.Container
import java.awt.BorderLayout
import javafx.embed.swing.JFXPanel
import javafx.application.Platform
import javafx.scene.Group
import javafx.scene.Scene
import javax.swing.JPanel
import javafx.scene.paint.Color as JFXColor
import javafx.scene.text.Font as JFXFont
import javafx.scene.text.Text as JFXText

fun main() = Window(
        title = "MyApp",
        size = IntSize(600, 550)
) {
    // JavaFX components
    val jfxpanel = remember { JFXPanel() }
    val jfxtext = remember { JFXText() }

    // The current container (depending on how you are using the CFD,
    // this could be ComposeWindow or ComposePanel)
    val container = LocalAppWindow.current.window // ComposeWindow

    val counter = remember { mutableStateOf(0) }
    val inc: () -> Unit = {
        counter.value++
        // update JavaFX text component
        Platform.runLater {
            jfxtext.text = "Welcome JavaFX! ${counter.value}"
        }
    }

    Box(
        modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp),
        contentAlignment = Alignment.Center
    ) {
        Text("Counter: ${counter.value}")
    }
        
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(
            modifier = Modifier.padding(top = 80.dp, bottom = 20.dp)
        ) {
            Button("1. Compose Button: increment", inc)
            Spacer(modifier = Modifier.height(20.dp))
            // The "Box" is strictly necessary to properly sizing and positioning the JFXPanel container. 
            Box(
                modifier = Modifier.height(200.dp).fillMaxWidth()
            ) {
                JavaFXPanel(
                    root = container,
                    panel = jfxpanel,
                    // function to initialize JFXPanel, Group, Scene
                    onCreate = {
                        Platform.runLater {
                            val root = Group()
                            val scene = Scene(root, JFXColor.GRAY)
                            jfxtext.x = 40.0
                            jfxtext.y = 40.0
                            jfxtext.font = JFXFont(25.0)
                            jfxtext.text = "Welcome JavaFX! ${counter.value}"
                            root.children.add(jfxtext)
                            jfxpanel.scene = scene
                        }
                    }
                )
            }
            Spacer(modifier = Modifier.height(20.dp))
        }
    }
}

@Composable
fun Button(text: String = "", action: (() -> Unit)? = null) {
    Button(
        modifier = Modifier.size(270.dp, 40.dp),
        onClick = { action?.invoke() }
    ) {
        Text(text)
    }
}

@Composable
public fun JavaFXPanel(
    root: Container,
    panel: JFXPanel,
    onCreate: () -> Unit
) {
    val container = remember { JPanel() }
    val density = LocalDensity.current.density

    Layout(
        content = {},
        modifier = Modifier.onGloballyPositioned { childCoordinates ->
            val coordinates = childCoordinates.parentCoordinates!!
            val location = coordinates.localToWindow(Offset.Zero).round()
            val size = coordinates.size
            container.setBounds(
                (location.x / density).toInt(),
                (location.y / density).toInt(),
                (size.width / density).toInt(),
                (size.height / density).toInt()
            )
            container.validate()
            container.repaint()
        },
        measurePolicy = { _, _ ->
            layout(0, 0) {}
        }
    )

    DisposableEffect(Unit) {
        container.apply {
            setLayout(BorderLayout(0, 0))
            add(panel)
        }
        root.add(container)
        onCreate.invoke()
        onDispose {
            root.remove(container)
        }
    }
}

Hi @aro311 @theone55

It seems that the problem was some ClassNotFoundException. However, Compose simply swallows any exception, which is why your app won’t crash and it would simply appear that the WebView didn’t load.

To fix that, I followed the tutorial at, Configuring included JDK modules | Native distributions & local execution | JetBrains/compose-jb, and as suggested by that tutorial, I ran the suggestModules gradle task on my project. It suggested that I add modules("java.instrument", "java.net.http", "jdk.jfr", "jdk.jsobject", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom") under compose.desktop.application.nativeDistributions.


Demo

Using the provided desktop-template, I made the following modifications to ./build.gradle.kts and ./src/main/kotlin/main.kt, and then I added a new file ./src/main/kotlin/JFXWebView.kt (sources below):

For ./build.gradle.kts:

import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
    kotlin("jvm")
    id("org.jetbrains.compose")
    id("org.openjfx.javafxplugin") version "0.0.13"
}

repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

javafx {
    version = "19"
    modules("javafx.swing", "javafx.web")
}

dependencies {
    // Note, if you develop a library, you should use compose.desktop.common.
    // compose.desktop.currentOs should be used in launcher-sourceSet
    // (in a separate module for demo project and in testMain).
    // With compose.desktop.common you will also lose @Preview functionality
    implementation(compose.desktop.currentOs)
}

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            modules("java.instrument", "java.net.http", "jdk.jfr", "jdk.jsobject", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom")
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "KotlinJvmComposeDesktopApplication"
            packageVersion = "1.0.0"
        }
    }
}

For ./src/main/kotlin/main.kt:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
     SwingPanel(
          factory = { JFXWebView() },
          modifier = Modifier.fillMaxSize(),
     )
}

For ./src/main/kotlin/JFXWebView.kt:

import javafx.application.Platform
import javafx.embed.swing.JFXPanel
import javafx.scene.Scene
import javafx.scene.web.WebView

/**
  * From, https://stackoverflow.com/a/26028556
  */
class JFXWebView : JFXPanel() {
     init {
          Platform.runLater(::initialiseJavaFXScene)
     }

     private fun initialiseJavaFXScene() {
          val webView = WebView()
          val webEngine = webView.engine
          webEngine.load("https://html5test.com/")
          val scene = Scene(webView)
          setScene(scene)
     }
}

Then run ./gradlew packageDistributionForCurrentOS, install the resulting native distribution, and upon running, you should get something like:

Capture


P.S. Not sure if we should also worry about firewall settings, which may prevent the JavaFX WebView from also working, but then other parts of your app that connects to the internet might also stop working.

Edit: The simplified JFXWebView class is not my original idea. Credits goes to Luke Quinane.