⬆️ ⬇️

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 builders, everything looks very modern.

')

Total Data Driven Development . We adore it. The logic based on the data bundle clears the code from trash, hetera / setters - “Down with!”. Work with data change events, bi-directional "binding".



FXML . Great thing for prototyping. Understandable 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 ordinary code. Simply supporting FXML is harder than code - you always have to edit two files, code and FXML. Plus when using code it is easier to use inheritance.



Nodes . Component structure. You can run on a tree. You can search by lookup (). Like in the DOM. Straight jQuery write.



CSS . This is really a thing. "We throw off" components through one common css-file. ID tags, CSS classes, selectors and pseudo selectors.



Text Engine . Very good engine for complex texts.



WebView . We realize the heaped components on the Webkit engine. Read about this previous article .



Which is not very good



This is good. What is wrong? JavaFX script at one time did not just come up with. Creating fields for accessing Bindable data through heters and setters is a step back and yesterday. Java is not very good here. In Java 8 there are lambda expressions, but their appearance is also the answer to the question that something needs to be done with Java and a reason to think about a more radical solution.



Groovy!



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



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



By the way, Groovy ranks 18th in terms of language popularity (according to TIOBE ).



Our practices



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



Configuring Components



Simply create an instance of the component through the code and configure it.

In Java, we had to step by step, line by line, 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 thing look like if you rewrite it to Groovy?



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


Gruvy, let me remind you, who does not know, allows access to 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 the value - text = “Add”. Plus, when compiling Groovy classes, getters and setters are added automatically to public fields. Therefore, it is not hard to call the set / get method out of the groove if there is no real need for this.



And in the constructor parameters you can pass pairs - name: value (in fact, this is the usual HashMap and the syntax is used here 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 the components immediately suggests whether it is possible to configure the structure of the components immediately?



Can!



 menus.addAll( new Menu(text: "File", newItems: [ new MenuItem( text: "New Window", onAction: { t -> ApplicationUtil.startAnotherColtInstance() } as EventHandler<ActionEvent>, 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<ActionEvent> ), newJs = new MenuItem( text: "New JS Project", id: "new-js", onAction: { t -> ProjectDialogs.newJsProjectDialog(scene, false) } as EventHandler<ActionEvent> ) ]), new SeparatorMenuItem(), new MenuItem( text: "Open Project", onAction: { t -> ProjectDialogs.openProjectDialog(scene, false) } as EventHandler<ActionEvent>, 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<ActionEvent> ), ]), new SeparatorMenuItem(), save = new MenuItem( text: "Save Project", id: "save", onAction: { t -> ProjectDialogs.saveProjectDialog() } as EventHandler<ActionEvent>, accelerator: new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN), disable: true ), saveAs = new MenuItem( text: "Save As...", onAction: { t -> ProjectDialogs.saveAsProjectDialog(scene) } as EventHandler<ActionEvent>, accelerator: new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN), ), new MenuItem( text: "Close Project", onAction: { t -> ProjectDialogs.closeProjectDialog() } as EventHandler<ActionEvent>, accelerator: new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), ), new SeparatorMenuItem(), new MenuItem( text: "Exit", onAction: { t -> ApplicationUtil.exitColt() } as EventHandler<ActionEvent> ), ]), new Menu(text: "Help", newItems: [ new MenuItem( text: "Open Demo Projects Directory", onAction: { t -> ProjectDialogs.openDemoProjectDialog(scene) } as EventHandler<ActionEvent> ), new MenuItem( text: "Open Welcome Screen", onAction: { t -> ProjectDialogs.openWelcomeScreen(scene) } as EventHandler<ActionEvent> ), ]) ) 


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



Dynamic properties and methods



An attentive reader will ask, what is the Menu field “newItems”? Yes, the Menu class has no such 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 there is only “getItems ()” and it is impossible to assign a new value. Read-only. To To configure the Menu as a structure, we added a dynamic field.



Adding such a field is very simple, but our Java entity has long resisted such sedition as dynamic methods. We thought a lot of bicycles until we came to terms with the fact that it was necessary to take advantage of the dynamics. And everything turned out to be simple and scary.



Adding dynamic fields we carried out in a separate class GroovyDynamicMethods. Here is his 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<MenuItem> it -> children.addAll(it) }) addSetter(Menu, "newItems", {List<MenuItem> 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 the configuration of components 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 were in the JavaFX API.



Work with Bindable Properties



Data binding is a great thing. Our team uses such a mantra - “If something can be done through bandinding, do it through banding”. "... so as not to redo it."



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



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<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> value, Boolean before, Boolean after) { System.out.println("value = " + value); } }); 


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



Java offers such a script (the code is generated by IDEA automatically).



 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 is good, and the IDE for us generates such code. Well, not stupid? Why do we need all this to see? Behind all this stuff we do not see our logic.



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



Our property (which we described in Java in 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 fork this annotation and you can take it from us on the githaba .



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



This transformation also creates methods setName, getName, getNameProperty. Plus, the name () method is added, which allows you to access the field by writing even fewer letters. It tastes good, but we most often use this 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. On the example of the menu structure, it can be seen that the event handler is a “cover”.



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


All the magic in “as EventHandler” - the body of the cokure is moved to the body of the handle method, the class EventHandler. Using such a brief entry to handle events makes the code cleaner. By the way, smart IDEA offers Quiche “Change to dynamic instantiation”. You can also use another one to write - via Map ([handler1: {}, handler2: {}]), if the class handler requests to overload several methods.



Work with XML



In our project, we needed to serialize the data model to 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 big and the converters are too lazy to write. We looked at JAXB, too bad. The same with Groovy XML serialization.



Fit built into Groovy SDK XmlSlurper.



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 the structure as a clause. The magic here is in calling and assigning non-existent methods and properties. If a non-existent method is called, then a property is created as a child node, if a value is assigned to a non-existent field, an XML attribute is created, if a non-existent method is called and a clime is passed to it as a parameter, then a nested XML node structure is created.



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



Work with files



Our program works a lot with the file system. Using Groovy, we were able to greatly reduce the IO code. We didn’t have to save every nanosecond, we don’t have a loaded web server, and Groovy did a lot of work for us.



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 file lines using the "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. Here still study and study. Gradle also uses AntBuilder, and the fact that there you can "bloat" impresses us.



Using GPath to work with Nodes



So the structure of components in JavaFX, we used requests to the nodes as collections. With this approach, getting rid of a large number of cycles, we have greatly reduced our code.



For example, to remove scrolls in Java:



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


Same 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, in our project we used other Groovy “goodies”, but if we list everything, we will go beyond the scope of the article, and you can find a lot of Groovy textbooks.



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 collar - half the pleasure from them is lost here). Also, we did not use mixins and overloaded operators. We consider code juggling as a harmful 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 during compilation and we, when writing the code, assume that something else is added to us for some construction by an automatic machine. Such is Java with autogeneration. And we don't need anything else.



Project site codeorchestra.com

Source: https://habr.com/ru/post/199050/



All Articles