First of all, I'm using Kotlin in my JavaFX Project.
I'm trying to implement my own JavaFX Node that extends Canvas
.
class BrikkCanvas(width: Double, height: Double, private val rows: Int, private val cols: Int) : Canvas(width, height)
I also want to be able to add BrikkCanvas
directly in the FXML File like so
<BrikkCanvas fx:id="myCanvas" width="100.0" height="100.0" rows="1" cols="1" />
My class has no default constructor, that's why including it in FXML is not trivial.
I found out, however, that you can implement custom BuilderFactory
, so I did:
class BrikkCanvasBuilderFactory : BuilderFactory {
private val defaultBuilderFactory = JavaFXBuilderFactory()
override fun getBuilder(clazz: Class<*>): Builder<*> =
if (clazz == BrikkCanvas::class.java) BrikkCanvasBuilder()
else defaultBuilderFactory.getBuilder(clazz)
private class BrikkCanvasBuilder : Builder<BrikkCanvas> {
var width: Double = 0.0
var height: Double = 0.0
var rows: Int = 0
var cols: Int = 0
override fun build(): BrikkCanvas = BrikkCanvas(width, height, rows, cols)
}
}
In the App
class that extends Application
I use my BrikkCanvasBuilderFactory
like this:
fun startTheGame(playerName: String) {
val loader = FXMLLoader(App::class.java.getResource("game.fxml"), null, BrikkCanvasBuilderFactory())
val scene = Scene(loader.load())
val controller = loader.getController<GameController>()
primaryStage.scene = scene
}
However, when I start the application and click the button that invokes startTheGame
, I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
...
Caused by: javafx.fxml.LoadException:
/path to project/target/classes/game.fxml:21
at App$Companion.startTheGame(App.kt:18)
...
Caused by: java.lang.IllegalStateException: defaultBuilderFactory.getBuilder(clazz) must not be null
at controller.BrikkCanvasBuilderFactory.getBuilder(BrikkCanvasBuilderFactory.kt:12)
game.fxml:21
= fx:controller="controller.GameController"
App$Companion.startTheGame(App.kt:18)
= val scene = Scene(loader.load())
BrikkCanvasBuilderFactory.kt:12
= else defaultBuilderFactory.getBuilder(clazz)
Please note that I haven't even included any <BrikkCanvas ... />
tags in the FXML File yet
I'm pretty sure that I'm missing something really basic that probably has to do with the fact that I'm using Kotlin instead of Java
As I understand it, the BuilderFactory
and Builder
interfaces are relics of the past (essentially deprecated) and should be avoided in new code—pretend they don't exist. You should instead annotate your constructor parameters with the @NamedArg
annotation.
class BrikkCanvas(
@NamedArg("width") width: Double,
@NamedArg("height") height: Double,
@param:NamedArg("rows") private val rows: Int,
@param:NamedArg("cols") private val cols: Int
) : Canvas(width, height)
The FXMLLoader
will then be able to tell which FXML attribute corresponds with which parameter, and thus can set the properties via the constructor.