Java Book Club BOF has been holding a "Kotlin in Action" book club since January 2018.
If you want to learn Kotlin with all your might, I decided to rewrite the GUI in which the Hello world message flows in the JavaFX I created earlier in Kotlin because I wanted to make something.
This article aims to replace the above Java code with Kotlin. Therefore, please refer to the above blog for the explanation of JavaFX.
Store the created code in the following repository (URL of repository viewer)
http://www.torutk.com/projects/swe/repository/revisions/master/show/learn/kotlin/javafx/MessageBoard
If you want to avoid the trouble of building an environment and utilize a lot of coding support such as code completion, you can almost choose IntelliJ IDEA, the development environment of JetBrains, which develops Kotlin, or Android Studio, which is based on IntelliJ IDEA. is.
In the article "Hello world flowing with JavaFX", I introduced how to create it in the Java command line environment. Knowledge of the Java runtime environment is useful and should be known. However, in the case of Kotlin, there is a lot of work in preparing the Kotlin environment in addition to the Java environment, and I do not have much opportunity to utilize that knowledge, so this time I will use the IDE from the beginning.
IntelliJ IDEA Community Edition IntelliJ IDEA is a commercial development environment product, but Community Edition is provided free of charge. Kotlin's development environment is included in Community Edition, so I will use it this time.
I think that it can be used in Android Studio (also provided free of charge by Google) in almost the same way, but I have not verified it.
Pleiades, a familiar Japanese localization tool in Eclipse, also supports Japanese localization of IntelliJ IDEA. Next, I'm writing a little introduction method.
In the Japanese Windows environment, European fonts are beautiful, but Japanese fonts are MS Gothic and jagged. Next, I'm writing a setting to clean the font a little.
In IntelliJ IDEA, create a project for a flowing Hello world program.
MessageBoard.kt
class MessageBoard {
}
From here, we will call JavaFX and create a flowing Hello world.
Call JavaFX in Kotlin and program the GUI for the flowing Hello world message.
JavaFX inherits the Application class and prepares a class that will be the entry point for JavaFX processing.
Let the class created by the application inherit the JavaFX Application class.
MessageBoard.kt
class MessageBoard : Application {
}
In Kotlin, inheritance is specified by a colon symbol. Here, the Application class needs to be imported, so in the IntelliJ IDEA code you will see a pop-up message like this:
Follow the pop-up message and press Alt + Enter to select javafx.application.Application from the candidate list.
An import statement is generated.
MessageBoard.kt
import javafx.application.Application
class MessageBoard : Application {
}
However, it is still incomplete and is in a compile error state. The following screen shows the situation where the error content is popped up.
You need to define a start method. From the Code menu> Implement Method, generate the abstract method start of the Application class.
The generated code is:
MessageBoard.kt
import javafx.application.Application
import javafx.stage.Stage
class MessageBoard : Application {
override fun start(primaryStage: Stage?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
However, there are still errors. A red wavy line is drawn below the Application. When you move the cursor to Application, the error content will pop up as follows.
The message is "This type has a constructor, and thus must be initialized here". What is this? Details will be described in the next section.
“Kotlin in Action” on page 106 of Section 4.2.1
open class Button ← Generates default constructor with no arguments
If you inherit from the Button class and don't have a constructor, you need to explicitly call the superclass's constructor, even if the superclass's constructor has no arguments.
class RadioButton : Button()
So, since we don't define a constructor in the MessageBoard class this time, the correct answer is to put parentheses in the inherited superclass.
MessageBoard.kt
import javafx.application.Application
import javafx.stage.Stage
class MessageBoard : Application() {
override fun start(primaryStage: Stage?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
Now you have the code with the compilation error removed.
But I still can't do it. Define the main method that is the entry point of the program. In Kotlin, instead of defining the main method as a static method in a class like Java, it is defined as a top-level function.
MessageBoard.kt
class MessageBoard : Application() {...}
fun main(args: Array<String>) {
//Write the code that calls the launch method of the Application class here
}
Kotlin does not have static methods / static fields, but it is possible to access static methods / static fields defined in Java classes, and their calling expressions are the same as in Java.
The JavaFX Application class launch method has different arguments when called from within a subclass of the Application class and when called from outside that subclass. This time, since it is from outside the subclass, Java calls the launch method of the following format.
public static void launch(
Class<? extends Application> appClass,
String... args
)
There are two challenges here:
The first is to fetch the class reference (KClass type) with :: class for the class, and then call and get the extended property java of KClass type.
The second uses the spread operator to expand the array.
fun main(args: Array<String>) {
Application.launch(MessageBoard::class.java, *args)
}
Where to specify variadic arguments with the spread operator, see "Kotlin in Action", Section 3.4.2 (p.76).
Kotlin needs to explicitly retrieve the contents of the array, so that all the elements of the array are independent arguments to the called function. Technically, this feature is called using the spread operator, but it's actually as simple as putting a * in front of the corresponding argument.
fun main(args: Array<String>) { val list = listOf("args: ", *args)← Spread operator retrieves the contents of an array print(list) }
There is.
This completes the minimal implementation. Let's run it (although the window isn't displayed yet).
If you define the main function at the top level of the file, the following icon will be added to the left side of the line.
Click this icon to pop up the Run / Debug / Run Coverage options. Select Run.
The code will then be built and executed. When you execute from this icon for the first time, [Execute] is enabled from the [Execute] menu, and the class name (file name) of the file in which the main function is defined in the list in [Execute] menu> [Execute startup configuration]. A class with the name Kt added to) will be added and can be selected.
When executed, the template implementation code TODO of the start method is called to display an exception.
"C:\Program Files\Java\jdk-9\bin\java" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\lib\idea_rt.jar=52509:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "D:\work\java\repos\swe.primus\learn\kotlin\javafx\MessageBoard\out\production\MessageBoard;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-reflect.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-test.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk7.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk8.jar" MessageBoardKt
Exception in Application start method
Exception in thread "main" Exception in thread "Thread-2" java.lang.RuntimeException: Exception in Application start method
at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:973)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:198)
at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: kotlin.NotImplementedError: An operation is not implemented: not implemented
at MessageBoard.start(MessageBoard.kt:6)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:919)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(PlatformImpl.java:449)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:175)
... 1 more
java.lang.IncompatibleClassChangeError
It's finally time to implement. First, describe the visualization of stage with the start method. As a Java programmer, I would code the following, which is a compilation error. The point is that the argument type is Stage? And has a? At the end.
MessageBoard.kt
override fun start(primaryStage: Stage?) {
primaryStage.show()
}
The compilation error is displayed in the following pop-up.
The argument types of the start method are Stage? Type and nullable type. Therefore, it is necessary to check for null before calling the method or to call the method for null.
Here, if the argument primaryStage is null, nothing is done, so the show method is called with the safe call operator (?.).
MessageBoard.kt
override fun start(primaryStage: Stage?) {
primaryStage?.show()
}
In "Kotlin in Action" Section 6.1.3 (p.181)
The safe-call operator ?. It combines null checking and method invocation into a single operator. (Omitted) That is, if the value you are trying to call a method is not null, the method call will be executed normally, if the value is null, the call will be skipped and null will be used as the value instead.
There is.
When run, an empty window will appear.
Display the text in an empty window.
First, generate a Text that displays the characters. The difference from Java is that there is no new operator, and the variable type is declared by immutable val.
MessageBoard.kt
val message = Text("Hello, world. This is JavaFX from Kotlin.")
Create a group to group JavaFX nodes together. Instance creation and variable types are similar to Text above. Next, what was called setLayoutY (50d) in Java becomes an assignment statement to a property in Kotlin. Also, in java, Kotlin does not have a D (or d) that indicates a Double numeric literal, so the decimal point is specified as .0. By the way, if you write 50 and an integer (Int type) literal, an error will occur if the types are different.
MessageBoard.kt
val group = Group(message)
group.layoutY = 50.0
Generate a Scene. As for the size (width, height), if the type is different for integers, an error will occur, so the decimal point is specified. Also, in java, it is set to primaryStage with the setScene method, and in Kotlin, it is assigned to a property.
MessageBoard.kt
val scene = Scene(group, 320.0, 160.0)
primaryStage?.scene = scene
The entire start method looks like this:
MessageBoard.kt
override fun start(primaryStage: Stage?) {
val message = Text("Hello, world. This is JavaFX from Kotlin.")
val group = Group(message)
group.layoutY = 50.0
val scene = Scene(group, 320.0, 160.0)
primaryStage?.scene = scene
primaryStage?.show()
}
I will do it.
Use JavaFX animations to move the text from right to left.
In the start method, define TranslateTransition for animation.
MessageBoard.kt
val messageTransition = TranslateTransition(Duration.seconds(8.0), message)
messageTransition.fromX = 320.0
messageTransition.toX = -320.0
messageTransition.interpolator = Interpolator.LINEAR
messageTransition.cycleCount = TranslateTransition.INDEFINITE
Again, the difference is that you don't need the new operator and the set method is an assignment to a property, but it's almost the same as Java code. Also, call play () at the end.
MessageBoard.kt
messageTransition.play()
When executed, the flowing Hello world will be displayed. The following is an animated GIF of the displayed screen at 10fps (the crazy thing is the problem of FPS when making an animated GIF, JavaFX draws at a maximum of 60fps).
In the above code, the description of TranslateTransition is redundant for Kotlin (property assignment to local variable appears repeatedly), so apply is used to reduce the amount of description. The entire start method after reduction is shown below.
MessageBoard.kt
override fun start(primaryStage: Stage?) {
val message = Text("Hello, world. This is JavaFX from Kotlin.")
val group = Group(message)
group.layoutY = 80.0
val scene = Scene(group, 320.0, 160.0)
primaryStage?.scene = scene
primaryStage?.show()
TranslateTransition(Duration.seconds(8.0), message).apply {
fromX = 320.0
toX = -320.0
interpolator = Interpolator.LINEAR
cycleCount = TranslateTransition.INDEFINITE
}.play()
}
Finally, set the color and size of the message text.
MessageBoard.kt
override fun start(primaryStage: Stage?) {
val message = Text().apply {
text = "Hello, world. This is JavaFX from Kotlin."
fill = Color.DARKMAGENTA
font = Font.font("Serif", FontWeight.SEMI_BOLD, 32.0)
}
val messageWidth = message.layoutBounds.width
val messageHeight = message.layoutBounds.height
val group = Group(message)
group.layoutY = messageHeight * 2
val scene = Scene(group, messageWidth, messageHeight * 3)
primaryStage?.scene = scene
primaryStage?.show()
TranslateTransition(Duration.seconds(8.0), message).apply {
fromX = messageWidth
toX = -messageWidth
interpolator = Interpolator.LINEAR
cycleCount = TranslateTransition.INDEFINITE
}.play()
}
I will do it.
This time, the goal is to put it together in a viable JAR.
In the NetBeans IDE, when I created a Java project, it created a JAR file that could be executed without any additional settings.
After investigating that IntelliJ IDEA should be able to do it, you can generate an executable JAR file by following the steps below.
You must specify the main class. In Kotlin, the main function is written at the top level of the file. When compiled into Java bytecode, it will be a class named after the file name (included in the class MessageBoardKt if the main function is defined at the top level of the MessageBoard.kt file).
When building and executing with IntelliJ IDEA, the following class files will be generated under out \ production \ MessageBoard under the project directory.
Examine the methods defined in each class file with the JDK's javap command.
D:~> javap MessageBoard
Compiled from "MessageBoard.kt"
public final class MessageBoard extends javafx.application.Application {
public void start(javafx.stage.Stage);
public MessageBoard();
}
D:~>javap MessageBoardKt
Compiled from "MessageBoard.kt"
public final class MessageBoardKt {
public static final void main(java.lang.String[]);
}
The class that has the main method is MessageBoardKt.class. So, in the continuation of the previous one, specify the main class on the next screen. Click […] at the right end of the main class column.
This will bring up the Select Main Class screen. On the Search by Name tab, enter part of the main class name (here "M") as it is initially blank. Then, the class corresponding to that name will be listed, so select [MessageBoardKt] here.
The JAR file field from the library is set to Extract to target JRA by default. This will include the library's class files in the executable JAR file when using a non-Java SE standard library. [Copy to output directory and link via manifest] is the difference between preparing a library file separately from the executable JAR file and writing a reference to the library file in the manifest in the executable JAR file.
This time, we will leave the default so that it can be executed by distributing only one JAR file. Click OK to display the configuration as shown below.
If you check [Include in project build], a JAR file will be generated when the build is executed. If you don't like the time it takes, leave it unchecked and choose Build menu> Build Artifacts to generate the JAR file. Select Build / Rebuild with the name of the configuration you just created.
The JAR file was generated in the project directory out \ artifacts \ MessageBoard_jar \ MessageBoard.jar. The capacity is as large as 3.3MB. This is because it also includes the Kotlin execution library.
If you distribute this JAR file independently, you can execute the program created by Kotlin in the environment where JRE is included.
If the following code starts to feel verbose,
val messageWidth = message.layoutBounds.width
val messageHeight = message.layoutBounds.height
I want to apply the decomposition declaration
val (messageWidth, messageHeight) = message.layoutBounds
I want to write. However, the Java class does not define the methods component1 (), component2 (), ... for applying the decomposition declaration.
Therefore, define it with an extension method.
operator fun Bounds.component1(): Double = width
operator fun Bounds.component2(): Double = height
This will allow the Bounds class to be used in decomposition declarations.
However, this is a big overkill. It is "no grace" (it is unavoidable to grace it).
Recommended Posts