Is it possible to use Java Lambdas to implement something like Groovy's SwingBuilder?

102 views Asked by At

It just occurred to me that with Lambdas it might be possible to build something like SwingBuilder in native Java--but it seems like if this were possible it would have been done by now.

Is there any reason this couldn't be done, or has it been done?

I realize this is similar to SwingBuilder like GUI syntax for Java?, but I was hoping to add Lambdas into the mix. SwingBuilder is mostly built on Lambdas this absolutely wasn't possible before Java 8. It also uses a bit of dynamic programming and that obviously can't be used so it won't ever be AS clean, but I think it's possible...

1

There are 1 answers

0
Holger On

First, we have to identify which issues we want to solve. The biggest issue is the creation of simple event bindings which needed the use of inner classes in previous Java versions. This can be replaced with lambda expression for single-method listener interfaces in most cases, for multi-method interfaces, you’d need adapters, like discussed in this or that Q&A, but this has to be done only once.

The other issue is the structure of the initialization code. In principle, imperative code works fine, but if you want to modify some properties of a component you’re going to add to a container, you have to change the container.add(new ComponentType()); to introduce a new local variable to be used by the subsequent statements. Also, adding to a container itself requires to keep it in a variable. While Java allows to limit the scope of local variables with curly braces, the result still is clumsy.

This is the best starting point. If we use, e.g.

public class SwingBuilder {
    public static <T> T build(T instance, Consumer<T> prepare) {
        prepare.accept(instance);
        return instance;
    }
    public static <T extends Container> T build(
                                        T instance, Consumer<T> prepare, Component... ch) {
        return build(build(instance, prepare), ch);
    }
    public static <T extends Container> T build(T instance, Component... ch) {
        for(Component c: ch) instance.add(c);
        return instance;
    }
}

These simple generic methods are already quiet powerful due to the fact that they can be combined. Using import static, a use site can look like

JFrame frame = build(new JFrame("Example"),
    f -> {
        f.getContentPane().setLayout(
            new BoxLayout(f.getContentPane(), BoxLayout.LINE_AXIS));
        f.setResizable(false);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    },
    build(new JLabel("\u263A"), l -> l.setFont(l.getFont().deriveFont(36f))),
    Box.createHorizontalStrut(16),
    build(new JPanel(new GridLayout(0, 1, 0, 5)),
        new JLabel("Hello World"),
        build(new JButton("Close"), b -> {
            b.addActionListener(ev -> System.exit(0));
        })
    )
);
frame.pack();
frame.setVisible(true);

Compared to Groovy, we still have to use a variable to express the target of method invocations to change properties, but these variable can be declared as simple as name -> using type inference when implementing the Consumer via lambda expression. Also, the variable’s scope is automatically limited to the duration of the initialization.

Using this starting point, you may add specialized method for often used components and/or often used properties, as well as the already mentioned factory methods for multi-method listeners. E.g.

public static JFrame frame(String title, Consumer<WindowEvent> closingAction,
                           Consumer<? super JFrame> prepare, Component... contents) {
    JFrame f = new JFrame(title);
    if(closingAction!=null) f.addWindowListener(new WindowAdapter() {
        @Override public void windowClosing(WindowEvent e) {
            closingAction.accept(e);
        }
    });
    if(prepare!=null) prepare.accept(f);
    final Container target = f.getContentPane();
    if(contents.length==1) target.add(contents[0], BorderLayout.CENTER);
    else {
        target.setLayout(new BoxLayout(target, BoxLayout.PAGE_AXIS));
        for(Component c: contents) target.add(c);
    }
    return f;
}

But I think, the picture is clear.