Groovy vs Java for JavaFX

    image

    JavaFX is good!


    First, a few words about JavaFX. What we liked working with her.

    Modern API . Even without the "builders", everything looks very modern.

    Total Data Driven Development . Adore it. Logic based on a bunch of data clears the code from junk, getters / setters - “down!”. Work with data change events, two-way “binding”.

    FXML . Great thing for prototyping. It is clear to the designer, there is a good visual tool from Oracle - "JavaFX Scene Builder". I note that then we still wanted to rewrite FXML in the form of regular code. It's easier to maintain FXML than code - you always have to edit two files, code and FXML. Plus, when using code, it is easier to use inheritance.

    Nodes. The structure of the components. You can run on a tree. You can search by lookup (). Like in the DOM. Just write jQuery.

    The CSS . This is really a Thing. "Throw off" the components through one common css-file. ID schnicks, CSS classes, selectors and pseudo selectors.

    Engine the Text . Very good engine for complex texts.

    The WebView . We implement sophisticated components on the Webkit engine. Read about this in the previous article .

    Which is not very good


    That is good. What is wrong? JavaFX script at one time was not just invented. Creating fields for accessing Bindable data through getters and setters is a step back and yesterday. Java is not very good here. Java 8 has lambda expressions, but their appearance is also the answer to the question that something needs to be done with Java and an occasion to think about a more cardinal solution.

    Groovy!


    We solved all these problems for ourselves by choosing Groovy. It is concise, in a good way old (matured) and well maintained in IDEA. Groovy allowed us to cut the code ten times for sure. It works, looks and reads almost like Java, but how good it is in terms of compactness!

    There are a bunch of good and beautiful languages ​​for the JVM, but it so happened that Groovy suits us. And we love brackets, annotations and we don’t want to break something in ourselves here. Plus, I personally had seven years of experience using Groovy, and when there is an expert in the team, it’s better to use, rather than take something completely unknown.

    By the way, Groovy takes 18th place in terms of popularity of languages ​​(according to TIOBE ).

    Our practices


    Now let's look at some examples. We copy from our project, the code is real.

    Component Configuration


    Just create an instance of the component through the code and configure it.
    In Java, we had to step by step, line by line, to assign values.

    Button button = new Button();
    button.setFocusTraversable(false);
    button.setLayoutX(23);
    button.setPrefHeight(30);
    button.setPrefWidth(30);
    button.setText("ADD");
    

    What does the same look like if you rewrite Groovy?

    Button button = new Button(focusTraversable: false, layoutY: 23, prefHeight: 30, prefWidth: 30, text: "Add")  
    

    Grooves, I remind you who do not know, allows you to access access methods (getters, setters) without the set / get prefix. That is, if there is a setText method in the class, then its call is made through a simple assignment of a value - text = "Add". Plus, when compiling Groovy classes, getters and setters are automatically added to public fields. Therefore, calling the set / get method from a groove is not realistically necessary.

    And you can pass pairs to the constructor parameters - name: value (in fact, this is ordinary HashMap and the syntax here is Groovy Maps - [key1: value1, key2: value]).

    Moreover, it is important that IDEA tells us all this, validates the data type and access restriction.

    This way of configuring components immediately suggests that it is impossible to configure the structure of components at once?

    Can!

    menus.addAll(
            new Menu(text: "File", newItems: [
                    new MenuItem(
                            text: "New Window",
                            onAction: { t ->
                                ApplicationUtil.startAnotherColtInstance()
                            } as EventHandler,
                            accelerator: new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN)
                    ),
                    new Menu(text: "New Project", newItems: [
                            newAs = new MenuItem(
                                    text: "New AS Project",
                                    id: "new-as",
                                    onAction: { t ->
                                        ProjectDialogs.newAsProjectDialog(scene, false)
                                    } as EventHandler
                            ),
                            newJs = new MenuItem(
                                    text: "New JS Project",
                                    id: "new-js",
                                    onAction: { t ->
                                        ProjectDialogs.newJsProjectDialog(scene, false)
                                    } as EventHandler
                            )
                    ]),
                    new SeparatorMenuItem(),
                    new MenuItem(
                            text: "Open Project",
                            onAction: { t ->
                                ProjectDialogs.openProjectDialog(scene, false)
                            } as EventHandler,
                            accelerator: new KeyCodeCombination(KeyCode.O, KeyCombination.SHORTCUT_DOWN)
                    ),
                    recentProjectsSubMenu = new Menu(text: "Open Recent", newItems: [
                            clearRecentProjects = new MenuItem(
                                    text: "Clear List",
                                    onAction: { t ->
                                        RecentProjects.clear()
                                    } as EventHandler
                            ),
                    ]),
                    new SeparatorMenuItem(),
                    save = new MenuItem(
                            text: "Save Project",
                            id: "save",
                            onAction: { t ->
                                ProjectDialogs.saveProjectDialog()
                            } as EventHandler,
                            accelerator: new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN),
                            disable: true
                    ),
                    saveAs = new MenuItem(
                            text: "Save As...",
                            onAction: { t ->
                                ProjectDialogs.saveAsProjectDialog(scene)
                            } as EventHandler,
                            accelerator: new KeyCodeCombination(KeyCode.S,
                                    KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN),
                    ),
                    new MenuItem(
                            text: "Close Project",
                            onAction: { t ->
                                ProjectDialogs.closeProjectDialog()
                            } as EventHandler,
                            accelerator: new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN),
                    ),
                    new SeparatorMenuItem(),
                    new MenuItem(
                            text: "Exit",
                            onAction: { t ->
                                ApplicationUtil.exitColt()
                            } as EventHandler
                    ),
            ]),
            new Menu(text: "Help", newItems: [
                    new MenuItem(
                            text: "Open Demo Projects Directory",
                            onAction: { t ->
                                ProjectDialogs.openDemoProjectDialog(scene)
                            } as EventHandler
                    ),
                    new MenuItem(
                            text: "Open Welcome Screen",
                            onAction: { t ->
                                ProjectDialogs.openWelcomeScreen(scene)
                            } as EventHandler
                    ),
            ])
    )
    

    Such code looks no less readable than FXML. Plus, here, in place, you can describe all the event handlers, which could not be done on FXML. And maintaining such code is easier.

    Dynamic properties and methods


    The attentive reader will ask, what is the menu field “newItems” for? Yes, the Menu class does not have such a method. And we added such a method, because we can only read the items field, but we cannot assign it. It does not have the “setItems ()” method, but it only has “getItems ()” and you cannot assign a new value. Read-only. To to configure Menu as a structure, we added a dynamic field. It

    is very simple to add such a field, but our Java entity resisted such sedition for a long time as dynamic methods. We invented a lot of bicycles until we put up with the fact of the need to use dynamics.

    Adding a dynamic field We moved it into a separate GroovyDynamicMethods class, here is its code:

    class GroovyDynamicMethods {
        private static inited = false
        static void init() {
            if(inited)return
            inited = true
            addSetter(javafx.scene.Node, "newStyleClass", { String it ->
                styleClass.add(it)
            })
            addSetter(Parent, "newChildren", {List it ->
                children.addAll(it)
            })
            addSetter(Menu, "newItems", {List it ->
                items.addAll(it)
            })
        }
        private static void addSetter(Class clazz, String methodName, Closure methodBody) {
            addMethod(clazz, "set" + methodName.capitalize(), methodBody)
        }
        private static void addMethod(Class clazz, String methodName, Closure methodBody) {
            ExpandoMetaClass exp = new ExpandoMetaClass(clazz, false)
            exp."$methodName" = methodBody
            exp.initialize()
            clazz.metaClass = exp
        }
    }
    

    As you can see, we only needed to add three methods to support component configuration through the structure.

    Plus we taught IDEA to understand that classes have these dynamic fields.

    image

    Now IDEA is aware of the existence of such fields as if they exist in the JavaFX API.

    Working with Bindable Properties


    Data binding is a wonderful thing. Our team uses this mantra - "If something can be done through binding, do it through binding." "... so as not to redo it later."

    Banding allows you to associate a data model and components. UI components themselves have binding properties that can be associated with the model data or build on the change of these properties logic - subscribe to data change events.

    A simple CheckBox example:

    CheckBox checkBox = new CheckBox();
    checkBox.selectedProperty().bindBidirectional(selectedProperty);
    

    And here we react to the event of clicking on the checkbox:

    CheckBox checkBox = new CheckBox();
    checkBox.selectedProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue value, Boolean before, Boolean after) {
            System.out.println("value = " + value);
        }
    });
    

    Use is convenient. It is not very convenient to describe such properties.

    Java offers such a scenario (IDEA code is automatically generated).

    private StringProperty name = new SimpleStringProperty(); // создали свойство
    //даем ссылку на свойство наружу (но не даем его изменять внешне)
    public StringProperty nameProperty() {
        return name;
    }
    // можно взять значение
    public String getName() {
        return name.get();
    }
    // даем возможность присвоить свойству новое значение
    public void setName(String name) {
        this.name.set(name);
    }
    

    Everything would be fine, and the IDE generates such code for us. Well, is it not stupidity? Why do we need to see all this? Behind all this rubbish we do not see our logic.

    Decision! We take the AST transformation that generates this code for us. When compiling.

    Our property (which we described in Java 10 lines) turns into Groovy in one line and looks like this:

    @FXBindable String name;
    

    @FXBindable you can take in GroovyFX , or you can take ours .
    We forked this annotation and you can get it from us on the github .

    Plus, in the same project you will find a file with the extension .gdsl , which will teach IDEA to use this annotation - autocomlite, etc.

    Such a transformation also creates the methods setName, getName, getNameProperty. Plus, the name () method is also added, which allows you to access the field by writing even less letters. Taste, but we most often use this particular method.

    this.nameInput.textProperty().bindBidirectional(this.name()) // this.name() - это наше строковое поле name
    

    Down with anonymous classes


    In the Menu example, we subscribe to events through anonymous classes. An example of the menu structure shows that the event handler is a “shell”.

    onAction: { t ->
        ProjectDialogs.newAsProjectDialog(scene, false)
    } as EventHandler

    All the magic in “as EventHandler” is that the body of the treasure moves to the body of the handle method of the EventHandler class. Using such a short record to handle events makes the code cleaner. By the way, smart IDEA offers a quick change to dynamic instantiation quickfix. You can also use another write - through Map ([handler1: {}, handler2: {}]), if the class handler requires you to overload several methods.

    XML work


    In our project, we needed to serialize the data model in XML and take it from disk. At first we wanted to use XStream out of habit, but we needed a more manageable structure - the Bindable properties of JavaFX are large, and converters are too lazy to write. Looked at JAXB, too bad. Also with Groovy XML serialization.

    The XmlSlurper built-in Groovy SDK came up.

    Each Bean model implements two methods - buildXml and buildModel - serialization and deserialization

    Closure buildXml(Project project) {
       return {
    		  'launcher'(launcherType)
          'browser-path'(browserPath)
          'nodejs-path'(nodejsPath)
          'console-value'(console)
        }
    }
    @Override
    void buildModel(Object node) {
        launcherType = node.'launcher'
        browserPath = node.'browser-path'
        nodejsPath = node.'nodejs-path'
        console = node.'console-value'
    }
    

    The buildXml method returns a structure as a bookmark. The magic here is in calling and assigning non-existent methods and properties. If a nonexistent method is called, then a property is created in the form of a child node, if a value is assigned to a nonexistent field, an XML attribute is created, if a nonexistent method is called and a pass is passed to it as a parameter, then an embedded XML node structure is created.

    The buildModel method takes a node argument, and parses the node through dynamic requests.

    Work with files


    Our program works a lot with the file system. Using Groovy we were able to greatly reduce the IO code. We did not need to save every nanosecond, we did not have a busy web server, and what Groovy did for us a lot of work suited us.

    The Groovy SDK offers many useful extensions for Java classes including File. For example, the ability to write / read the contents of a file simply through the “text” field, or work with lines of a file using “splitEachLine”.

    In addition, we liked AntBuilder, which can also be used to search and filter files.

    The following example copies files:

    def ant = new AntBuilder()
    ant.sequential {
        myDir = "test/to/"
        mkdir(dir:myDir)
        copy(todir:myDir) {
            fileset(dir:"text/from/") {
                include(name:"**/*.*")
            }
        }
    }
    

    You can search for files by pattern using fileScaner:

    def ant = new AntBuilder()
    def scanner = ant.fileScanner {
        fileset(dir: file) {
            include(name: "**/*.jpg")
        }
    }
    scanner.each{ printlt(it) }
    

    And of course AntBuilder is a full-fledged ANT, with all its extensions and capabilities. There is still to study and study. Gradle also uses AntBuilder, and the fact that there you can "twist" us is impressive.

    Using GPath to work with Nodes


    Since the structure of components in JavaFX, we used requests for nodes as collections. With this approach, getting rid of a large number of loops, we greatly reduced our code.

    For example, to remove scrolling in Java:

    webView.getChildrenUnmodifiable().addListener(new ListChangeListener() {
        @Override
        void onChanged(ListChangeListener.Change change) {
            Set scrolls = webView.lookupAll(".scroll-bar");
            for (Node  scroll : scrolls) {
                scroll.setVisible(false);
            }
        }
    });
    

    Same thing on Groovy:

    webView.childrenUnmodifiable.addListener({ change ->
        webView.lookupAll(".scroll-bar")*.visible = false
    } as ListChangeListener)
    

    Fighting NPE


    The operator "?." - in our opinion, only he alone can make you think about switching from Java to Groovy.

    model?.projectSettings?.projectPaths?.livePaths?.each{ println(it) } 
    

    We translate this into Java and get at least twenty lines of code.

    Conclusion


    That's probably all that we could remember. Of course, we used other Groovy “goodies” in our project, but if you list everything, we will go beyond the scope of the article, and there are a lot of textbooks on Groovy.

    But I want to talk about what does not suit us from Groovy. First, we avoided unnecessary dynamics. In our team, we agreed that it is necessary to specify the type when creating any variable or field (except for the parameters of the stockings - here half the pleasure of them is lost). Also, we did not use mixins and overloaded operators. We consider code juggling a bad practice - not only compact, but also controlled, supported code is important to us. That's probably all. Groovy is very similar to Java and we used it in this context - we know that AST transformations are performed for us during compilation, and when we write the code, we assume that something else is added automatically to some construction for us. Such is Java with auto-generation. And we don’t need anything else.

    Project sitecodeorchestra.com

    Also popular now: