2

I have a javafx project and I want to access the main controller by kotlin singleton reference.

But the injection cannot succeed.

A minimal example on the below:

App.kt

class App: Application() {
    override fun start(primaryStage: Stage) {

        val loader = FXMLLoader(javaClass.getResource("Window.fxml")).also { it.setControllerFactory { Controller } }
        val root = loader.load<Parent>()
        val scene = Scene(root, 400.0, 600.0)

        primaryStage.scene = scene
        primaryStage.show()
    }

    fun main(vararg args: String) {
        launch(*args)
    }
}

Controller.kt

object Controller : Initializable {

    @FXML private lateinit var button: Button
    @FXML fun buttonAction() { println("Clicked") }

    override fun initialize(location: URL?, resources: ResourceBundle?) {
        button.text = "Click Me"
    }
}

Window.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="Controller"
            prefHeight="400.0" prefWidth="600.0">
    <Button fx:id="button" onAction="#buttonAction"/>
</AnchorPane>

Then I got the Exception:

Exception in Application start method
Exception in thread "main" java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
    at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: javafx.fxml.LoadException: 
/D:/WorkPlace/Kotlin/LabelPlusFX/target/test-classes/window.fxml

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
    at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2435)
    at DemoApp.start(DemoApp.kt:16)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
    ... 1 more
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property button has not been initialized
    at Controller.initialize(Controller.kt:20)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2573)
    ... 12 more

Why cannot fxml do injection successfully?

  • [mcve] please .. – kleopatra Aug 23 '21 at 10:28
  • I don’t speak Kotlin, but it looks like you are trying to access `cMenuBar` in the constructor, which of course is before it’s initialised. The whole premise to this question seems wrong though. It doesn’t make sense to make a JavaFX controller a singleton. – James_D Aug 23 '21 at 11:49
  • @James_D No, the access of `cMenuBar` is in fun `initialize`. It works when I use `class` instead of `object` – Meodinger Wang Aug 23 '21 at 11:51
  • Well, whatever: `cMenuBar` is accessed on line 104 of Controller.kt, and it's null at that point. It's impossible to diagnose that without a [mre]. – James_D Aug 23 '21 at 11:55
  • @James_D Added minimal example – Meodinger Wang Aug 23 '21 at 12:06
  • Where? You haven't even included any code that would indicate `cMenuBar` is not null. What is `...` supposed to be? Please read the link again and understand what "reproducible" means. – James_D Aug 23 '21 at 12:07
  • @James_D I'm sure that the problem is not related to `cMenuBar`. I added three new code blocks that can reproduce the problem. – Meodinger Wang Aug 23 '21 at 12:09
  • *"I'm sure that the problem is not related to `cMenuBar`"*. But the stack trace explicitly tells you that's exactly the problem. – James_D Aug 23 '21 at 12:11
  • @James_D The problem casued by **some lateinit component**, in the minimal example the lateinit component is `button`, it occurs the same Exception but only the field name different. – Meodinger Wang Aug 23 '21 at 12:13
  • Ah, I see. The last three code blocks were supposed to replace the previous stuff. You should remove the code not related to your MRE, which is just confusing, and post the stack trace generated by your actual MRE. As to the actual problem: that's not clear to me but I suspect it's because I don't know enough about the lifecycle of Kotlin singletons. The JavaFX part of this is that the `FXMLLoader` will create a controller instance using your controller factory (which I think returns the singleton), and then should inject `@FXML`-annotated fields before calling `initialize`. – James_D Aug 23 '21 at 12:26
  • 1
    @James_D Thanks for the advice, I removed the useless code blocks. – Meodinger Wang Aug 23 '21 at 12:27
  • 1
    Actually, I suspect what happens here is that the fields in a Kotlin singleton are compiled to `static` fields in the Java class. The `FXMLLoader` uses reflection (of course) to initialized the `@FXML`-annotated fields, and only looks for instance fields (not static ones). See https://stackoverflow.com/questions/23105433/javafx-8-compatibility-issues-fxml-static-fields. It doesn't really make sense to have static `@FXML`-annotated fields (for the same reason it doesn't make sense to make your controller a singleton). – James_D Aug 23 '21 at 12:47
  • 2
    So I suppose a workaround is to make the controller a class, then define a singleton wrapper which has a single instance of the controller class. Define your controller factory to access the controller instance of the singleton. But again, I'd strongly caution you that creating a controller as a singleton is a really bad idea, and it's not clear why you'd want to do it. – James_D Aug 23 '21 at 12:50
  • @James_D OK. I'll improve my code. And I tried use reflection to see the attributes of `button`, it did have `static` attribute. Sounds that's the problem. Thank you! – Meodinger Wang Aug 23 '21 at 12:55

2 Answers2

0

Thanks to James_D.

The key to this problem is how kotlin compiles object singleton.

Kotlin chooses to make the singleton fields static, but FXMLLoader only looks for instance fields (not static) since JavaFX8. That's why cannot FXMLLoader does injection successfully.

Related Question

javafx-8-compatibility-issues-fxml-static-fields

0

Controller.kt

class Controller: View() {
   override val root: AnchorPane by fxml("/fxml/Window.fxml",hasControllerAttribute = true)
   @FXML
   lateinit var button: Button
   init{
      button.text = "Click me"
   }
   fun buttonAction(){
      println("Clicked")
   }
}

App.kt

class MyApp: App(Controller::class){
   override fun start(stage: Stage) {
      super.start(stage)
    
   }
}
  • Thank you for this code snippet, which might provide some limited, immediate help. A [proper explanation](https://meta.stackexchange.com/q/114762/349538) would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please [edit] your answer to add some explanation, including the assumptions you’ve made. – helvete Nov 29 '21 at 14:41