I try to run a simple JavaFX application on Android. Therefore I read the javafxports Getting Started Guide using gradle to build the app, but I got stuck somewhere. I'm able to build and install the app on an Android device using the installDebug
task of gradle within Eclipse, however, when I start the app I get a black screen. When I extract the .apk
file it does not contain the jar file of the JavaFX application. I assume the apk should contain the JavaFX application jar-file, but I have no idea how to include it into the .apk
.
I also tried to use gradle to build the JavaFX application itself (jar-file), which works fine. However, I don't know where to put this jar-file that it can be included into the apk-file. I read I've to put it into a dist
directory, but I assume this would only be used in Netbeans right? I'm using the Eclipse gradle integration to build the project.
Here is what I tried. Since the JavaFX application is as simple as the HelloWorld sample app and works like a charm, I expect a missconfiguration within my gradle build file.
build.gradle - to build the apk
apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'android'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:2.5.0'
classpath 'com.android.tools.build:gradle:1.0.1'
}
}
repositories {
jcenter()
}
ext {
dalvikSdkHome = getProjectProperty('dalvikSDK')
dalvikSdkLib = dalvikSdkHome + '/rt/lib'
}
android {
compileSdkVersion 21
buildToolsVersion "21.1.1"
dexOptions {
preDexLibraries = false
}
sourceSets {
main {
jniLibs.srcDir file("${dalvikSdkLib}/")
assets.srcDirs = ['assets']
}
}
lintOptions {
abortOnError false
}
}
dependencies {
compile files ("${dalvikSdkLib}/ext/jfxrt.jar",
"${dalvikSdkLib}/ext/jfxdvk.jar",
"${dalvikSdkLib}/ext/compat-1.0.0.jar")
}
project.tasks.withType(com.android.build.gradle.tasks.Dex) {
additionalParameters=['--core-library']
}
String getProjectProperty(String propertyName) {
project.hasProperty(propertyName) ? project.property(propertyName) : null
}
build.gradle - to build the jar
// Declares binary plugin and its required JavaFX classpath
apply from: "http://dl.bintray.com/content/shemnon/javafx-gradle/0.4.0/javafx.plugin"
// Configures plugin
javafx {
// Points to JDK and its JavaFX libraries, also declares target runtime JDK
javaRuntime = getProjectProperty('javaJDKPath')
// Application name and ID presented by target OS
appID 'HelloWorldApp'
appName 'Hello World Application'
// Main class of application
mainClass 'helloworld.HelloWorld'
// JVM arguments, system properties, application command line arguments
jvmArgs = ['-XX:+AggressiveOpts', '-XX:CompileThreshold=1']
systemProperties = ['prism.disableRegionCaching':'true']
arguments = ['-l', '--fast']
// Keystore credentials for signing JAR
// Generate key: keytool -genkey -alias release -keyalg RSA -keystore keystore.jks -keysize 2048
releaseKey {
alias = 'release'
keyStore = file(getProjectProperty('keystoreJKSFile')) // keyStore = file("${System.properties['user.home']}/keystore/keystore.jks")
keyPass = getProjectProperty('keyStorePassword')
storePass = getProjectProperty('storePassword')
}
signingMode 'release'
// ...
}
String getProjectProperty(String propertyName) {
project.hasProperty(propertyName) ? project.property(propertyName) : null
}
gradle.properties
javaJDKPath=D:/Java/jdk1.8.0_20
dalvikSDK=D:/Java/dalvik-sdk-8u20b3/dalvik-sdk
keystoreJKSFile=D:/Java/jre1.8.0_20/bin/keystore.jks
keyStorePassword=password
storePassword=password
local.properties
sdk.dir=D:/programme/Android/adt-bundle-windows-x86_64-20130917/sdk
And this is my project structure
HelloWorld
-- src\main
-- java\helloworld\HelloWorld.java
-- res\
-- AndroidManifest.xml
-- assets\
-- javafx.platform.properties
-- javafx.properties
-- build.gradle
-- gradle.properties
-- local.properties
Do I need to use a dist
directory? Where would I put the jar-file of my JavaFX application that it will be included into the apk-file?
Now answering my own question.
The configuration of the gradle setup, as posted in my question, is already running. In my question, I did not show you that I was using a .fxml file for the layout. So the reason why I did not get it running was because the .fxml layout has not been placed at the right spot to be packaged with the apk file (LogCat showed the error Location Not Found
and I had a black screen on my device).
First of all, here is a working sample for the HelloWorld.java (see structure and gradle setup etc. in my question):
package helloworld;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
If you would like to use .fxml
files, you need to change the structure of your project a little bit. All .fxml
files, .css
files, graphics etc. belong into the resources\assets
directory or subdirectory. This ensures that the .fxml files etc. will be packaged in the apk.
HelloWorld
-- src\main
-- java\helloworld\
-- HelloWorld.java
-- MyController.java
-- resources\assets\
-- sample_layout.fxml
-- AndroidManifest.xml
-- assets\
-- javafx.platform.properties
-- javafx.properties
-- build.gradle
-- gradle.properties
-- local.properties
I did not check, if the assets
folder which contains the javafx.platform.properties
and the javafx.properties
(both from the dalvikVM sdk) is still required. If I check the content of the .apk file, the apk contains both files twice. Seems like the dalvikVM library copies these files automatically.
Note: If you need to check the content of your apk, extract the apk file, then extract the classes.dex which is part of the apk (see this post for more details)
Here is an example using .fxml files:
HelloWorld.java
package helloworld;
import java.io.IOException;
import java.net.URL;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
try {
URL fxmlFile = getClass().getResource("/assets/sample_layout.fxml");
FXMLLoader loader = new FXMLLoader(fxmlFile);
AnchorPane page = (AnchorPane) loader.load();
MyController controller = (MyController) loader.getController();
Scene scene = new Scene(page);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX Sample");
primaryStage.show();
} catch(IOException e) {
e.printStackTrace();
}
}
}
MyController.java
package helloworld;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
public class MyController {
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private Label label_Counter;
@FXML
private Button button_IncrementCounter;
@FXML
private Button button_DecrementCounter;
private static final String OUTPUT_PREFIX = "Counter: ";
private static int counter = 0;
@FXML
void onIncrementButtonPressed(ActionEvent event) {
label_Counter.setText(OUTPUT_PREFIX + ++counter);
}
@FXML
void onDecrementButtonPressed(ActionEvent event) {
label_Counter.setText(OUTPUT_PREFIX + --counter);
}
@FXML
void initialize() {
assert label_Counter != null : "fx:id=\"label_Counter\" was not injected: check your FXML file 'sample_layout.fxml'.";
assert button_IncrementCounter != null : "fx:id=\"button_IncrementCounter\" was not injected: check your FXML file 'sample_layout.fxml'.";
assert button_DecrementCounter != null : "fx:id=\"button_DecrementCounter\" was not injected: check your FXML file 'sample_layout.fxml'.";
label_Counter.setText(OUTPUT_PREFIX + 0);
}
}
sample_layout.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloworld.MyController">
<children>
<VBox layoutX="332.0" layoutY="71.0" prefHeight="400.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label text="Please click on the buttons to increment or decrement the counter:" />
<Button fx:id="button_IncrementCounter" mnemonicParsing="false" onAction="#onIncrementButtonPressed" text="Increment Counter">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</Button>
<Button fx:id="button_DecrementCounter" mnemonicParsing="false" onAction="#onDecrementButtonPressed" text="Decrement Counter">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</Button>
<Label fx:id="label_Counter" text="<output-placeholder>">
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>
</children>
</AnchorPane>
Hope that helps everyone else getting started with JavaFX on Android.