Fragmentcomponents. While the
Sceneare used by TornadoFX, the
Fragmentintroduces new concepts that streamline development. Many of these components are automatically maintained as singletons, and can communicate to each other through TornadoFX's simple dependency injections or direct instantiation.
Appto create an entry point that launches a TornadoFX application.
App. An App is the entry point to the application and specifies the initial
View. It does in fact extend JavaFX
Application, but you do not necessarily need to specify a
Appto create your own implementation and specify the primary view as the first constructor argument.
Stage. It is automatically managed as a singleton. When you declare a
Viewyou must specify a
rootproperty which can be any
Parenttype, and that will hold the View's content.
View. Override the abstract
rootproperty and assign it
VBoxor any other
tornadofx.*import. This is important and should be present in all your TornadoFX related files. The reason this is important is that some of the functionality of the framework isn't discoverable by the IDE without the import. This import enabled some advanced extension functions that you really don't want to live without :)
main()method. A JavaFX application, and by extension a TornadoFX application, is any class that extends
javafx.application.Application, TornadoFX apps are no different. Therefore you would start the app by referencing
com.example.app.MyApp, and you do not necessarily need a
main()function (unless you need to supply command line arguments). In that case you would add a package level main function to the
com.example.app.MyAppKt. Notice the
Ktat the end. When you create a packagelevel main function, it will always have a class name of the fully qualified package, plus the file name, appended with
App, we will use Intellij IDEA. Navigate to Run→Edit Configurations (Figure 3.1).
Appclass. You will also need to specify the module it resides in. Give the configuration a meaningful name such as "Launcher". After that click "OK" (Figure 3.3).
Viewworks and how it can be invoked. A
Viewcontains a hierarchy of JavaFX Nodes, starting from the root. In the next section we will learn how to leverage the builders to create these
Nodehierarchies quickly. There is only one instance of
MyViewmaintained by TornadoFX, effectively making it a singleton. TornadoFX also supports scopes, which can group together a collection of
Controllers in separate instances, resulting in a
Viewonly being a singleton inside that scope. This is great for Multiple-Document Interface applications and other advanced use cases.
View. Below we embed a
MasterView. We assign these sub views to sections inside the
inject()delegate will lazily assign a given component to a property. The first time that component is called is when it will be retrieved. Alternatively, instead of using the
inject()delegate you can use the
find()function to retrieve a singleton instance of a
Viewor other components. In the following example we retrieve an instance of the
find()respectively, and assign the root element of those sub views to the sections inside the
BorderPane. This is normally not necessary but might help you understand how the above short hand works under the covers.
inject(), but using
inject()delegates is the most idiomatic means to perform dependency injection and has the advantage of lazy loading.
There are other flavors of MVC like MVVM and MVP, all of which can be leveraged in TornadoFX.
Controllercan be injected to support a
TextFieldwhose value is bound to an observable string property and later written to a "database" when a
Buttonis clicked. We can inject a
Controllerthat handles interacting with the model that writes to the database. Since this example is simplified, there will be no database but a printed message will serve as a placeholder (Figure 3.7).
inputproperty is automatically bound to the textfield just by referencing it in the
textfieldbuilder. As an alternative, you could have created a reference to the textfield and retrieved its
textproperty, but that approach would create more complex UI code without much benefit. In rare cases you might need to reference individual UI elements.
inputFieldso that it can be referenced from the
onClickevent handler of the "Commit" button later, hence why we save it to a variable. When the "Commit" button is clicked, you will see the Controller prints a line to the console.
ViewModellater, which provides even easier ways to deal with this type of interaction.
ListView, and the
itemsproperty of the
ListViewis assigned to the
valuesproperty of our
runAsyncfunction to help with this.
runAsyncblock will run in the background. If the result of the background call should update your UI, you must make sure that you apply the changes on the JavaFX Application Thread. The
uiblock, which typically follows, does exactly that.
actionbuilder is run. It makes a call out to
myController.loadText()and applies the result to the text property of the textfield when it returns. The UI stays responsive while the controller function runs.
runAsynccreates a JavaFX
Taskobject, and spins off a separate thread to run your call inside the
Task. You can even assign this
Taskto a variable and bind it to a UI to show progress while your operation is running.
TaskStatuswhich contains observable values for
progress. You can supply the
runAsynccall with a specific instance of the
TaskStatusobject, or use the default. There is also a version of
runAsyncWithProgresswhich will cover the current
Nodewith a progress indicator while the long running operation runs.
Viewyou create is a singleton, which means you typically use it in only one place at a time. The reason for this is that the root node of the
Viewcan only have a single parent in a JavaFX application. If you assign it another parent, it will disappear from its previous parent.
Fragment. A Fragment is a special type of
Viewdesigned to have multiple instances. They are particularly useful for popups or as pieces of a larger UI (such as ListCells, which we look at via the
openInternalWindow()functions that will open the root node in a separate Window.
openModal()to modify a few of its behaviors
ESCkey to call
openModalopens in a new
openInternalWindowopens over the current root node, or any other node if you specify it:
InternalWindow.Stylesclass for more information about styleable properties.
openInternalWindow()from within the
Viewyou want it to open on top of. You supply the View you want to show, and you can optionally supply what node to open over via the
ESCkey to call
openInternalWindow()can be closed by calling
close(). It is also possible to get to the
InternalWindowinstance directly if needed using
replaceWith(), and optionally add a transition. In the example below, a
Viewwill switch to the other view, which can be
Nodeon a given
root. There are two functions you can override on
Viewto leverage when a View's
Nodeis connected to a parent (
onDock()), and when it is disconnected (
onUndock()). You can leverage these two events to connect and "clean up" whenever a
Viewcomes in or falls out. You will notice running the code below that whenever a
Viewis swapped, it will undock that previous
Viewand dock the new one. You can leverage these two events to manage initialization and disposal tasks.
ViewModel. Even so, it can still be convenient to be able to pass parameters to other components. The
injectfunctions supports varargs of
Pair<String, Any>which can be used for just this purpose. Consider a customer list that opens a customer editor for the selected customer. The action to edit a customer might look like this:
tosyntax to create the parameter. This could also have been written as
Pair(CustomerEditor::customer, customer)if you prefer. The editor can now access the parameter like this:
mapOf("customer" to customer), but then you miss out on automatic refactoring if you rename a property in the target view.
Viewhas a property called
primaryStagethat allows you to manipulate properties of the
Stagebacking it, such as window size. Any
Fragmentthat were opened via
openModalwill also have a
Fragment. This can be achieved with
root.scene, or if you are within a type safe builder, just call
toExternalFormof a URL. To retrieve a resource url one would typically write something like:
resourcesobject which can retrieve the external form url of a resource like this:
URL, it can be retrieved like this:
resourceshelper also has several other helpful functions to help you turn files relative to the
Componentinto an object of the type you need:
It is worth mentioning that the
jsonArrayfunctions are also available on
Componentbut you can also retrieve a resource by it's full path, starting with a
Fragment. While the applications we built so far are pretty simple, hopefully you appreciate the simplified concepts TornadoFX introduces to JavaFX. In the next chapter we will cover what is arguably the most powerful feature of TornadoFX: Type-Safe Builders.