Introduction
After literally a decade writing JavaFX applications, it can be hard to remember how I thought things were supposed to be done when I started out. Thankfully, there seems to be a nearly endless supply of beginners having trouble, posting their code and giving us a glimpse into their thought processes.
In this article we are going to look at some beginner code that really highlights how much more complicated JavaFX is when you don’t know a lot about it.
Even after getting this code to compile and run, it’s still not clear to me exactly what it’s supposed to do. In general terms, it displays a screen with a title and three Buttons
, one of which is supposed to hide some of the Buttons
while displaying a Label\TextField\Button
combination to allow some user entry. This data entry display also contains a “Back” Button
to go back to the original, three Button
display.
While the general idea of the application is easy to figure out, details of how the screens are supposed to look are a bit murky because the approach taken, and the fact that it wouldn’t even compile in its original state. I’ve taken a “best try” approach when refactoring it to come up with something that seems to hold to the original spirit, if not the fine details of the intended layout.
My version is in Kotlin. It’s pretty small, so you shouldn’t have much trouble following it and the main point of this article is to highlight general concepts and JavaFX techniques - none of which are language dependent.
The Code
public class opening extends Application {
// first scene (buttons
Label gameTitle = new Label("Basil's Cat Game :3");
// new game buttons
TextField putNameHere = new TextField();
Label putNameTxt = new Label("Name:");
Button nameOkButton = new Button("OK");
VBox titleVBox = new VBox();
HBox titleHBox = new HBox();
GridPane titleGroup = new GridPane();
Button backBtn = new Button("BACK");
VBox title = new VBox();
Button exitBtn = new Button();
Group titleGroupSec = new Group();
boolean buttonClicked = false;
@Override
public void start(Stage primaryStage) throws Exception {
// needs buttons to select what to do and also a text field to store a name
gameTitle.setFont(Font.font(STYLESHEET_CASPIAN, 40));
Button playBtn = new Button("NEW");
playBtn.setFont(Font.font(30));
playBtn.setBackground(Background.EMPTY);
Button loadBtn = new Button("LOAD");
loadBtn.setFont(Font.font(30));
loadBtn.setBackground(Background.EMPTY);
Button exitBtn = new Button("EXIT");
exitBtn.setFont(Font.font(30));
exitBtn.setBackground(Background.EMPTY);
Button exitBtnSec = new Button("EXIT");
exitBtnSec.setFont(Font.font(30));
exitBtnSec.setBackground(Background.EMPTY);
VBox btnVBox = new VBox();
btnVBox.getChildren().add(0, gameTitle);
btnVBox.getChildren().add(1, playBtn);
btnVBox.getChildren().add(2, loadBtn);
btnVBox.getChildren().add(3, exitBtn);
btnVBox.setAlignment(Pos.TOP_CENTER);
btnVBox.setSpacing(20);
titleGroup.getChildren().remove(titleHBox);
titleGroup.getChildren().remove(titleVBox);
titleGroup.getChildren().addLast(btnVBox);
titleGroup.setAlignment(Pos.TOP_CENTER);
titleGroup.setPadding(new Insets(50));
/**
* maybe should change to say "NEW"? anyways, make them pick a name (move name
* feature to here & show a quick text based tutorial that does some waiting
* liek im sayin it to the player :3
*
* make them roll for a cat nd name the cat. save the cat and it's features to a
* file that is specific to this player.
*
* make it so they can access a list to see player info (name) and cat info(how
* many cats, names, breeds, traits, colors)
*/
playBtn.setOnMouseClicked(playBtnClickedEvent -> {
btnVBox.getChildren().remove(loadBtn);
btnVBox.getChildren().remove(exitBtn);
btnVBox.getChildren().remove(playBtn);
exitBtn.setAlignment(Pos.TOP_LEFT);
gameTitle.setFont(Font.font(STYLESHEET_CASPIAN, 40));
putNameTxt.setFont(Font.font(STYLESHEET_CASPIAN, 20));
putNameHere.setEditable(true);
nameOkButton.setFont(Font.font(10));
title.getChildren().add(putNameTxt);
title.getChildren().addLast(exitBtn);
title.getChildren().addFirst(gameTitle);
title.setSpacing(10);
title.setAlignment(Pos.TOP_CENTER);
titleHBox.getChildren().addFirst(putNameHere);
titleHBox.getChildren().addLast(nameOkButton);
titleHBox.setPadding(new Insets(100));
titleHBox.setAlignment(Pos.CENTER);
titleGroup.getChildren().addFirst(backBtn);
titleGroup.getChildren().add(title);
titleGroup.getChildren().addLast(titleHBox);
});
/**
* here, u will probably have to implement something more complex than a file
* writer if you decide to use photos or graphics to make ur kitties :P
*
* call the names of players (only players that have pressed the PLAY button &
* gotten a cat.. show the amt of cats they have next to their name :P)
*
* if they click on the button that has their name/amt of cats then make open
* that save file in the stage
*/
loadBtn.setOnMouseClicked(loadBtnClickedEvent -> {
});
exitBtn.setOnMouseClicked(exitBtnClickedEvent -> {
primaryStage.close();
});
Scene scene = new Scene(titleGroup, 500, 500);
primaryStage.setScene(scene);
primaryStage.setTitle("love u <3");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The Fundamentals
Breaking the Single Responsibility Principle (SRP)
SRP and DRY (see below) are the two foundational principles for writing good code. Yet beginners violate this all the time.
Every block of code should be directly responsible for one thing, and one thing only. Every method should be directly responsible for one, and one thing only. Every class should be directly responsible for one thing, and one thing only. Do you see a pattern here?
In this program we have one class that does everything, and one 130 line method that does almost everything. This is a clear indication that SRP has been violated.
Breaking “Don’t Repeat Yourself” (DRY)
Take a look at this snippet from the code:
Button playBtn = new Button("NEW");
playBtn.setFont(Font.font(30));
playBtn.setBackground(Background.EMPTY);
Button loadBtn = new Button("LOAD");
loadBtn.setFont(Font.font(30));
loadBtn.setBackground(Background.EMPTY);
Button exitBtn = new Button("EXIT");
exitBtn.setFont(Font.font(30));
exitBtn.setBackground(Background.EMPTY);
Here we have the same three lines of code repeated three times. The only differences are in the variable that is created and the text in the Button
.
Why is this bad? First, it clutters things up. There’s now 9 lines of code that you have to read carefully to determine that all three Buttons
are essentially configured the same. Secondly, if you want to change something, you now have to do it 3 times.
Finally, it’s error prone. One of the most common places for making silly mistakes that way more time to track down than you’d expect comes from repeated blocks of code. Take a look at this:
Button playBtn = new Button("NEW");
playBtn.setFont(Font.font(30));
playBtn.setBackground(Background.EMPTY);
Button loadBtn = new Button("LOAD");
loadBtn.setFont(Font.font(30));
playBtn.setBackground(Background.EMPTY);
Button exitBtn = new Button("EXIT");
exitBtn.setFont(Font.font(30));
exitBtn.setBackground(Background.EMPTY);
It’s almost the same block of code, but it has an error in it. The kind of error that you get from cutting and pasting repeatedly.
You should make it a habit to never cut and paste from your own code. Maybe from StackOverflow, but cutting and pasting from your own code almost guarantees that you’re breaking DRY, and you should just stop doing that.
Violating YAGNI and Clinginess
Programmers always seem to want to over-engineer and future-proof everything. Yet the truth is that most of the time, “You Ain’t Gonna Need It” (YAGNI). And then, once they’ve built something they don’t need they won’t use the Delete
key on it - clinginess.
In this example we have both titleGroupSec
and buttonClicked
which are instantiated as fields, but never used. In the end this just clutters things up and causes confusion.
Excessive Coupling
Coupling is the thing that will really complicate your project. Both SRP and DRY are huge helpers in reducing coupling, which is another good reason to apply them.
The main tool in reducing coupling is to give every element in you application the smallest possible scope. Define variables inside of blocks of code when you can, and use local variables instead of fields whenever possible.
Bad Names
Yes, naming things well is hard. Get over that and just do it.
Avoid the temptation to be cute or clever with your names. We all go through that phase. I know that I did, and I’m pretty sure that none of my coworkers appreciated it.
In this code we have:
titleHBox.getChildren().addFirst(putNameHere);
What is putNameHere
? You really cannot tell from this code. It’s a Node
subclass or it couldn’t be put into an HBox
, but other than that, we don’t know. Yes, you can put the cursor over it in any modern IDE and it will tell you what it is. But you still can’t just read the code an know. I can appreciate that putNameHere
is cute, but it’s not very clear and, therefore, not a very good name.
I’m not against the idea of putting an indicator of the Node
type in the name, as in titleHBox
in the snippet. Just be sure to change the name if you change the Node
type. We also have this in the code:
GridPane titleGroup = new GridPane();
Presumably, titleGroup
was originally defined as a Group
and later changed to a GridPane
. Looking at the code where it was used, I had no idea it was a GridPane
and this had me confused for a while.
JavaFX Principles
Now we move on to JavaFX principles that you should try to follow, and how you can get into trouble if you don’t.
Changing Layouts
I have written thousands and thousands of lines of JavaFX layout code, and I can hardly remember ever writing any code to change the layout of a screen while it was running. But beginners love to do this. I guess it just seems like the natural way to do things.
In this particular example, the desire to change the layout “on the fly” leads to virtually all of the other problems with the code.
In particular, this is not a thing:
titleHBox.getChildren().addFirst(putNameHere);
titleHBox.getChildren().addLast(nameOkButton);
This just created a bunch of red squiggly lines in my IDE. I think that these methods, addFirst()
and addLast()
are for LinkedLists
, which the ObservableLists
from Pane.children
are not.
However, the need to put stuff in at the beginning of a list of children
is really only something you’ll need if you’re messing around with a running layout. There’s also lots of calls to List.remove()
, which does work, but shouldn’t ever be needed.
Part of the problem is that in order to do all of this swapping around, you need to have references to the various Nodes
that you’re going to be adding and removing on the fly. And, since you’re going to be doing all of the swapping from different places in the layout, these references need to be global throughout all of the layout code. This creates a large amount of coupling. A really large amount.
In this code, we have two places where the swapping would occur. One is in the action for playBtn
, and the other would be in the backBtn
action. However, there is no code included for the backBtn
action, probably because putting everything back was a daunting task that the programmer couldn’t bring themselves to face yet. Both these action handlers would have to have access to references to the containing layout class and virtually all of the children.
Not Keeping the Layout Understandable
You should be able to look at a piece of layout code and get an idea in your head about how it’s going to look when it’s running. If you can’t, then something has gone seriously wrong.
In general, following SRP and DRY is going to force you to break your layout into chunks, and put those chunks together in a manner that makes it clear how the layout should look.
In this example we can see a Button
instantiated and configured in one section of code, added to a layout 20 lines later, and then getting its action defined 50 lines after that. This means that you have to read through almost all of the layout code to understand how an element is configured and added to the layout.
Not Using the Correct Layout Classes
Perhaps this rule should just be called “Don’t Abuse AnchorPane
and GridPane
”. AnchorPane
is the most abused layout class, perhaps because it’s alphabetically first in the choices in SceneBuilder and beginners think it’s some kind of default. Then they never use it as an AnchorPane
and use setLayoutX()
and setLayoutY()
(by dragging stuff in SceneBuilder) to position the children.
And for some reason, beginners think that everything else needs to be a GridPane
. I don’t know why.
Here we have titleGroup
which is actually a GridPane
but its not used like a
GridPane`. Children are dumped in, but never assigned to rows or columns. I was a little tempted to see what that actually does, but then I decided I didn’t need to know. I do know that when I ran the original code, it looked a little confused.
Not Using the Correct Event Types
This code uses the MouseClickedEvent
for all of the Button
actions. This will work for mouse clicks, but won’t work for keyboard access. So if the user tabs around the screen to put the focus on a Button
and hits Enter
, it’s not going to work.
The New Version
The new version is in Kotlin. Let’s take a look at it first:
class Opening : Application() {
private val isShowingNewGame: BooleanProperty = SimpleBooleanProperty(false)
override fun start(primaryStage: Stage) {
primaryStage.scene = Scene(createContent(), 500.0, 500.0).apply {
Opening::class.java.getResource("beginners.css")?.toString()?.let { stylesheets += it }
}
primaryStage.title = "love u <3"
primaryStage.show()
}
private fun createContent(): Region = BorderPane().apply {
top = HBox(Label("Basil's Cat Game :3") styleWith "title-label").centred()
bottom = HBox(createButton("EXIT") { Platform.exit() }).centred()
center = StackPane(buttonBox() showWhen isShowingNewGame.not(), newGameBox() showWhen isShowingNewGame)
}
private fun buttonBox() =
VBox(20.0, createButton("NEW") { isShowingNewGame.value = true }, createButton("LOAD") {}).centred()
private fun newGameBox(): Region = BorderPane().apply {
center = VBox(
HBox(6.0, Label("Name:") styleWith "prompt-label", TextField(), Button("OK")).centred()
).centred()
top = Button("Back").apply { setOnAction { isShowingNewGame.value = false } } styleWith "transparent-button"
}
private fun createButton(title: String, action: EventHandler<ActionEvent>) = Button(title).apply {
onAction = action
styleWith("transparent-button")
}
}
infix fun <T : Node> T.styleWith(selector: String): T = apply { styleClass += selector }
infix fun <T : Node> T.showWhen(observed: ObservableBooleanValue): T = apply {
visibleProperty().bind(observed)
managedProperty().bind(observed)
}
fun HBox.centred() = apply { alignment = Pos.CENTER }
fun VBox.centred() = apply { alignment = Pos.CENTER }
fun main() {
Application.launch(Opening::class.java)
You’ll see that pretty much all of it is still inside Opening
which extends Application
. It was left this way because the application really doesn’t “do” anything, so there was no need to create a framework for the application. It would be easy, though, to replace createContent()
with OpeningController.getView()
and introduce an MVCI framework.
Application.start()
Application.start()
should only deal directly with the mechanics of Stage
and Scene
. If you put anything else in here, you’re doing it wrong. The original code had everything in Application.start()
, which was an extreme violation of SRP:
override fun start(primaryStage: Stage) {
primaryStage.scene = Scene(createContent(), 500.0, 500.0).apply {
Opening::class.java.getResource("beginners.css")?.toString()?.let { stylesheets += it }
}
primaryStage.title = "love u <3"
primaryStage.show()
}
This is short and sweet. We create the Scene
and delegate the creation of the layout to createContent()
. Then we attach the StyleSheet, and put it into the Stage
. Then we configure the title and put the Stage
on the screen.
That’s it. That’s all this method should ever do.
The createContent() Method
The entire layout comprises about 20 lines of code across 4 methods. Let’s look at the top level method, createContent()
:
private fun createContent(): Region = BorderPane().apply {
top = HBox(Label("Basil's Cat Game :3") styleWith "title-label").centred()
bottom = HBox(createButton("EXIT") { Platform.exit() }).centred()
center = StackPane(buttonBox() showWhen isShowingNewGame.not(), newGameBox() showWhen isShowingNewGame)
}
It’s just 5 lines of code, so it can’t be hard to understand. You can immediately see that this is going to return a BorderPane
and that the top
region is populated with a styled Label
popped into an HBox
so that it can be centred. There’s a bit of Kotlin magic here, the HBox.centred()
extension function and the Node.styleWith()
extension function, both of which we’ll look at in a bit. But for now, it’s pretty clear what they do.
The bottom
region gets an exit Button
created through a builder/factory method and put in an HBox
so that it too can be centred.
The center
region gets a StackPane
with two “boxes” created through builder methods and then modified with something called showWhen
. We’ll look at showWhen
later, but those builder methods now…
The buttonBox() Method
private fun buttonBox() =
VBox(20.0, createButton("NEW") { isShowingNewGame.value = true }, createButton("LOAD") {}).centred()
This is technically one line of code, but it’s been formatted out to 2 for readability. It’s a VBox
with spacing of 20 pixels between the children. The children are two Buttons
created through that same builder/factory method we saw used in createContent()
. The contents of the VBox
are centred. That’s all…pretty simple.
The newGameBox() Method
private fun newGameBox(): Region = BorderPane().apply {
center = VBox(
HBox(6.0, Label("Name:") styleWith "prompt-label", TextField(), Button("OK")).centred()
).centred()
top = Button("Back").apply { setOnAction { isShowingNewGame.value = false } } styleWith "transparent-button"
}
This is another BorderPane
because we want to have the “Back” Button
in the top left corner, and the HBox
holding the input stuff centred. There’s no problem putting BorderPane
inside another BorderPane
or any other container class. Also, there’s nothing “heavyweight” about BorderPane
that would have you want to avoid it.
The center
region has a VBox
holding an HBox
both of which are centred. This puts the contents of the HBox
right in the middle of the region. Inside the HBox
we have a styled Label
that says, “Name:”, a TextField
and a Button
labeled “OK”. Neither the Button
nor the TextField
do anything simply because they didn’t in the original code.
The “Back” Button
has an action that sets the value of isShowingNewGame
to false. isShowingNewGame
is the only field in the application now, and it’s a BooleanProperty
. Clicking “Back” sets it to false.
The Button Builder/Factory Method
private fun createButton(title: String, action: EventHandler<ActionEvent>) = Button(title).apply {
onAction = action
styleWith("transparent-button")
}
This is an application of DRY to the original code. All the Buttons
were configured similarly, so this method does the repeated stuff. Additionally, we have a parameter that accepts the EventHandler
that’s attached to the Button
for its OnAction Event
. We’re not setting the font or the background programmatically any more, either, now were using StyleSheets via Node.styleWith()
.
Kotlin Extension Methods
That’s it for the layout code. However, there are those mysterious Kotlin extension functions that were used all over the place. Let’s look at them:
infix fun <T : Node> T.styleWith(selector: String): T = apply { styleClass += selector }
infix fun <T : Node> T.showWhen(observed: ObservableBooleanValue): T = apply {
visibleProperty().bind(observed)
managedProperty().bind(observed)
}
fun HBox.centred() = apply { alignment = Pos.CENTER }
fun VBox.centred() = apply { alignment = Pos.CENTER }
In Kotlin, you can add a method to an existing class without extending it using “Extension Functions”. These evaluated statically, so they’re not really breaking any JVM rules, but they can save a lot of boilerplate with JavaFX. You might also notice that these methods are declared outside of Opening
and not part of any class at all. This is something that you can do in Kotlin, and these are generally referred to as “top level functions”.
Let’s look at HBox.centred()
. It uses a “Scope Function” called apply{}
which is defined as a member function of Object
and passes the object it’s called on to the body of apply{}
as this
. Then the function returns the object itself. Just like in Java, when this
can be inferred, it can be left out. In this case it shortens this.setAlignment(Pos.CENTER)
to alignment = Pos.CENTER
where setAlignment()
is also replaced with the field accessor version.
Unfortunately, alignment
is defined at the HBox
and VBox
level, so we need an extension function for each one. Each one will return the HBox
or VBox
it’s called on, acting like a decorator.
For the styleWith
there are a couple extra things. First, styleClass
is defined at Node
, but we want our decorator to return the actual class of the object it’s called on. So, if we call Label.styleWith()
we want a Label
back, not a Node
. So we have to define it generically as a type T
that’s a subclass of Node
. The styleClass
value is a List
and we use the List.plusAssign()
method to add a new selector to it, through +=
.
Finally, we have a modifier, infix
. Using this means that we can skip the .
and the ()
. You can define any function that has one passed parameter this way if you want. This allows Label("Hello") styleWith "some-selector"
. Once again, it acts a decorator, returning the original object.
The showWhen() Function
This is another infix decorator function, one that simply binds the Visible
and Managed Properties
of the Node
to some ObservableBooleanValue
.
This is called in two ways, one with isShowingNewGame
as the parameter, and one with isShowingNewGame.not()
as the parameter. In other words, each the two Nodes
will only be visible when the other one is not.
Originally I had the calls to this method as part of the builder methods buttonBox()
and newGameBox()
, but then I thought it made more sense as part of the layout code since it was related more to how the entire layout functions. Having the two calls sitting side-by-side in the code also made it easier to see how it was being used:
center = StackPane(buttonBox() showWhen isShowingNewGame.not(), newGameBox() showWhen isShowingNewGame)
How Does This Fix The Issues?
Single Responsibility Principle
Now we don’t have any methods that are larger than 5 lines long. It’s pretty hard to cram too much responsibility into 5 lines or less.
One place where we do get close is with the definition of the top
section of the main BorderPane
and the center
section of the BorderPane
defined in newGameBox()
. In both these cases we’re configuring multiple elements at one time. In the second case it’s a VBox
holding an HBox
holding a Label
, TextField
and Button
.
In practice I find that there’s a certain “threshold of triviality” that can be applied to these situations. Even if you’re technically breaking SRP, are the second, third and fourth responsibilities so trivial, individually and collectively, that they can be left in with the primary responsibility? The only trade-off with following SRP to the letter is that you need to drill down into the delegate methods to see what they do. So it’s never going to hurt much to split out and delegate the extra responsibilities, but you might lose some readability.
In the case of the center
section defined in newGameBox()
, it’s only trivial because neither the TextField
nor the Button
do anything. The moment that you start to add configuration in for these elements, it will breach the “threshold of triviality” and needs to be split out into its own method.
Don’t Repeat Yourself
Nothing is repeated here. Between the extension functions and the redesign itself, all of the repetitious code has been excised from this application.
YAGNI
In all honesty, the TextField
and the “OK” Button
feel a bit like YAGNI, simply because they don’t do anything. In truth, though, you are going to need them, and they’re more like unfinished features than anything else. That being said, half implemented features are almost as bad as YAGNI violations. Features should be so small that there’s never any possibility of implementing only half of one.
In this particular case, it seems like making that “OK” Button
do something is going to require some application logic and access to a persistence layer. That will mean adding a framework, which will mean some refactoring and redesign. That should have been done before the Button
was added to the screen.
Excessive Coupling
The entire application now has only one variable defined, and that is the final BooleanProperty
field, isShowingNewGame
. It is very, very difficult to have coupling without variables to reference. In fact, isShowingNewGame
was only introduced to act as a central coupling point for the functionality of the Buttons
and layout swapping - eliminating the need to instantiate those Buttons
or layouts as variables in order to reference them from other layout elements.
Bad Names
Almost no variables means almost no variables to name, too! What we do have, however, are methods that need good names. I feel like the extension functions, styleWith()
, showWhen()
and centred()
have good names, although perhaps styledWith()
and shownWhen()
- changing the tense, might be better.
Maybe createButton()
might be more descriptive as createBigButton()
or createMainButton()
, but with an application this small it hardly matters. If the application grew and more Button
types were added, then you’d definitely need to change this name.
The same goes for the other methods. Perhaps buttonBox()
is fine for now, but maybe menuBox()
would be more descriptive if the application grows. The main method, createContent()
will become build()
if this is changed over to a framework, and this code is all moved into a Builder<Region>
.
Changing Layouts
This layout is now static, but it behaves dynamically in response to the value in isShowingNewGame
. There no longer is any attempt to change the running layout.
Understandable Layout
It’s 20 lines of code. It’s a BorderPane
and you can see at a glance that only the top
, center
and bottom
sections are used. You can see right away that the centre is a StackPane
with two layouts that alternate in visibility. You can drill down into those two layouts and understand them easily.
Conclusion
It’s pretty easy to look at the final code and say, “Yeah, but that’s a pretty trivial example. It’s only 20 lines of layout code. You can’t do that with something complex.” But yet…the original code was over 100 lines long, and was non-trivial enough that the author got stuck trying to figure out how to make it work.
Hopefully, you can also see how Kotlin makes life so much better with JavaFX.
One really big rule when writing layout code is NOT to reference Nodes
from other Nodes
, especially when those Nodes
aren’t closely related in the layout. Especially, especially in the case where they are so unrelated in the layout that you need to instantiate one of them as a field, so that it’s global to the layout. Instead, create a Property
as a field, or in your Model, and then reference that Property
from the Nodes
that need to interact (but now indirectly).