The State of JVM Desktop Frameworks: Swing
While quite old, Swing still gets the job done. This post focuses on Swing API, creating a sample application for comparative purposes, and lessons learned along the way.
Join the DZone community and get the full member experience.Join For Free
In the first post of this series, we went through the rise and fall of some of the desktop frameworks, mainly Java ones. This post and the following will each focus on a single JVM framework. To compare between them, a baseline is in order. Thus, we will develop the same application using different frameworks. Will use the Kotlin language because some of the frameworks require Kotlin.
This post focuses on the Swing API.
A Sample Application
We need a simple application to develop for comparison purposes. The Web offers a lot of application ideas:
- A TODO list.
- A quiz.
- A calculator.
- A client over a web API e.g. Reddit, Twitter, etc.
- Conway’s game of life.
Years ago, I developed a custom dice-rolling application for the Champions role-playing game. But it’s complex and requires a non-trivial amount of work.
In the past, when I tried Griffon, I used a file renamer sample. In short, it allows to select a folder and to run a batch rename command on its child files with the help of a regular expression. The wireframe looks like the following:
User interactions with the application are as follows:
|Application starts||Fill the Folder field value with the path to the current user’s home.|
|Button Browse clicked||
|Folder field value changed||Refresh file candidates name.|
|Regex field value changed||Refresh file candidates name.|
|Replacement field value changed||Refresh file candidates name.|
|Button Apply clicked||Rename files.|
A couple of other rules apply:
- When the File Browser popup opens, it’s initialized with the Folder field value.
- In the table, if the candidate name is different from the current name i.e. if the renaming changes the name, paint the cell’s background in yellow.
- One can select folders, but not files.
The benefit of this sample is two-fold: the business logic is quite limited, while the GUI has enough behavior to be of interest.
A Quick Overview of Swing
Giving the age of Swing, a lot of material is readily available on the Web. Still, let’s have a quick overview of the Swing framework.
Swing is not the first Java framework. This honor belongs to AWT.
AWT is the first GUI framework, available since 1.0. It’s a thin abstraction layer on top of the system-specific graphical objects. For this reason, an AWT application displays — and uses — the platform’s Look-And-Feel. AWT controls are thus called heavyweight ones in Java.
In the 1.2 release, Java offered Swing. While AWT relies on OS graphics, Swing paints every component itself. To do so, it relies on the Java 2D Graphics library. Java 2D and Swing make up the Java Foundation Classes.
Because it’s independent of the OS, Swing provides two main benefits compared to AWT:
- A large catalog of widgets. AWT is limited by the set of widgets that are available on all OS providing a JDK. Having no such limitations, Swing can offer any widget.
- OS-independent Look-And-Feel. The OS LAF constrains AWT. Because Swing implements the painting of widgets, it can (and it does) provide pluggable LAFs. For example, Metal is the basic OS-independent one hut it also provides several LAFs based on specific OS.
You can set the LAF at startup time and change it dynamically during the application lifecycle.
You can also design a custom LAF — though it’s not trivial.
On the flip side, a Swing application consumes more memory than an AWT one.
The Swing class hierarchy is pretty standard for developers. An abstract
JComponentclass is the parent. A
Containerclass represents a component that has children components.
As the diagram shows, Swing makes use of some of the AWT classes.
This theoretically allows you to mix components of both frameworks. Yet, you should avoid doing this in general since the mix of AWT components, which are heavyweight, with (lightweight) Swing components, can end in odd behaviors.
Swing offers a full-fledged event system. It’s based on the classical Observer pattern.
For example, here’s a simplified view of the
JButton class diagram for the
Classical layouts are available, such as
BoxLayout — which can be either vertical or horizontal - and
GridLayout. It’s possible to design any application by combining them.
For most applications, the nesting of layouts will be too deep. A powerful alternative to nested layouts is the
GridBagLayout: it allows the precise placement of components on a parent container.
Swing offers a unified API across all available layouts. Hence, the second parameter of the
Container.add()method is of type
Object. For example, on a
GridBagLayout, the second parameter needs to be a
Of course, generics would allow stricter type-checking. But Swing was designed a long time before generics and the API never did use them — and never will because of Swing’s status.
In Java, the code is boilerplate-y. Kotlin can improve it, thanks to extension functions, and named/default arguments.
- Allow defining non-default values.
- Accept any number of arguments.
- Loop over the pairs to add them.
- Add component with defined constraints.
Here are the lessons I learned, in no particular order:
Event Bus for the Win
This is not specific to Swing: introducing an Event Bus between event producers and event listeners allows to decouple the latter from the former. If you’re interested to read more about the Event Bus, please read a previous post on the subject.
In the sample, the Event Bus implementation is Green Robot.
To rename, the Apply button needs data: the path, the regex, and the replacement. These data are available in the three different text fields.
How can one access these data in the button? At least two different ways are available:
- Store a reference to the fields in the button.
- Each time a field value changes, send an event with the new value, make the button listen to those events, and store event values.
Moreover, when the value of either the Regex or the Replacement text fields changes, the application needs to refresh the right column of the table.
My preference goes to the second option to decouple components from each other.
This is the flow’s representation:
PathModel is a singleton that has no GUI associated. With the help of the Event Bus, it allows us to have a single place to listen to events, and dispatch them afterward.
Components Are Not Scrollable
While nobody expects text fields or buttons to be scrollable, it’s a different matter for text boxes and tables. But by default, no Swing component is scrollable. To make such a component scrollable, one needs to embed it in a
One can customize the scroll pane in a fine-grained way, but it works out-of-the-box with
JTable. For example, column headers are always visible by default.
Text field model and events
Swing decouples components from the data they display:
JTable store its data in a
JComboBox in a
JTextField in a
Regarding events, models are the objects to listen to. For example,
Document offers fine-grained events when its content changes: proper change, insertion, and removal.
The following class diagram is a summary:
In the sample app, different text fields need to send events when their content changes. By taking advantage of Kotlin, it’s possible to handle this in a centralized place:
- Handle each change in the same way.
The Swing threading model is easy to get wrong. The most important rule is that no long-running task should ever run on the main event thread, known as the Event Dispatch Thread.
Tasks on the event dispatch thread must finish quickly; if they don’t, unhandled events back up and the user interface becomes unresponsive.— The Event Dispatch Thread
Two approaches are possible:
- Make all subscriptions to the event bus asynchronous. The Event Bus calls methods triggered in this way on a dedicated thread, not on the EDT. In that case, the developer needs to explicitly run the code on the EDT when necessary.
- Keep the default synchronous behavior. Posting events on the bus fits the definition of 'finishing quickly'. But then, a
Runnableneeds to wrap long-running tasks. One can use
SwingUtilities.invokeLater()to start it.
In the sample app, I favored the second approach, as renaming is the only task that can potentially run for 'a long time.'
- There’s no need to design a widget to choose files from scratch. Swing offers the
JFileChooserclass that displays such a configurable widget.
- Objects need to collaborate. We have already written about the Event Bus to manage runtime events. To assemble components, there are other ways: Dependency Injection is a pretty popular one. For a simple application, even more so a GUI one, singletons are more than enough. For example, the
JTablecan be singletons:Kotlin
While quite old, Swing still gets the job done. It’s the baseline with which we will compare other approaches.
If you’re interested, you can run the application by yourself.
Thanks to Hendrik Ebbers for his kind review.
The complete source code for this post can be found on Github in Maven format.
To Go Further:
Originally published at A Java Geek on January 17th, 2020.
Published at DZone with permission of Nicolas Fränkel, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.