The state of JVM desktop frameworks: TornadoFX

The state of JVM desktop frameworks: TornadoFX

The two previous posts of this series were respectively dedicated to Swing and SWT. This post is dedicated to Tornado FX, which itself is built on JavaFX.

  1. The state of JVM desktop frameworks: introduction
  2. The state of JVM desktop frameworks: Swing
  3. The state of JVM desktop frameworks: SWT

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.

-- From Starting a JavaFX Project with Gluon Tools

Compared to Swing, JavaFX adds an application abstraction. Here's an overview of the JavaFX API:

Overview of the JavaFX API

Besides, you can create a JavaFX user-interface by taking two different approaches:

  1. Either define all objects in pure Java code
  2. 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:

Overview of the Tornado FX 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;

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:

Screenshot of IntelliJ IDEA folding feature

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:

  1. Use the inject() delegate:

     class MyView: View("My View") {
       val myController: MyController by inject()
       val myController2 by inject<MyController>()
     }
    
  2. 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
  }
}
  1. Called when the Event Bus receives a FooEvent
  2. 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