The state of JVM desktop frameworks: Swing

Nicolas Fränkel - Jan 17 '21 - - Dev Community

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:

File Renamer application wireframe

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
  • Open a new File Browser popup. When the user chooses a folder, replace the Folder field value with the folder's path
  • Refresh file candidates name
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.

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:

  1. 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.
  2. 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.

Swing components' hierarchy class diagram

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:

Swing JButton event simplified class diagram

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.

Simplified layout class diagram

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>
)
Enter fullscreen mode Exit fullscreen mode
  1. Allow to define non-default values
  2. Accept any number of arguments
  3. Loop over the pairs to add them
  4. 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:

  1. Store a reference to the fields in the button
  2. 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:

Renamer application sequence diagram

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.

JScrollPane class diagram

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:

Summary of JTextField class diagram

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) }
)
Enter fullscreen mode Exit fullscreen mode
  1. 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.

-- The Event Dispatch Thread

Two approaches are possible:

  1. 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.
  2. 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 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".

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.

    Screenshot of the JFileChooser

  • 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 the JTable 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:

File Renamer

This project provides a simple Swing GUI to rename files in batch.

The goal of this project is two-fold:

  1. Showcase GitHub and Maven artifact management integration

  2. Provide a comparison baseline for projects using Kotlin and the Java Swing API

To go further:

Originally published at A Java Geek on January 17th 2021

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .