Cannot successfully init controller by ControllerFactory that returns a kotlin singleton object in javafx

Viewed 145

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?

2 Answers

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

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)
    
   }
}
Related