How to run JavaFX on Android

Michael picture Michael · Jan 6, 2015 · Viewed 17k times · Source

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?

Answer

Michael picture Michael · Jan 21, 2015

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();
    }
}

Run JavaFX on Android using fxml layouts

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="&lt;output-placeholder&gt;">
               <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.