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")
}
}
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.
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.
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:
Have fx:root
as the root element of the FXML file.
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.
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.
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.
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
andresources
properties into the controller.FXMLLoader
will now automatically call any suitably annotated no-arginitialize()
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
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
<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
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'!"
}
}
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>
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"
}