As promised in Part 1, it's now time to build a more useful example to try out if/how data-classes work in a ZK application. Finally wrapping up to check how Kotlin might help when writing the oh-so-dreaded test cases.

Building a Simple CRUD UI

The screenshot on the left shows a basic CRUD UI (without any fancy styling, just concentrating on the essentials) for Persons (with an Address).

As the acronym indicates, it is possible to Create new and Read/Update existing Persons, as well es Deleting them.

Using ZK's-MVVM pattern should be straightforward, so this article can concentrate on how to use idiomatic Kotlin features during the implementation and how to overcome obstacles on the way.

Let's dive into the implementation.

Using Data Classes

Data classes are created in a breeze — just define the properties and their default values.

java.lang.RuntimeException: zk.kotlin.vm.Person is final
at javassist.util.proxy.ProxyFactory.checkClassAndSuperName(ProxyFactory.java:804)
at javassist.util.proxy.ProxyFactory.makeSortedMethodList(ProxyFactory.java:826)
at javassist.util.proxy.ProxyFactory.computeSignature(ProxyFactory.java:836)
at javassist.util.proxy.ProxyFactory.createClass(ProxyFactory.java:398)
at org.zkoss.bind.proxy.ProxyHelper.createFormProxy(ProxyHelper.java:246)
at org.zkoss.bind.impl.FormBindingImpl.initFormBean(FormBindingImpl.java:81)
...

"OK", I thought. Fair enough, classes are final by default in Kotlin (preventing Java-proxies from generating subclasses dynamically). No problem, I just declare it open instead, but then ...

... better RTF(riendly)M.

Luckily, especially for those cases, there are compiler plugins (called allopen and noarg) that generate extensible classes compatible with Java-proxies (also useful when dealing with Hibernate entity classes or Spring proxies).

The allopen plugin makes compiled classes open so they can be extended at runtime, but still keeps them final to avoid unintended programming mistakes.

In a similar way, the noarg plugin adds a no-arg-constructor, allowing runtime instantiation of generated proxy classes without exposing it to the developer unnecessarily.

We all know boilerplate code(-comments) like this, which complicate our well thought-out API - allowing for Person instances with uninitialized properties if used improperly:

Other Goodies

1. Worth mentioning is the commands-object in PersonCrudViewModel. Aggregating the commands, the ViewModel wants to expose to the ZUL file (almost don't need to mention anymore: in a refactoring-safe manner).

In the ZUL file, they can be accessed via @command(vm.commands.SAVE) or as in my example shortened by a reference binding cmds="@ref(vm.commands)" -> @command(cmds.SAVE)(personCrud.zul Line 11 / Line 45).

2. Kotlin's (setter/getter functions) can be used for calculated view model properties with dependencies.

The property personToEdit holds the current person being edited. Whenever it is changed in the code, I want the UI to update (i.e. all data-bindings referring to this instance to reload). The depending flag editing is notified programmatically to render/re-render/detach the editPersonForm accordingly.

In each case, the setter-function is called updating the field, and notifying the UI to reflect the changes automatically.

Testing With ZATS

ZK comes with a testing framework called ZATS, which allows testing ViewModels/Controllers by mimicking the useragent without the complexities and maintenance effort of real browser tests (e.g. using Selenium — still Selenium tests are important to verify the final result is working in each targetted browser). The goal with ZATS is to test as much interaction between ZK and your application as possible without having to deal with real browser specifics. As a benefit, the test-cases run much faster — encouraging to run them more often during development.

Unfortunately — not being originally designed for Kotlin — ZATS uses a reserved word, as, as a function name so it needs to be escaped using backticks `as`. On top of that, the references to Java classes aren't beautiful either. With a simple extension function for the ComponentAgent interface, this can be streamlined using an Inline Function with Reified Type Parameters (sounds frightening at first — but is well-explained). I often miss such a feature in Java — maybe Project Valhalla will add this in future Java versions.

To increase maintainability, the component selectors can be defined above the test functions, which makes them reusable in multiple test methods. Adjusting them in one place should be sufficient when pages are restructured or component IDs change.

At the same time, repeated component casting and property access can be extracted into additional extension functions (here done by asTextbox/asLabel). I might be over-engineering things a little, so decide for yourself what's useful for your eyes.

Since most query operations are performed on the same desktopAgent object, Kotlin's with() {} is a match-made-in-heaven to further reduce distracting code (Line 10 in the code above).

The line nameInput.input("Tester") will actually call this.nameInput.input("Tester"), where this refers to the DesktopAgent object returned by client.connect(...).

I Want a More Complex Example

Going back the PersonCrud ... a test case for all CRUD-functions of the above example can be implemented in just under 100 lines without looking too cryptic. See for yourself: PersonCrudTest.kt.

This includes chained component selectors (e.g. to get the delete button after the 2nd person in the personList — Line 59)

Because personEdit is a calculated value, its result can change with each evaluation. That's the reason we can use it multiple times to assert changes between NULL and non-NULL values. We can't accidentally reuse a stale value.

In lines 5 and 14, assert[Not]Null(personEdit) will query the personEdit each time to check the editPerson element is actually removed from the page after clicking the save-button (in line 12).

Epilogue

In the end, this was an insightful experience for me and I am looking forward to future language developments be it Kotlin or Java 10, 11 ... or something different. It's great to see how much even a modern programming language such as Kotlin cares about existing APIs/Frameworks allowing for a smoother transition or, as in my case, just experimenting without having to relearn everything from scratch.