I have been struggling for several weeks (!) to get a previously working JavaFX application to build and run in a clean manner using:
- The current version of JavaFX (including use of .fxml resource files)
- Java 9 modules (a module for the project as a whole, plus references to modules for including referenced libraries such as JavaFX)
- A current version of Gradle (with a gradle script written using the Kotlin DSL syntax).
- Some Kotlin code in the project itself (my project is a Kotlin/Java hybrid gradually being translated to Kotlin over time).
- I am using IntelliJ Idea, although a command-line-only solution would be fine as a starting point.
I have encountered severe problems doing this, and at this point I have tried almost every idea I had as to how to accomplish it. Much googling has located for me a variety of fragments of Gradle scripts that accomplish one or two of these goals at a time, but which invariably are structured in such a way as to make the other goals difficult or impossible.
This should not be as difficult as it is, but lack of examples that use these items TOGETHER in a straightforward manner has made it an incredibly frustrating experience. For instance:
- Most Gradle scripts I have found don't provide a way to put .fxml resource files into a place where the Java runtime can later find them using its existing getClass().getResource("foobar.fxml") calls. The JavaFX instructions do not appear to address this question.
- Most Gradle scripts don't specify how to use JavaFX JAR files in conjuction with a Java 9 or later modular program, particularly with regards to --module-path, --add-modules, etc. on the command line that may need to be supplied when building or running a Java 9 JavaFX app.
- Most Gradle scripts don't specify how to invoke jlink for a project that uses Java 9 modules in conjunction with JavaFX JAR files in order to produce a final executable that doesn't depend on an installed JDK,
- Most Gradle scripts that use JavaFX jar files attempt to have Gradle automatically download the jar files itself (unlike the examples for openjfx at https://openjfx.io/openjfx-docs/#IDE-Intellij, which for some odd reason expects the user to manually download them). Thus, the "official" example scripts used on the JavaFX site are using a very non-typical way to access library files such as JavaFX JARs, which is not the way that Groovy or Maven typically do it.
- Most Gradle scripts don't use Kotlin DSL syntax (now the "standard" way to use Gradle for Android projects, and increasingly popular), which means that even trying to adapt them to work first forces the user to translate them from Groovy syntax into Kotlin syntax before anything else can be done with them. This translation is often not trivial! Note that the examples given on the JavaFX pages such as https://openjfx.io/openjfx-docs/#IDE-Intellij do not currently show Kotlin Gradle syntax.
- Some Gradle scripts use third-party JavaFX plugins such as https://github.com/FibreFoX/javafx-gradle-plugin, but these plugins are not clearly documented, and it is unclear how to accomplish some of the other goals listed above while using it.
and so forth. Each of the above observations has involved many hours of googling and experimenting on my part to discover what was going on.
Granted, some of these are Gradle problems, and I will be filing some bug reports against Gradle in the near future, but at the same time, I have found that the documentation for how to use JavaFX with Gradle seems to be very lacking with regards to how to use the two together. Example scripts that use up-to-date JavaFX practices use out-of-date or deprecated Gradle practices, and vice versa. Assumptions are made as to whether or not .fxml files will be used in the project, whether Kotlin will be used, and so forth. This lack of good, clean, simple, complete, and well-supported examples is a severe impediment to using both Gradle and JavaFX.
I have scoured the internet searching for a single example that works and incorporates all of these items together and have so far not found a single one. I am hoping that someone on the JavaFX team who is more knowledgeable than I with JavaFX can provide some examples on the "Getting Started with JavaFX 11" page that will accomplish most if not all of these goals in a single self-consistent script.
My current code, which SOMEWHAT works, and which can perhaps be used as a starting point for documentation or can be mined for ideas, is as follows:
import org.apache.tools.ant.types.resources.JavaResource
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.gradle.internal.os.OperatingSystem
plugins {
java
kotlin("jvm") version "1.2.51"
application
}
application {
group = "org.example.something"
version = "1.0-SNAPSHOT"
applicationName = "Something"
mainClassName = "org.example.something.Something"
}
// Note: OperatingSystem is a non-public Gradle API. I am using it here to avoid having to include an
// external plugin to determine the operating system, such as com.google.osdetector. Personally,
// I think that Gradle's OperatingSystem should be made public, and should include an instance method
// for getting the platform name in a format suitable for use with JavaFX Jars (like the following)
// so that using JavaFX with Gradle is simplified. There are already too many hurdles to jump through
// in order to get JavaFX to work with Gradle.
val os = OperatingSystem.current()!!
val platform = when { os.isWindows -> "win"; os.isLinux-> "linux"; os.isMacOsX -> "mac"; else -> error("Unknown OS") }
repositories {
jcenter()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("org.openjfx:javafx-fxml:11:$platform")
implementation("org.openjfx:javafx-web:11:$platform")
implementation("org.openjfx:javafx-media:11:$platform")
implementation("org.openjfx:javafx-swing:11:$platform")
implementation("org.openjfx:javafx-base:11:$platform")
implementation("org.openjfx:javafx-graphics:11:$platform")
implementation("org.openjfx:javafx-controls:11:$platform")
// other implementation dependencies go here...
}
configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_1_10
targetCompatibility = JavaVersion.VERSION_1_10
}
// Put *.fxml files from src/main/java into a 'resources' subtree instead of alongside *.class files.
// This is necessary because Gradle won't copy them from the subtree containing class files into
// a location where getClass().getResource(...) can find them at runtime.
sourceSets {
getByName("main") {
resources {
srcDir(files("src/main/java"))
// outputDir = file("java") // Is this needed or helpful?
}
}
}
// The name of the top-level module (target module) we are building.
val moduleName = "org.example.something"
// This task will compile all Java code in the target module except for test code.
// Note that we are supplying a module path (from the 'dependencies' section above) instead of
// a class path to the Java compiler.
tasks.named<JavaCompile>("compileJava") {
inputs.property("moduleName", moduleName)
doFirst {
val modulePath = classpath.asPath
classpath = files() // Clear classpath since we are using module path instead
options.compilerArgs.addAll(listOf(
"--module-path", modulePath // Supply accumulated class path as module path instead.
))
}
}
// This task will compile all Java test code in the target module.
// Note that we are supplying a module path (from the 'dependencies' section above) instead of
// a class path to the Java compiler.
tasks.named<JavaCompile>("compileTestJava") {
// See https://stackoverflow.com/questions/46991022/junit-5-java-9-and-gradle-how-to-pass-add-modules
inputs.property("moduleName", moduleName)
doFirst {
val modulePath = classpath.asPath
classpath = files() // Clear classpath since we are using module path instead
options.compilerArgs.addAll(listOf(
"--module-path", modulePath // Supply accumulated class path as module path instead.
))
}
}
// This task will compile all Kotlin code found in the Java source tree.
tasks.withType<KotlinCompile> {
// See https://stackoverflow.com/questions/47657755/building-a-kotlin-java-9-project-with-gradle/47669720#47669720
destinationDir = tasks.withType<JavaCompile>().first().destinationDir // Use same dir for Kotlin classes as Java classes.
kotlinOptions.jvmTarget = "1.8"
}
// This task will run the JavaFX version of the app.
// In addition to supply ing the module path, we need to patch the top-level module to
// include the resources directory (where .fxml files will be located) so they can be found
// at runtime.
tasks.named<JavaExec>("run") {
doFirst {
val modulePath = classpath.asPath
jvmArgs = listOf(
"--module-path", modulePath,
"--add-modules", "ALL-MODULE-PATH",
"--patch-module", "$moduleName=build/resources/main",
"--module", "$moduleName/${application.mainClassName}"
)
}
}
In summary, please modify your "Getting Started With JavaFX 11" examples page to provide a working example of a Gradle script that accomplishes these goals. Basically, I would like to see a "Hello, World" that is implemented using idiomatic Gradle, Kotlin, Java 9 modules, and JavaFX with .fxml files.
Thanks in advance.