This post is dedicated to Tornado FX, which itself is built on JavaFX.
JavaFX
JavaFX started as a scripting language named JavaFX script. Sun Microsystems intended to use it to compete with Adobe Flex (now Apache Flex) and Microsoft Silverlight to a lesser extent.
In 2010, at Java One, Oracle, which had bought Sun in the meantime, announced that it would stop the development of the language while keeping the API. With Java 8 - released in 2014, JavaFX became the official successor of the Swing API: the latter just got bug fixes since then.
In the past JavaFX was included in the Oracle JDK up till version 11.
But it has always been a separate project and can also be installed separately from the JDK.
Compared to Swing, JavaFX adds an application abstraction. Here's an overview of the JavaFX API:
Besides, you can create a JavaFX user-interface by taking two different approaches:
- Either define all objects in pure Java code
- Or use XML-based layout files (FXML) that integrate with Java code
Here's a sample of the former and the latter for the same application.
Tornado FX
Kotlin allows improving a Java API to provide a better developers-experience. We could do that by ourselves. But the Tornado FX project already takes care of it.
Here's an a birds eye view of the API:
Tornado FX has a couple of benefits compared to plain JavaFX. Here are some of them.
Components and layouts DSL
Like Groovy, Kotlin allows to create usable DSLs. Unlike Groovy, the DSLs created are type-safe by default. You can find two of my previous experiment with DSLs;
- Kaadin, a DSL to create user interfaces with Vaadin
- An embryo of a DSL to configure Hazelcast
Likewise, Tornado FX provides all out-of-the-box components and layouts of JavaFX via a DSL. Here's an example of how it looks like:
vbox {
text("Name")
textfield()
button("Button").setOnAction {
println("Button pressed")
}
}
Some layouts allow for more complex configuration. For example, JavaFX offers a GridPane
layout, similar to AWT's GridbagLayout
. You need to pass the configuration as a GridPaneContraints
object for each laid out element. Here's a sample:
override val root = gridpane {
padding = insets(space)
textfield {
gridpaneConstraints {
columnIndex = 0
fillWidth = true
hGrow = Priority.ALWAYS
marginBottom = space
}
}
button("Button") {
gridpaneConstraints {
columnIndex = 1
hAlignment = HPos.RIGHT
marginBottom = space
}
}
textfield {
gridpaneConstraints {
rowIndex = 1
fillWidth = true
hGrow = Priority.ALWAYS
}
}
textfield {
gridpaneConstraints {
columnRowIndex(1, 1)
fillWidth = true
hGrow = Priority.ALWAYS
marginLeft = space
}
}
}
While it might seem not that readable, the IDE can be of tremendous help. With IntelliJ IDEA, you can fold the un-important bits:
I tend to prefer to create dedicated classes for each component when possible instead of using a generic class that is configured when instantiated. It doesn't work well with an existing DSL, as I need to supplement it with my own.
Controllers
Tornado FX's controllers implement the C part in the MVC pattern. They are responsible to encapsulate business logic. The UI thread should never run long-running tasks because it will make it unresponsive. Since controllers may execute such tasks, you should decide on a case-by-case basis. Finally, you can inject a controller (link:#dependency-injection[see below]) into other components as singletons.
Not that the API doesn't enforce any requirement. It's up to the developer to design controllers according to the above guidelines.
For example, here's a controller that fires an event when it received one of another kind:
class PathModelController : Controller() {
init {
subscribe<DirectoryPathUpdatedEvent> {
fire(PathModelUpdatedEvent(it.path))
}
}
}
Tornado FX's controllers are injectable into views. That approach couples the view to the logic. I'd rather have it the other way around: inject views into controllers. Thus, it would possible to reuse UI components with different logic. The existing design allows reusing the same logic within different UI components, which is much less frequent.
Dependency Injection
Tornado FX provides Dependency Injection. The API provides two ways to inject dependencies:
-
Use the
inject()
delegate:
class MyView: View("My View") { val myController: MyController by inject() val myController2 by inject<MyController>() }
-
Explicitly call the
find()
function:
class MyView: View("My View") { val myController = find(MyController::class) val myController2 = find<MyController>() }
Note that inject()
is available in View
and Controller
but you need to use find()
in other classes.
Event Bus
TornadoFX provides a singleton-scoped Event Bus. Its usage is nothing but classical.
Event classes must inherit from an FXEvent
superclass. TornadoFX requires to set the thread that manages the event, whether applicative or background. Long-running tasks should run on background threads.
Component
offers a fire()
function that will push the event to the bus. It also offers the register()
function that will notify about the reception of an event, filtered by type.
Here's how it looks like:
class FooEvent: FXEvent(BackgroundThread)
class BarEvent: FXEvent(BackgroundThread)
class Dummy {
init {
subscribe<FooEvent> {
println(it) // 1
}
}
fun bar() {
fire(BarEvent()) // 2
}
}
- Called when the Event Bus receives a
FooEvent
- Send a
BarEvent
Conclusion
After having developed just a simple demo application, I can form an opinion neither on JavaFX nor on Tornado FX. More experience is necessary. I like the embedded Event Bus but I dislike the design of the relationships between controllers and UI components.
In all cases, as mentioned in the preamble, Swing won't get any update anyway. Whether you like it or not, JavaFX is part of the available options.
Thanks to Hendrik Ebbers and Frank Delporte for their review of this post.
The complete source code for this post can be found on GitHub:
To go further:
Originally published at A Java Geek on January 31th 2021
Top comments (0)