Using the @FXML annotations in Kotlin to get at FXML


#1

Well, a picture is worth a thousand words, so here goes:

But to sumarize, there is first the question of how I should use @FXML at all. Finally I try to sidestep this by going the (possibly questionable) route of declaring the member to be public, but then it seems that kotlin doesn’t realize that JavaFX magic (read: I don’t know exactly how it works) is happening and takes care of the initialization.

Actually, there may be a more fundamental problem. I have fx:controller=“ui.mainController” in my root FXML element. According to intellij, this seems to look good, since it isn’t red, and if I make a typo in “ui.mainController” it will change to red. However, attempting to build with this fx:controller set yields the following stracktrace (now for an actual thousand words…):

FAILURE: Build failed with an exception.

  • What went wrong:
    Execution failed for task ‘:compileKotlin’.
    > Compilation error. See log for more details

  • Try:
    Run with --info or --debug option to get more log output.

  • Exception is:
    org.gradle.api.tasks.TaskExecutionException: Execution failed for task ‘:compileKotlin’.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
    at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:64)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:42)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
    at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure(AbstractTask.java:305)
    at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.executeTask(AbstractTaskPlanExecutor.java:79)
    at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:63)
    at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:51)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:23)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:88)
    at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:29)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
    at org.gradle.execution.DefaultBuildExecuter.access$200(DefaultBuildExecuter.java:23)
    at org.gradle.execution.DefaultBuildExecuter$2.proceed(DefaultBuildExecuter.java:68)
    at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
    at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:55)
    at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:149)
    at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:106)
    at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:86)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:80)
    at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:33)
    at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:24)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:36)
    at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
    at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:51)
    at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:171)
    at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:237)
    at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:210)
    at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:35)
    at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:24)
    at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:206)
    at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:169)
    at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
    at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
    at org.gradle.launcher.Main.doAction(Main.java:33)
    at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
    at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:54)
    at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:35)
    at org.gradle.launcher.GradleMain.main(GradleMain.java:23)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
    Caused by: org.gradle.api.GradleException: Compilation error. See log for more details
    at org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile.callCompiler(Tasks.kt:119)
    at org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile.compile(Tasks.kt:95)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:63)
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.doExecute(AnnotationProcessingTaskFactory.java:218)
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:211)

at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:200)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:579)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:562)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
… 45 more

Here is how mainController.kt looks:

package ui;

import javafx.stage.Stage
import kotlinfx.builders.Stage
import kotlinfx.builders.Scene
import kotlinfx.properties.root
import javafx.fxml.FXMLLoader
import javafx.scene.control.TabPane
import java.io.IOException
import javafx.fxml.FXML
import javafx.scene.control.TableView

class mainController : TabPane() {   val root: TabPane   public val logTable: TableView

  {
  try {
           root = FXMLLoader.load(
                   javaClass<LaRGUI>().getClassLoader()
                           .getResource(“main.fxml”)
           );
  } catch (e: IOException) { throw RuntimeException() }

  }
}


#2

Hi Brandon,

I can’t say I’ve played with JavaFX, but here’s a few general tips that might help:

  • "@FXML" is not an annotation in Kotlin, use unadorned “FXML”. ("@" is used for labels. In a few rare cases you need to put the annotation in square brackets)
  • You probably need vars and not vals and they must be initialised. You have two options:
    •  
      FXML var logTable: TableView<>? = null
      FXML var logTable: TableView<
      > by Delegates.notNull()


The first one just assigns it to null, but can be painfull if in reality it’s going to be not null when initialised.

The second one uses delegates to treat it as not null. This will throw a NPE on use if not initialised by JavaFX.

Also note you need to specify a type parameter for TableView as there are no raw types in Kotlin - I’ve used a projection (*), but replace it with a real value where sensible.

How well all of this works with JavaFX and with what you’re trying to do, I can’t really say.

Good luck!

Cheers,
Andrew


#3

Thanks Andrew,

This looks like a step in the right direction and also clears up a number of my misconceptions.

I think the more fundamental issue arises when I specify the Kotlin controller class (as mentioned above) within the FXML root element:

I’d like to look into this more, but frankly don’t know where to start. It appears that there occurs on line 26 with the FXML load. This will work fine if I don’t specify the controller class. Additionally, from the attached stracktrace, it looks as though JavaFX is having difficulty constructing an application that includes a kotlin controller class, but I can’t make much more sense of it than that.



stacktrace_javafx_kotlin.txt.zip (1.57 KB)

#4

The gradle compiler log doesn't tell us much, I'm afraid. You can use Kotlin with JavaFX though. Some of us are doing it.

Make sure your class doesn’t have any constructor parameters. FXML wants to construct the controller class for you, which means, you must have a no-args constructor.


#5

Good to know! But I don't believe my constructor class is requiring any parameters, as you mentioned. You can see it at the bottom of my first post, but since I'm not sure of how to best show useful output (please let me know if I miss something obvious), I've included the entire project as a zip file.



LaR.zip (1.03 MB)

#6

Hi, As I understand JavaFx, you have a recursion in loading mainController class because you load main.fmxl inside it and in main.fxml you have a reference to mainController class. To fix a recuirsion you should load mainController from main.fxml once outside of class itself. ex.

val root: TabPane = try {   FXMLLoader.load(javaClass<mainController>().getClassLoader().getResource("main.fxml")) } catch (e: IOException) { throw RuntimeException() }

class mainController : TabPane() {
  FXML var logTable: TableView<*> by Delegates.notNull()
}


#7

That was a very insightful observation, though I'm also a bit ashamed for not noticing at least the possibility.  I am now pretty sure it arose because I was reading two separate JavaFX examples on two different evenings and muddling them together in my mind. Many thanks to all on this error.


#8

A few followup issues. After taking some time to upgrade to M11 and mess around with getting kotlinfx and M11, I went back to this project and had some issues actually using logTabe. Using this:

 
class mainController : TabPane() {

  FXML var logTable: TableView<logEntry> by Delegates.notNull();
  // FXML var logTable: TableView<logEntry>? = null;
  init {
  // Some simple Test Code
  var x = logEntry()
  val xs: ArrayList<logEntry> = ArrayList()
  xs.add(x)
  var initList: ObservableList<logEntry> = FXCollections.observableArrayList(xs);

  logTable.setItems(initList) // line 36 or 37 in errors below
  }

}


The first declaration of logTable (uncommented above) results in the following error:

$ ~/gradle-2.3/bin/gradle ui
:compileKotlin UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:uiException in Application constructor
Exception in thread “main” java.lang.reflect.InvocationTargetException
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:483)
  at sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:767)
Caused by: java.lang.RuntimeException: Unable to construct Application instance: class ui.LaRGUI
  at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:865)
  at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$147(LauncherImpl.java:157)
  at com.sun.javafx.application.LauncherImpl$$Lambda$48/1401420256.run(Unknown Source)
  at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.reflect.InvocationTargetException
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
  at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$152(LauncherImpl.java:777)
  at com.sun.javafx.application.LauncherImpl$$Lambda$49/1431950915.run(Unknown Source)
  at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$166(PlatformImpl.java:323)
  at com.sun.javafx.application.PlatformImpl$$Lambda$45/1051754451.run(Unknown Source)
  at com.sun.javafx.application.PlatformImpl.lambda$null$164(PlatformImpl.java:292)
  at com.sun.javafx.application.PlatformImpl$$Lambda$47/197390767.run(Unknown Source)
  at java.security.AccessController.doPrivileged(Native Method)
  at com.sun.javafx.application.PlatformImpl.lambda$runLater$165(PlatformImpl.java:291)
  at com.sun.javafx.application.PlatformImpl$$Lambda$46/1775282465.run(Unknown Source)
  at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
  at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
  at com.sun.glass.ui.win.WinApplication.lambda$null$141(WinApplication.java:102)
  at com.sun.glass.ui.win.WinApplication$$Lambda$37/1109371569.run(Unknown Source)
  … 1 more
Caused by: java.lang.IllegalStateException: Property logTable should be initialized before get
  at kotlin.properties.NotNullVar.get(Delegation.kt:124)
  at ui.mainController.getLogTable(mainController.kt)
  at ui.mainController.<init>(mainController.kt:37)
  at ui.LaRGUI.<init>(main.kt:23)
  … 18 more
FAILED

By the way, since I updated the IntelliJ plugin to M11, it seems to have trouble finding Delegates in “import kotlin.properties.Delegates” (all occurrences of Delegates appears in red is noted as being an unresolved reference).

Commenting the first declaration of logTable out and going with the null asignment results in this:

$ ~/gradle-2.3/bin/gradle ui :compileKotline: C:cygwin64homebrand_000LaRsrcmainkotlinmainController.kt: (36, 17): Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type javafx.scene.control.TableView<datalog.logEntry>? FAILED

A quick google search showed I should be doing this, which fixes this problem (but not if I revert to the Delegates.notNull() method):
 

logTable?.setItems(initList)

So correct me if I’m wrong in desiring this, but is there a way to assert that logTable is not null, so that the first method will work, or so that I don’t need to use the safe call operator every time for something that must be safe (if we can’t get ahold of a fundamental UI element, we have big problems). Furthermore, maybe this is a problem with how Kotlin Delegates is working in general (at least in M11)?


#9

Arg, I think at least part of the Delegates problem is that my project/lib directory still had old files for:

kotlin-runtime.jar          kotlin-runtime-sources.jar


How do you update these? Either from IntelliJ or manually?

Edit: I found only the kotlin-runtime.jar in the plugin bundle, but this wasn’t enough to fix the undefined references issue in IntelliJ.


#10

If you delete the jars and run Configure Kotlin in Project action in the IDE, new jars will be put in (in fact the IDE should have suggested updating them automatically) It will also probably add kotlin-reflect.jar

One thing I don’t quite get here is why you have some jars in the lib/ directory when using Gradle in your build process


#11

Aha, this clears up several things.

Firstly, I’d been getting a message to configure kotlin, but each time I did this it seemed to mess up my gradle file (through no fault of the plugin - it is just that I already had a gradle file that was working for kotlin).
But it was trivial to fix the changes, which amounted to removing some of my older (redundant) gradle statements, and using ‘kotlin_version’ instead of ‘kotlinVersion’.

I deleted the lib directory … you are right, totally unnecessary.

Eventually I had to delete my entire .idea directory; things seemed to be in a bad state somehow (not only Kotlin-related things; ‘javaHome’ was not being detected, despite my specification not to use JAVA_HOME for gradle in IntelliJ). Now everything is going smoothly. See here for more info: https://devnet.jetbrains.com/message/5538168#5538168


#12

Well, I should note that the following issue still occurs if trying to use:

FXML var logTable: TableView<logEntry> by Delegates.notNull();


Caused by: java.lang.IllegalStateException: Property logTable should be initialized before get
at kotlin.properties.NotNullVar.get(Delegation.kt:124)
at ui.mainController.getLogTable(mainController.kt)
at ui.mainController.<init>(mainController.kt:37)
at ui.LaRGUI.<init>(main.kt:23)
… 18 more

But perhaps it is time for a new discussion for the sake of brevity.


#13

Hi.

I think that using Delegates.notNull() will not work without doing some additional “magic”.

Lets says that we have this declaration

FXML var tabPane: TabPane by Delegates.notNull()

At Kotlin Bytecode we can see that tabPane is converted into field_name$delegate and it's type is migrated to ReadWriteProperty. To set TabPane it is required to invoke setTabPane method which FXMLLoader will not do.

I think that in order to make FXML annotaton works we(community) have to create custom FXMLLoader and maybe some custom Delegator (witch allow make FXML fields as values not variables).

In my opinion someting like this should be a nice way to bind FXML components within controller class:

val tabPane: TabPane by bindFXML()

Where bindFXML() should be initialized by custom FXMLLoader

I will try do some proof-of-concept in few next days.


#14

Something else that might be quite nice is an FXML to Kotlin compiler. There's one for FXML to Java already. Parsing FXML is quite slow due to all the reflection, which the JVM doesn't optimise very well. Most of the time of course it's irrelevant but for performance sensitive apps it can be an issue.

By the way, FXMLLoader has a BeanAdapter system. I think, subclassing BeanAdapter and using the new Kotlin reflection tools might work better than making a custom FXMLLoader.


#15

Hi.

I have been thinking about some requirements and features that would be nice to have while working with Kotlin and FXML files.

Must have:

  1. Injected fields should be val’s not var’s.
  2. Simple and clear syntax.
  3. Runtime validation of declared fields.


Good to have:

  1. Do not copy and modify code from original FXMLLoader.
  2. Do not use private API.
  3. No need to inherit other class or implement trait.
  4. Compile time validation of declared fields.


Today I was experimenting with FXMLLoader class (and some of other classes that is uses) and I was able to create proof-of-concept app whic. It is avaliable at https://github.com/pablow91/KotlinFXML

I think it fulfils all must haves, but it has some problems with good to haves. Unfortunately FXMLLoader does not have any callback mechanism. I was able to plug into private methods which will not be a part of JavaFX standard.

The API is really simple. It consists two classes: bindFXML and bindOptionalFXML. First one inform that field is require and if it is not present IllegalStateException will be raised. The other one will be injected when proper Object is available, but will not raise exception when there is no such object. e.g.

val button : Button by bindFXML()
val optionalButton: Button by bindOptionalFXML()

There are other ways to consider. For example it is possible to inject all fields using only public API, but it would require Controller class to inherit from some AbstractController class which would store all values in map and during initialization inject all fields. It would be much less elegant solution, but it would always work - despite changes in internal API.

As Mike wrote it is also possible to create FXML to Kotlin compiler. I am not sure what is the best way to do it, but it probably require to rely on some private API or require rewriting FXMLLoader (usage code of existing FXML to Java should be investigated first). But it would make Controller syntax more verbose. Because it either create template class (with is really ugly solution) or either create abstract class which some abstract methods (but how to set generic type for e.g. ListView).


#16

Hello, Paweł

I review your code and think that you at the right direction. Your solution with bindFXML() looks pretty good.

I try to use your code on my project and retrieve several error messages. To fix it I firstly change default value of builder to allow use Font and PropertyValueFactory classes into .fxml file (they was no be initialized correctly). I found this solution in FXMLLoader class sources.

Secondly, I increase your tag deep counter in each callback with “begin…Element” name (It allow me use <children> tag in .fxml).

And at the last I replace your code which work only with Node class instances to my code which require getId method presence (it allow me bind TableColumn class instances).

Update

I change type of KotlinFXMLLoader from object to class to enable class calling from java code. To do it I inherit KotlinFXMLLoader from FXMLLoader and reload two method (load() and load(inputStream: InputStream?)). Now load(inputStream: InputStream?) method has no implementation. But I did not now how to override static load() methods in kotlin code to hide his implementation.

Currently I make a pull request to your repo. Hope It would be helpful.