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. We 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
- etc.
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:
Event | Action | ||||||||
---|---|---|---|---|---|---|---|---|---|
Application starts | Fill the Folder field value with the path to the current user's home
| ||||||||
Button Browse clicked
|
|
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.
Before Swing
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.
Components overview
The Swing class hierarchy is pretty standard for developers. An abstract JComponent
class is the parent. A Container
class represents a component that has children components.
As the diagram shows, Swing makes use 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.
Events
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 ActionListener
:
Layout
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 Container
with a GridBagLayout
, the second parameter needs to be a GridBagConstraints
object.
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.
private fun constraints(
gridx: Int = 0, gridy: Int = 0, gridwidth: Int = 1, gridheight: Int = 1,
weightx: Double = 0.0, weighty: Double = 0.0, anchor: Int = CENTER,
fill: Int = NONE, insets: Insets = Insets(0, 0, 0, 0),
ipadx: Int = 0, ipady: Int = 0) = GridBagConstraints().apply { // 1
// Apply the parameters
}
private fun JPanel.add(vararg components: Pair<JComponent, // 2
GridBagConstraints>) {
components.forEach { // 3
add(it.first, it.second)
}
}
JPanel().add(
JLabel("Folder:") to constraints(insets = Insets(4, 4, 4, 0)), // 4
DirectoryTextField to constraints(gridx = 1, fill = HORIZONTAL, weightx = 1.0, gridwidth = 2), <4>
FolderPickerButton to constraints(gridx = 3, anchor = LINE_END, insets = Insets(4, 0, 4, 0)) <4>
)
- Allow to define non-default values
- Accept any number of arguments
- Loop over the pairs to add them
- Add component with defined constraints
Lessons learned
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.
Event model
To rename, the btn:[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 JScrollPane
component.
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 TableModel
, JComboBox
in a ComBoxModel
, JTextField
in a Document
, etc.
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:
abstract class FiringTextField<T>(private val eventBus: EventBus,
private val create: (String) -> T) : JTextField() {
private val Document.text: String
get() = getText(0, length)
init {
document.addDocumentListener(object : DocumentListener {
override fun changedUpdate(e: DocumentEvent) = postChange(e) // 1
override fun insertUpdate(e: DocumentEvent) = postChange(e) // 1
override fun removeUpdate(e: DocumentEvent) = postChange(e) // 1
})
}
private fun postChange(e: DocumentEvent) =
eventBus.post(create(e.document.text))
}
object ReplacementTextField : FiringTextField<ReplacementUpdatedEvent>(
EventBus.getDefault(),
{ ReplacementUpdatedEvent(it) }
)
- Handle each change in the same way
Threading
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.
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
Runnable
needs to wrap long-running tasks. One can useSwingUtilities.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".
Additional takeaways
There's no need to design a widget to choose files from scratch. Swing offers the
JFileChooser
class 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
TableModel
and theJTable
can be singletons:object FileModel : AbstractTableModel() { // Initialization } object FileTable : JTable(FileModel) { // Initialization }
Conclusion
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 2021