Kotlin from the ground up, Part 3 - creating a GUI App

0 27
Avatar for marley
Written by
3 years ago

In Part 2, you had a taste of how Kotlin works from the interactive command line (REPL). Now let's try our hand at coding a GUI Hello World app.

One of the major reasons for using Java to code GUI apps is so that you can have apps running on Windows, MacOS or Linux using not only a single source code base, but also without even having to recompile your class files/jar! E.g. the same - essentially executable - file can run unmodified on all three OSes!

We will be using RxKotlinFX and TornadoFX - which provides Kotlin extensions to the RxJavaFX and JavaFX frameworks, making for very concise, expressive Kotlin code compared to how it would otherwise be expressed in Java. You can, of course, call the respective Java APIs directly from Kotlin if/when necessary.

Download tornadofx-x.y.z.jar from https://github.com/edvin/tornadofx/releases/latest and place it in a new, empty directory together with your main source file Hello.kt below:

import tornadofx.*

class MyApp: App(MyView::class)

class MyView: View() {
    override val root = vbox {
        button("Hello, press me!")
    }
}

MyApp is derived from App and serves as the entry point to the application. MyApp is fed with MyView which derives from and customizes a View by displaying a single button contained within a vbox (vertical box container). The code is the shortest, easiest-to-understand GUI Hello World I've yet to come across and it only gets better as you start to see the benefits of reactive programming.

As usual in Java land, writing the code seems far easier than getting things to build and run, and this is what the rest of the post is going to address.

Compiling and running for JDK 8 (10?) and below:

JavaFX is still part of the JDK in Java 10 and below, so you should be able to get away with including only the TornadoFX framework in the compile-time classpath:

> kotlinc -cp tornadofx-x.y.z.jar Hello.kt

You can now run the program by calling the main class MyApp as shown below:

> java -cp "/usr/local/kotlinc/lib/*:*:." MyApp

In the run-time classpath -cp above, /kotlinc/lib/* points to the kotlin runtime libraries, * brings in tornadofx-x.y.z.jar (and possibly other jars in the current directory) and . is so java can see the MyApp.class in the directory from where it is invoked. Hint: remember to use ; instead of : as classpath separator under Windows.

Note: I am using JDK 8 and as the Java Module Platform System (JMPS) was introduced in Java 9, I'm not sure if you might also need to adjust the invocation to be more like that shown below for JDK/JFX11 and up.

Compiling in JDK 11 and above:

You will need to download JavaFX separately. Get a suitable version for your platform from https://gluonhq.com/products/javafx and unpack it.

As JavaFX is no longer part of the JDK, you have to explicitly include its JAR files when compiling. To save effort in typing, first create a text file that contains the needed classpath as shown below. Hint: remember to use ; instead of : as classpath separator under Windows.

-cp "tornadofx-1.7.20.jar:/javafx-sdk-dir/lib/javafx.base.jar:/javafx-sdk-dir/lib/javafx.graphics.jar:/javafx-sdk-dir/lib/javafx.controls.jar"

Replace /javafx-sdk-dir/ with where you unzipped to, then save the snippet above as a text file called, say, classpath. Then you can invoke the Kotlin compiler as below:

> kotlinc @classpath Hello.kt

After the above, you will see a bunch of .class files in the directory corresponding to the two classes in Hello.kt.

Running in JDK 11 and above:

If you are using a version of JavaFX that is modularized (definitely from JDK/JFX 11 and up and possibly in JDK 9 and 10), you have to load JavaFX as modules:

> java -cp "/usr/local/kotlinc/lib/*:*:." -p "/javafx-sdk-dir/lib" 
  --add-modules javafx.controls MyApp

Rather than pointing to the JavaFX JARs via classpath, you specify paths via -p instead and use --add-modules to specify individual modules. A module is contained in a respective JAR file, which is usually named the same as the module itself.

To test whether a JAR file is modularized or not, you can use jar -d or jar --describe-module (should be available from Java 9 and up).

# old, unmodularized JavaFX runtime JAR file
> jar -f /jdk8/jre/lib/jfxrt.jar -d
No module descriptor found. Derived automatic module.

jfxrt automatic
requires java.base mandated
contains com.sun.deploy.uitoolkit.impl.fx
contains .... 
contains .... 
contains .... 

# modularized JavaFX JAR files 
> jar -f /javafx-sdk-14.0.1/javafx.base.jar -d
javafx.base jar:file:///d:/javafx-sdk-14.0.1/lib/javafx.base.jar/!module-info.class
exports javafx.beans
exports ...
exports ...
requires java.base mandated
requires ...
requires ...
qualified exports com.sun.javafx to javafx.controls javafx.fxml javafx.graphics javafx.swing
qualified exports ... 
qualified exports ...

As you can see from the above, older, non-module JAR files show a No module descriptor found message and are turned into a kind of automatic module by JMPS.

[ Aside: Java modules come with additional complications and may seem overengineered, but they have enormous benefits if you're compiling big systems that rely on a lot of packages. For now, just think of modules as JAR files, but with additional info attached to that greatly reduces JAR hell. ]

Result of running

With any luck, you should get a window containing a button saying "Press Me" as shown below for MacOS and Windows (Linux will show something similar as well).

In my case, I compiled on Windows and was able to run the class files from both Windows and MacOS to get the above. Linux should work the same too.

Compiling and running from JAR files

As your program becomes more complex and the number of class files grow, you can start compiling them into JAR files (which are just zip files with a bit of additional metadata) before distributing them to end-users. Below is how to do it, the differences again having to do with the module system introduced in Java 9 and the uncoupling of JavaFX from the JDK in later JDKs.

in Java 8 (10?) and below

> kotlinc -include-runtime -cp tornadofx-x.y.z.jar Hello.kt -d hello.jar

> java -cp * MyApp

Here you add the -include-runtime option so that the kotlin runtime is bundled inside the jar, eliminating the need to put it in the classpath. You can now invoke java with -cp pointing to the directory where hello.jar and tornadofx-x.y.z.jar (non-modules) are and invoke the MyApp class explicitly.

For the traditional way of running a jar file:

> java -jar hello.jar
no main manifest attribute, in hello.jar

you can fix the "no main manifest attribute" error by adding a Main-Path and Class-Path attribute in the META-INF/MANIFEST.MF file inside hello.jar as shown below:

Manifest-Version: 1.0
Created-By: JetBrains Kotlin
Class-Path: tornadofx-x.y.z.jar
Main-Class: MyApp

As long as tornadofx-x.y.z.jar is in the same directory, java -jar hello.jar should now properly run the program.

in Java 11 and above

Because of the situation mentioned here you will need to invoke the jar differently in Java 11 and up. First, compile as below:

> kotlinc -include-runtime @classpath Hello.kt -d hello.jar

> java -p /javafx-sdk-dir/lib --add-modules javafx.controls -cp * MyApp

Then use -p to indicate directory where JavaFX modules are and load via --add-modules. You still use -cp to indicate where hello.jar and tornadofx-x.y.z.jar are and then call MyApp directly.

For JDK 11, I haven't studied yet how to modify the JAR manifest such that you can invoke it via jar -jar hello.jar so that is left as an exercise for the reader.

Update: I've discovered that the below works:

import com.github.thomasnield.rxkotlinfx.actionEvents
import tornadofx.*

class MyApp: App(MyView::class)

class MyView: View() {
  override val root = vbox {
    button("press me").actionEvents()
                  .subscribe { println(it) }
  }
}

fun main(args: Array<String>) {
  launch<MyApp>(args)
}

or, if you target earlier JVM versions:

import javafx.application.Application
import com.github.thomasnield.rxkotlinfx.actionEvents
import tornadofx.*

class MyApp: App(MyView::class)

class MyView: View() {
  override val root = vbox {
    button("press me").actionEvents()
                  .subscribe { println(it) }
  }
}

fun main(args: Array<String>) {
    Application.launch(MyApp::class.java,*args)
}

Caveats re JDK versions

I am doing this under JDK 8 and JDK 14 so steps for Java versions from 9 to 13 are guesses based on my understanding of the changes that occurred when the modules system was introduced in Java 9 as well as when Java FX was moved out of JDK 11.

It took a LOT of tears and sweat figuring out how to build and run successfully from the command line, but I pick up stuff that I won't understand if I immediately use build tools like Gradle, Maven, IDEs. Plus doing it this way is snappier for very small tutorial projects and the complex build tools have their own sets of grief to deal with.

If you experience issues or have any suggestions, post in the comments section below to see if I or others can help you out. Apart from the article, comments themselves can also be tipped with Bitcoin Cash (BCH)!

1
$ 0.00
Avatar for marley
Written by
3 years ago

Comments