TornadoFX: EventBus

Reducing threading complexity between your views and controllers

mvndy
Kotlin Thursdays

--

In JavaFX, events can either take place in the foreground or the background; by default, these events are processed in the main thread. However, implementing long-running tasks on the JavaFX main thread, or the Application thread, can make an application UI unresponsive. It is recommended to send these tasks on one or more background threads and let the Application thread handle user events.

While there are different ways to handle multithreading from coroutines, RXJava, Async Tasks and more, TornadoFX does offer EventBus, which is excellent for reducing coupling between controllers and views by passing messages instead of having hard references to each other and without all the manual housekeeping. An EventBus is a mechanism that allows different components to communicate with each other without knowing about each other.

Hi folks! If the text is blurry, make sure the playback settings is at the highest resolution.

Resources

What I write won’t be original or revolutionary, but it’s always helpful to have more examples at hand, especially when they’re useful in real life projects!

TornadoFX documentation is pretty thorough and simple to follow! But if you have more questions, feel free to pop on the #TornadoFX channel on the KotlinLang slack. There are lots of friendly and helpful folks on the channel!

We’re going to take some inspiration from my TornadoFX-Suite app and make our own file reader and print out TornadoFX files into the console! It’s not a big deal to worry about sending file reading to a background thread, but it can slow down the UI thread significantly the more complicated it gets and the bigger the job is.

You can follow along with the project here:

Before the Glow Up

Alright, let’s take a look at what we got here.

In particular, we’re mostly concerned with our MainView and our MainController.

In our MainView, we have a console that will hold file text that is read. The button “Upload your project” will allow us to select a TornadoFX project and start reading the Kotlin files that are relevant to us through the walk method. We’re able to call walk thanks to dependency injection for the controller.

This leads us to our controller for MainView -

In our MainView, we have a console that will hold file text that is read. The button “Upload your project” will allow us to select a TornadoFX project and start reading the Kotlin files that are relevant to us through the walk method. We’re able to call walk thanks to dependency injection for the controller.

This leads us to our controller for MainView -

The Makeover

There’s not a lot going on in the controller method, but imagine if the processes got really heavy with every file with something like say, AST parsing. Let’s move the our file reads to into a background thread with FXEvent:

class ReadFilesRequest(val file: File) : FXEvent(EventBus.RunOn.BackgroundThread)

By default, the RunOn property parameter is set to the Application Thread, but we can also set it to BackgroundThread. That means it will be given a background thread by default, so that it can do heavy work without blocking the UI. Luckily for us, we can pass parameters for our FXEvent object as needed.

An EventBus doesn’t know where things are coming or going, but you need two components: a trigger and a listener. For us, we will trigger our event for reading files on the action for our button:

button("Upload your project.") {
setOnAction {
chooseDirectory {
title = "Choose a TornadoFX Project"
initialDirectory = File(System.getProperty("user.home"))
}?.let {
console.items.clear()
console.items.add("SEARCHING FILES...")
fire(ReadFilesRequest(it))
}
}
}

We can create a listener with a subscriber in the init {…} portion of our MainView class to trigger the walk event.

class MainView : View() {    init {
subscribe<ReadFilesRequest> { event ->
controller.walk(event.file.absolutePath)
}
}
...
}

Alright! We have sent this to the background thread. Let’s run this and see what happens:

JavaFX is lit up and it won’t calm down

It appears that we have sent our job back and tried to write our items back on to the console while we’re still in the background thread. This won’t work!

If we send a job for processing in a background thread, we will need to send the result back to the Application thread to be able to use it in the UI.

Let’s create another FXEvent type that intends on sending emitted event data back to the main thread:

class PrintFileToConsole(val file: String, val textFile: String): FXEvent()

As mentioned previously, the default parameter for RunOn is set to the Application Thread, so we don’t need to define anything here.

We can read and filter our files all the same, but all we really care about sending back to the file is when we want to print what we read to the console in our controller class:

private fun readFiles(file: File, path: String) {
val fileText = file.bufferedReader().use(BufferedReader::readText)


if (filterFiles(fileText)) {
fire(PrintFileToConsole(file.toString(), fileText))
}
}

As always, if we have a trigger, we’ll need a listener. Now that we’re sending our event data back to the main thread, we can do a neat trick by subscribing right in our view!

override val root = stackpane {
...
console = listview {
items.add(consolePath)
subscribe<PrintFileToConsole> { event ->
items.add(consolePath + event.file)
items.add(event.textFile)
items.add("========================="")
}
...
}

Our console will be updated whenever the system emits new data from the files that are read, no matter where it came from or when!

Hope you enjoyed this post! If you’d like to see the full example of the code, you can see it here:

--

--

mvndy
Kotlin Thursdays

software engineer and crocheting enthusiast. co-author of "Programming Android with Kotlin"