Search code examples
kotlinjavafxfxml

Why doesn't this Kotlin-backed FXML element get initialized or displayed?


I want to write reusable, composable JavaFX/FXML components in Kotlin. I am using Java 21 and my JavaFX is provided by gradle at version 22.0.1

My main class is loading the initial scene in a window via FXMLLoader.load and I see evidence of that. However, the skeleton custom component that is part of that scene graph is not displaying.

The custom component's controller's init {} does get invoked, but no combination of implementing/not implementing Initializable with/without arguments causes initialize() to be invoked. I have tried extending Control and VBox, and using fx:root and VBox for my root element.

How can I correct this? Is there a current and reasonably thorough resource that introduces FXML with Kotlin? I have tried everything I've found in the Oracle resources and 3rd-party blog posts that remotely matches my scenario, but it's not working.

This is the top-level scene, which does display in a window and I see "Test1".

// Launcher.fxml loaded via FXMLLoader.load in Main.kt

<?xml version="1.0" encoding="UTF-8"?>
<?package project.ui?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import project.ui.LauncherPanel?>
<VBox xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml"
      fx:controller="project.ui.Launcher"
      prefHeight="400.0" prefWidth="600.0">
    <Label>Test1</Label>
    <LauncherPanel/>
</VBox>

However, the inner element doesn't seem to be initialized and "Test2" is not displayed. I have tried using fx:root and just VBox as the root element.

// LauncherPanel.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?package project.ui?>

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

<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml"
        fx:controller="project.ui.LauncherPanel">
    <Label text="Test2"/>
</fx:root>

This code-behind does seem to be somewhat implicated, since its init {} gets called but its initialize() does not under any combination of advice I have applied. I have also tried adding @FXML to initialize(), and tried extending Control and VBox with and without implementing Initialize, with and without parameters to initialize, with both fx:root and VBox as the FXML root element, per various advice.

//LauncherPanel.kt

package project.ui

import javafx.fxml.Initializable
import javafx.scene.layout.VBox
import java.net.URL
import java.util.*

class LauncherPanel: VBox(), Initializable {
    init {
        println("init gets invoked")
    }
    
    override fun initialize(p0: URL?, p1: ResourceBundle?) {
        println("initialize is not invoked")
    }
}

Solution

  • FXML

    The definitive source on how FXML works is the Introduction to FXML document.

    There are also quite a few tutorials for FXML. One of them is the Oracle tutorial. That tutorial was written for JavaFX 8, but not much about FXML has changed since then so it's still applicable. The biggest change is what you need to do if deploying your application as a Java module. Another good tutorial is the JavaFX Tutorials on jenkov.com, which is actually linked to on https://openjfx.io.

    The Problem in Your Code

    When the FXMLLoader sees the <LauncherPanel/> element in the FXML file, it will simply try to instantiate the type by reflectively invoking one of its constructors; in this case it will be the no-argument constructor. But that's it. Nothing about that element or the LauncherPanel class indicates any additional FXML should be loaded. That means the class is not being instantiated as an FXML controller, let alone as an FXML root, and thus the initialize method is not being invoked.

    Reusable FXML Components

    There are two ways to create reusable FXML components: fx:root and fx:include.

    fx:root

    You use fx:root when you want to hide the use of FXML. You create a custom class which has its object graph defined in an FXML file, but code using the custom class does not know this. When you use this approach, you must:

    1. Have fx:root as the root element of the FXML file.

    2. Define the type attribute in the root element. The Introduction to FXML document sets the value of this attribute to the class which the custom class extends.

    3. Manually set the root of the FXMLLoader. If the controller should be the same instance as the root, then manually set the loader's controller as well; do not define the fx:controller attribute in this case. Otherwise, if the root and controller are different classes, then you can use fx:controller. Which approach you use is up to you.

    4. Load the FXML in the custom class's constructor.

    You then use the custom class like any other.

    Since this is the approach you attempt in your question, there is a full example of using fx:root below.

    fx:include

    Using fx:include is more straightforward. There's nothing special about the FXML file you include. It doesn't use fx:root, it can define an fx:controller attribute like normal, and you don't link it to a custom class. All you need is:

    <fx:include source="<path to reusable FXML file>"/>
    

    Whenever you want to nest one FXML file in another.

    Note this approach is not just for creating reusable FXML components. Sometimes you may simply want to split a monolith FXML file into multiple smaller FXML files for maintainability.

    Controller Initialization

    Note that the Initializable interface, while not technically deprecated, is obsolete. From its documentation:

    NOTE This interface has been superseded by automatic injection of location and resources properties into the controller. FXMLLoader will now automatically call any suitably annotated no-arg initialize() method defined by the controller. It is recommended that the injection approach be used whenever possible.

    That means the preferred approach looks like:

    import javafx.fxml.FXML
    import java.net.URL
    import java.util.ResourceBundle
    
    class Controller {
    
        @FXML private lateinit var location: URL
        @FXML private lateinit var resources: ResourceBundle
    
        @FXML
        private fun initialize() {
           // perform any needed initialization
        }
    }
    

    Both the location and resources properties and the initialize method are all individually optional. Only include them when you need them.

    Note if your controller is capable of working with a ResourceBundle but can also function when a bundle is not specified, then you should define the property like so:

    @FXML private var resources: ResourceBundle? = null
    

    Example - fx:root

    Here is a working example using fx:root with a Kotlin class, where the "root type" is used in another FXML file (like in your question).

    The example was developed using:

    • Java 22.0.1

    • JavaFX 22.0.1

    • Gradle 8.8

    • Windows 11

    Directory Layout

    <project-directory>
    |   build.gradle.kts
    |   gradlew
    |   gradlew.bat
    |   settings.gradle.kts
    |
    +---gradle 
    |   \---wrapper
    |           gradle-wrapper.jar
    |           gradle-wrapper.properties
    |           
    \---src
        \---main
            +---kotlin
            |       LauncherPanel.kt
            |       Main.kt
            |       
            \---resources
                    LauncherPanel.fxml
                    Main.fxml
    

    Kotlin Sources

    Main.kt

    package com.example.fxmlsample
    
    import javafx.application.Application
    import javafx.stage.Stage
    import javafx.fxml.FXMLLoader
    import javafx.scene.Scene
    import javafx.scene.Parent
    
    fun main(args: Array<out String>) {
      Application.launch(Main::class.java, *args)
    }
    
    class Main : Application() {
    
      override fun start(primaryStage: Stage) {
        val loader = FXMLLoader().apply {
          location = Main::class.java.getResource("/Main.fxml")!!
        }
        primaryStage.scene = Scene(loader.load<Parent>())
        primaryStage.title = "FXML Sample"
        primaryStage.show()
      }
    }
    

    LauncherPanel.kt

    package com.example.fxmlsample
    
    import javafx.scene.layout.StackPane
    import javafx.scene.control.Label
    import javafx.fxml.FXML
    import javafx.fxml.FXMLLoader
    
    class LauncherPanel : StackPane() {
    
      @FXML
      private lateinit var label: Label
    
      init {
        val loader = FXMLLoader().apply {
          location = LauncherPanel::class.java.getResource("/LauncherPanel.fxml")!!
          setRoot(this@LauncherPanel)
          setController(this@LauncherPanel)
        }
        loader.load<LauncherPanel>()
      }
    
      @FXML
      private fun initialize() {
        label.text = "Hello, from 'initialize'!"
      }
    }
    

    FXML Files

    Main.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import com.example.fxmlsample.LauncherPanel?>
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.Separator?>
    <?import javafx.scene.layout.VBox?>
    
    <VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
          spacing="10" alignment="TOP_CENTER" prefWidth="500" prefHeight="300">
      <padding>
        <Insets topRightBottomLeft="10"/>
      </padding>
      <Label text="FXML Sample"/>
      <Separator/>
      <LauncherPanel VBox.vgrow="ALWAYS"/>
    </VBox>
    

    LauncherPanel.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Label?>
    
    <fx:root type="javafx.scene.layout.StackPane" xmlns="http://javafx.com/javafx" 
        xmlns:fx="http://javafx.com/fxml">
      <Label fx:id="label"/>
    </fx:root>
    

    Gradle Files

    settings.gradle.kts

    rootProject.name = "fxml-sample"
    

    build.gradle.kts

    plugins {
      kotlin("jvm") version "2.0.0"
      id("org.openjfx.javafxplugin") version "0.1.0"
      application
    }
    
    group = "com.example"
    version = "0.1.0"
    
    repositories { 
      mavenCentral()
    }
    
    javafx {
      modules("javafx.controls", "javafx.fxml")
      version = "22.0.1"
    }
    
    application {
      mainClass = "com.example.fxmlsample.MainKt"
    }