Express your Swing code easily in Kotlin
This project was born when I had an urgent need to quickly provide a simple UI for remote JVM application. I needed a remote control for my Robot, talking over OSC protocol, already coded in Kotlin and based on functional reactive programming principles (See we-are-the-robots on GitHub). I decided to go for Swing, to stay in the same ecosystem, but I remember the experience of coding GUI in Swing years ago, and I remember what I liked about it and what was painful. Fortunately now I also have the experience of building Domain Specific Languages in Kotlin and I quickly realized that I can finally swing the way I always wanted to.
import com.badoo.reaktive.observable.*
import com.badoo.reaktive.scheduler.ioScheduler
import java.awt.Dimension
import javax.swing.SwingConstants
fun main() = mainFrame("My Browser") {
val contentBox = label("") {
horizontalAlignment = SwingConstants.CENTER
verticalAlignment = SwingConstants.CENTER
preferredSize = Dimension(300, 300)
}
val urlBox = textField(10)
val goAction = button("Go!") {
isEnabled = false
actionEvents
.withLatestFrom(urlBox.textChanges) { _, url: String -> url }
.doOnAfterNext { url ->
isEnabled = false
contentBox.text = "Loading: $url"
}
.delay(1000, ioScheduler) // e.g. REST request
.observeOn(swingScheduler)
.subscribe { url ->
isEnabled = true
contentBox.text = "Ready: $url"
}
}
urlBox.textChanges.subscribe { url ->
goAction.isEnabled = url.isNotBlank()
contentBox.text = "Will load: $url"
}
contentPane = borderPanel {
layout.hgap = 4
panel.border = emptyBorder(4)
north = borderPanel {
west = label("URL")
center = urlBox
east = goAction
}
center = contentBox
}
}
will produce:
Benefits:
- compact code, minimal verbosity
- declarative instead of imperative
- functional reactive programming way of handling events (Reaktive)
- component encapsulation - communication through well defined event streams
swingScheduler
for receiving asynchronously produced events (see below)
And here is an equivalent code in Java for the sake of comparison:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.lang.reflect.InvocationTargetException;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
public class SwingJavaWay {
public static void main(String[] args) throws InvocationTargetException, InterruptedException {
SwingUtilities.invokeAndWait(SwingJavaWay::createFrame);
}
private static void createFrame() {
JFrame frame = new JFrame("My Browser");
final JLabel contentBox = new JLabel();
contentBox.setHorizontalAlignment(SwingConstants.CENTER);
contentBox.setVerticalAlignment(SwingConstants.CENTER);
contentBox.setPreferredSize(new Dimension(300, 300));
JPanel contentPanel = new JPanel(new BorderLayout());
JPanel northContent = new JPanel(new BorderLayout(4, 0));
northContent.add(new JLabel(("URL")), BorderLayout.WEST);
northContent.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
JTextField urlBox = new JTextField(10);
JButton goAction = new JButton("Go!");
goAction.setEnabled(false);
goAction.addActionListener(
e -> {
contentBox.setText("Loading: " + urlBox.getText());
goAction.setEnabled(false);
Timer timer =
new Timer(
1000,
t -> {
contentBox.setText("Ready: " + urlBox.getText());
goAction.setEnabled(true);
});
timer.setRepeats(false);
timer.start();
});
northContent.add(goAction, BorderLayout.EAST);
urlBox
.getDocument()
.addDocumentListener(
new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
fireChange(e);
}
@Override
public void removeUpdate(DocumentEvent e) {
fireChange(e);
}
@Override
public void changedUpdate(DocumentEvent e) {
fireChange(e);
}
private void fireChange(DocumentEvent e) {
String text = urlBox.getText();
contentBox.setText("Will load: " + text);
goAction.setEnabled(!text.trim().isEmpty());
}
});
northContent.add(urlBox, BorderLayout.CENTER);
contentPanel.add(northContent, BorderLayout.NORTH);
contentPanel.add(contentBox, BorderLayout.CENTER);
frame.setContentPane(contentPanel);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
ℹ️ There is also another more elaborate Kotlin example:
SwingKotlinWayWithReactiveModelViewPresenter
, showing how to use Model-View-Presenter
pattern with this library, where events from the view are represented as reactive Observable
s.
By default everything in Swing is supposed to run on the same thread. Any update to any GUI
component should happen there, otherwise concurrency might cause consistency issues. But in many
cases long running computation, or asynchronous IO, will require us to receive results in one thread
and display them in the main Swing thread. This is what swingScheduler
is for:
fun main() = mainFrame("swingScheduler example") {
contentPane = label{
observableInterval(1000, swingScheduler)
.subscribe { tick ->
text = tick.toString()
}
preferredSize = Dimension(100, 100)
horizontalAlignment = SwingConstants.CENTER
}
}
The observableInterval
creates a constant stream of ticks produced by another thread.
Once they happen, the subscription code will be handled by the Swing thread.
Most of the time it is not needed to specify swingScheduler
for simple event handling,
because the default scheduler for receiving events will be usually the same as the one
used for publishing them, and this one is already the
Swing event thread.