1. Introduction

Gluon provides an easy and modern approach for developing Java Client applications. These applications can run on the JVM or can be converted to a platform specific native-images which have lighting fast startup and takes a fraction of space. Moreover, applications can also be targeted to Android, iOS, and embedded apart from all the desktop environments.

Gluon provides all the tools necessary to build these applications including, but not limited to, build tools, IDE plugins, UI library, cloud connectivity, data-binding etc.

2. Getting Started

Any Java application can be converted to a native application for a specific platform using Gluon technology. This platform specific application has much faster startup time, since the JVM no longer needs to be started. The resulting application will be entirely integrated into the native operating system.

An easy way to get started is to use the just add Gluon Client plugin to your Java application. Gluon Client plugin leverages GraalVM and OpenJDK by compiling the Java Client application and all its required dependencies into native code, so that it can be directly executed as a native application on the target platform.

At this moment, Gluon supports the following target platforms:

  • Linux

  • Mac OS X

  • Windows

  • iOS

  • Android

2.1. Requirements

Gluon applications can be developed, run, and tested on any desktop and embedded platform using the latest JDK.

Platform specific requirements for creation and deployment of native-images is discussed in depth in the platforms section.

2.2. IDE Plugins

At Gluon, we introduced IDE plugins to help developers get started with Gluon as quickly as possible. The plugin aids in creating a Gluon application project inside your IDE.

At present, we support NetBeans, IntelliJ IDEA and Eclipse.

You can read more about the IDE plugins in the IDE Plugins section of the documentation.

2.3. Gluon Start

Gluon Start is a website which enables you to generate the structure and skeleton of your Java client application. You can configure the following properties of the application:

  • Name, groupId and artifactId to use for the generated Maven project

  • JavaFX version and modules required by the project

  • Gluon Mobile stack to include in the project. For example, Glisten (for the UI toolkit) and Gluon Maps can be selected to be used in the same project

  • Gluon Attach services required in the project

  • If the application needs a flexible and secure way to connect to an existing or new backend or cloud service, Gluon CloudLink can be selected as well

All options are easily selectable and Gluon Start will generate the project with correct dependencies for you!

2.4. Maven Archetype

A simple Gluon application can be created by using the Client Maven Archetype for simple Gluon Mobile application.

For example:

mvn archetype:generate \
        -DarchetypeGroupId=com.gluonhq \
        -DarchetypeArtifactId=client-archetype-mobile \
        -DarchetypeVersion=0.0.2 \
        -DgroupId=com.gluonhq  \
        -DartifactId=gluon-mobile-sample \
        -Dversion=1.0.0-SNAPSHOT \
        -Djavafx-version=15

3. IDE Plugins

Gluon releases IDE plugins for all major Java IDEs. These plugins can be used to create a basic project with all the required dependencies to natively build the project and ultimately run the native image. These plugins can be used to create either a Maven or Gradle project. We will briefly discuss on how to install these plugins, create a new project and build a native image using Gluon Client in the following section.

3.1. The Gluon Plugin for IntelliJ IDEA

In this section, we’ll explain briefly how to install the plugin on IntelliJ IDEA and how to use it to create a sample application that can be deployed on desktop, Android and iOS devices. Before we start, make sure to check the Platforms section for a list of prerequisites for each platform.

3.1.1. Plugin Installation

You can get it from here, or you can directly install it from IntelliJ IDEA: click File→Settings…​ (or IntelliJ IDEA→Preferences…​) and select Plugins on the left. You will see the installed plugins on your system.

Now click Marketplace, type Gluon, select the result and click Install.

Find Gluon Plugin

The Gluon Plugin will be downloaded and installed. Then press Restart IDE or simply press OK to close the plugins dialog, and restart the IDE when asked.

3.1.2. Create a new Gluon project

Once the plugin is installed, we can use it to create a sample application.

In IntelliJ IDEA, click File → New → Project…​ and select Gluon on the left, and one of the available projects on the right. For instance, Gluon Mobile - Single View Project. Press Next.

New Gluon Project

The first time you use the plugin, you will be asked to enter your email address.

Gluon Settings

If you already have a Gluon Mobile key for your projects, you can insert it here as well, so it will be added by default to your new projects. If you don’t, you will be using the free (trial) version. Please find more about Gluon Mobile licenses.

You can access these settings later on from File→Settings→Gluon (or IntelliJ IDEA→Preferences…​→Gluon):

Gluon Settings

Once you have completed this first-time-only form, click Next.

Type the package name and the main class name. Select the platforms to deploy the application, and the build tool of your choice. For this tutorial, we will use the Maven build tool. Press Next.

Package name

From the list, select a valid Java JDK (11+) and press Next.

Java JDK

Now add a name and a location for the project and press Finish.

Project name

The project will be imported and opened.

3.1.3. The Gluon Mobile project

The plugin already adds some default code in the main folder, so we’ll be able to run it without adding a single line of code.

The Maven project has a dependency on the OpenJFX Maven Plugin and the Gluon Client Maven plugin to the project.

These plugins add a series of goals and to access them we just need to open Maven tool window in IntelliJ IDEA. The Maven tool window shows all the available Maven plugins along with their respective goals.

IDEA Project

3.1.4. Run the application

Make sure JAVA_HOME is properly set: from the Maven tool window, select Maven Settings→Maven→Runner, click on browse Environment variables, and if JAVA_HOME is not included, add it.

Java Home

Before creating a native image, it’s easier to run the application first and verify that there are no errors.

Select Plugins → javafx → javafx:run from the Maven tool window. Verify that all the tasks are executed without errors, and the project runs fine on your desktop. Alternatively you can open the terminal (View→Tool Windows→Terminal) and run mvn javafx:run.

Now let’s make a slight change to the code. In the BasicView class, update the text of the label on line 20 to the following:

button.setOnAction(e -> label.setText("Hello from IntelliJ IDEA!"));

Run it again to see that the new message shows up on your desktop when you click the button.

Run Project

3.1.5. Create & run a native image of the application

In order to run the native image steps it is better to use a system terminal or the embedded terminal from your IDE. However, it is still possible to use the IDE’s Maven window for this.

Make sure you set the environment variable GRAALVM_HOME. Alternatively, you can add path to GraalVM installation directory by adding graalvmHome to the client-plugin configuration.

If you are running on Windows, you need to run all the Client goals from an x64 terminal.

However, you can use the terminal from IntelliJ for this. Go to File→Settings…​→Tools→Terminal, and change Shell path from cmd.exe to cmd.exe /k "<path to VS2019>\VC\Auxiliary\Build\vcvars64.bat.

If you are running from IDE, make sure that the mvn executable under path/to/IntelliJ/plugins/maven/lib/maven3/bin/ has exec permissions, or select a custom Maven installation by setting the Maven home directory from the Maven tool window, Maven Settings→Maven.

The Gluon Client plugin provides us various goals which are explained in detail earlier in the documentation. For this tutorial, we will be using the client:build goal to create a native image of the application.

Execute mvn client:build from the terminal, or, select Plugins → client → client:build goal from the Maven tool window.

This goal typically takes a couple of minutes to complete and may vary depending on the system. Once the goal is executed successfully, the native image can be run by executing client:run goal.

3.1.6. Create & install an Android native image

This part is only applicable for Linux.

Now we are ready to deploy the same application on an Android device.

Go to Maven tool window, expand Profiles and check android. Make sure that all other profiles are unchecked. This will activate the pre-existing android profile.

Run mvn -Pandroid client:build from the terminal, or alternatively, execute the client:build goal from the Maven plugins section.

Once the native image is created, we need to package it into an apk before we can install it on a physical Android device.

Execute mvn -Pandroid client:package to create an apk. Once the goal is executed successfully, connect a physical device and install the native image by executing the mvn -Pandroid client:install goal.

When the installation ends successfully, run mvn -Pandroid client:run or find the application on your device and open it up:

Android app

3.1.7. Create & install an iOS native image

This part is only applicable for MacOS.

Now we are ready for deploying the same application on an iOS device.

Go to Maven tool window, expand Profiles and check ios. Make sure that all other profiles are unchecked. This will activate the pre-existing ios profile. All client goals will now target iOS platform.

Run mvn -Pios client:build from the terminal, or alternatively, execute the client:build goal from the Maven plugins section.

Once the goal is executed successfully, connect a physical device and run the native image on it by executing by executing mvn -Pios client:run.

iOS app

3.2. The Gluon Plugin for NetBeans

In this section, we’ll explain briefly how to install the Gluon plugin on Apache NetBeans and how to use it to create a sample application that can be deployed on desktop, Android and iOS devices. Before we start, make sure to check the Platforms section for a list of prerequisites for each platform.

3.2.1. Plugin Installation

You can get it from the Apache NetBeans plugin portal, or you can directly install it from NetBeans: click Tools → Plugins. Now select Available Plugins…​ and find Gluon Plugin.

Plugins Window

Select the plugin, click Install and follow the steps:

Install plugin

Accept the license and click Install. Click Continue when prompted:

Plugin warning

Wait until the Gluon Plugin is installed and click Finish.

Plugin installed

You will find the plugin under the Installed tab in the Plugins window.

3.2.2. Create a new Gluon project

Once the plugin is installed, we can use it to create a sample application.

In NetBeans, click File → New → Project…​ and select Gluon on the left, and one of the available projects on the right. For instance, Gluon Mobile - Single View Project. Press Next.

New Gluon Project

The first time you use the plugin, you will be asked to enter your email address.

Gluon Settings

If you already have a Gluon Mobile license key for your projects, you can insert them here as well, so they will be added by default to your new projects. If you don’t, you will be using the free (trial) version. Please find more about Gluon Mobile licenses.

You can access these settings later on from Tools→Options→Miscellaneous→Gluon (or NetBeans→Preferences…​→Miscellaneous→Gluon):

Gluon Settings

Once you have completed this first-time-only form, click Next.

Type the name of the project, find a proper location, add the package name and change the main class name if required. Select the platforms to deploy the application, and the build tool of your choice. For this tutorial, we will use the Maven build tool.

Project Settings

Press Finish and the project will be created and opened.

You will notice that a Maven project has been created with the pom containing profiles for the platforms you selected. These profiles make it easier to create native images targeted to each of these platforms.

3.2.3. The Gluon Mobile project

The plugin already adds some default code in the main folders, so we’ll be able to run it without adding a single line of code.

The Maven project has a dependency on the OpenJFX Maven Plugin and the Gluon Client Maven plugin to the project.

These plugins add a series of goals and to access them we just need to switch to the Navigator panel in NetBeans. The Navigator panel shows all the available Maven goals.

NetBeans Project

3.2.4. Run the application

Make sure the Maven default JDK is properly set to 11+. Tools→Options→Java→Maven (or NetBeans→Preferences…​→Java→Maven), set Default JDK to JDK 11+ (or click Manage Java Platfoms to add a new Platform if it doesn’t exist).

Default JDK

Before creating a native image, it’s easier to run the application first and verify that there are no errors.

Select Run project (F6) or select javafx:run from the Navigator. Verify that all the tasks are executed without errors, and the project runs fine on your desktop. Alternatively you can open the terminal (Tools→Open in Terminal) and run mvn javafx:run.

Let’s make a slight change to the code. In the BasicView class, update the text of the label on line 21 to the following:

button.setOnAction(e -> label.setText("Hello from NetBeans!"));

Run it again to see that the new message shows up on your desktop when you click the button.

Run Project

3.2.5. Create & run a native image of the application

In order to run the native image steps it is better to use a system terminal or the embedded terminal from your IDE. However, it is still possible to use the IDE’s Navigator window for this.

Make sure you set the environment variable GRAALVM_HOME. Alternatively, you can add path to GraalVM installation directory by adding graalvmHome to the client-plugin configuration.

If you are running on Windows, you need to run all the Client goals from a x64 terminal.

However, you can use the terminal from NetBeans for this (it requires Cygwin installed). On the terminal, run once cmd.exe /k "<path to VS2019>\VC\Auxiliary\Build\vcvars64.bat.

The Gluon Client plugin provides us various goals which are explained in details, earlier in the documentation. For this tutorial, we will be using the client:build goal to create a native image of the application.

Execute mvn client:build from the terminal, or, run the client:build goal from the Navigator with modifiers, and add Env.GRAALVM_HOME=/path/to/GraalVM.

This goal typically takes a couple of minutes to complete and may vary depending on the system. Once the goal is executed successfully, the native image can be run by executing client:run goal.

3.2.6. Create & install an Android native image

This part is only applicable for Linux.

Now we are ready for deploying the same application on an Android device.

Right-click on the project and select Set Configuration and select android. This will activate the pre-existing android profile.

Run mvn -Pandroid client:build from the terminal, or, execute the client:build goal from the Navigator with modifiers, adding profile android, and Env.GRAALVM_HOME=/path/to/GraalVM.

Once the native image is created, we need to package it into an apk before we can install it on a physical Android device.

Execute mvn -Pandroid client:package to create an apk. Once the goal is executed successfully, connect a physical device and install the native image by executing the mvn -Pandroid client:install goal.

When the installation ends successfully, run mvn -Pandroid client:run or find the application on your device and open it up:

Android app

3.2.7. Create & install an iOS native image

This part is only applicable for MacOS.

Please check the prerequisites for iOS.

Now we are ready for deploying the same application on an iOS device.

Right-click on the project and select Set Configuration and select ios. This will activate the pre-existing ios profile.

Run mvn -Pios client:build from the terminal, or, execute the client:build goal from the Navigator with modifiers, adding profile ios, and Env.GRAALVM_HOME=/path/to/GraalVM.

Once the goal is executed successfully, connect a physical device and run the native image on it by executing by executing mvn -Pios client:run.

iOS app

3.3. The Gluon Plugin for Eclipse

In this section, we’ll explain briefly how to install the Gluon plugin on Eclipse IDE and how to use it to create a sample application that can be deployed on desktop, Android and iOS devices. Before we start, make sure to check the Platforms section for a list of prerequisites for each platform.

3.3.1. Plugin Installation

Eclipse Marketplace

The Gluon Plugin can be installed through the Eclipse Marketplace. You can install it from this link or directly open the Eclipse Marketplace from Eclipse→Help→Eclipse MarketPlace…​. Then type Gluon in the search field and press 'Go' to find the plugin.

Eclipse Marketplace

Press Install, accept the terms of the license agreement when prompted and press Finish to begin the installation of the plugin. Press Confirm to install the installation of the selected features, and Install anyway when prompted with an unsigned sofware warning.

The Gluon Plugin will be downloaded and installed.

When the installation is completed, Eclipse will ask you to restart your IDE for the changes to take effect.

You can now continue to Creating a new Gluon Project.

Update Site

Alternatively, you can install the Gluon Plugin by providing an update site.

Open Eclipse and choose Help → Install New Software…. Paste the following Update Site URL in the Work with: text field: http://download.gluonhq.com/tools/eclipse/release, and select Gluon Plug-in for Eclipse. Then proceed as indicated above.

3.3.2. Create a new Gluon project

Now that the plugin is installed, we can use it to create a sample application.

In Eclipse, click File → New → Project… and select one of the available projects under the Gluon category, for instance, Gluon Mobile - Single View Project and click Next.

New Gluon Project

The first time you use the plugin, you will be asked to enter your email address.

Gluon Settings

If you already have a Gluon Mobile license key for your projects, you can insert them here as well, so they will be added by default to your new projects. If you don’t, you will be using the free (trial) version. Please find more about Gluon Mobile licenses.

You can access these settings later on from Window→Preferences→Gluon (or Eclipse→Preferences…​→Gluon):

Gluon Settings

Once you have completed this first-time-only form, click Next.

Provide a name for the project, optionally choose a custom location to store the project and click Next.

Provide project name and location

The plugin will generate a JavaFX application class. In the following step you can configure the name of the package and the name of the class that will be generated. Select the platforms to deploy the application, and the build tool of your choice. For this tutorial, we will use the Maven build tool.

Provide package and class name and select platforms

Click Finish to complete the creation of the Gluon project.

You will notice that a Maven project has been created with the pom containing profiles for the platforms you selected. These profiles make it easier to create native images targeted to each of these platforms.

3.3.3. The Gluon Mobile project

The plugin already adds some default code in the main folders, so we’ll be able to run it without adding a single line of code.

Gluon project

The Maven project has a dependency on the OpenJFX Maven Plugin and the Gluon Client Maven plugin to the project. These plugins add a series of goals which will be used to run and create native images of the application.

3.3.4. Run the application

Before creating a native image, it’s easier to run the application first and verify that there are no errors.

Right-click on the project, select Run As → Maven Build…​. An "Edit Configuration" window opens. In the Goals textfield, type javafx:run and click Run. Verify that all the tasks are executed without errors, and the project runs fine on your desktop.

Show the Maven Build view

Let’s make a slight change to the code. In the BasicView class, update the text of the label on line 20 to the following:

button.setOnAction(e -> label.setText("Hello from Eclipse!"));

Run the application again to verify that the text inside the label has changed.

Run project

3.3.5. Create & run a native image of the application

In order to run the native image steps it is better to use a system terminal or the embedded terminal from your IDE. However, it is still possible to use the IDE’s Run Configurations dialog for this.

Make sure you set the environment variable GRAALVM_HOME. Alternatively, you can add path to GraalVM installation directory by adding graalvmHome to the client-plugin configuration.

If you are running on Windows, you need to run all the Client goals from a x64 terminal.

However, you can use the terminal from Eclipse for this (it can be installed from the Eclipse Marketplace). On the terminal, run once cmd.exe /k "<path to VS2019>\VC\Auxiliary\Build\vcvars64.bat.

The Gluon Client plugin has various goals which are explained in details earlier in the documentation. For this tutorial, we will be using the client:build goal to create a native image of the application.

Execute mvn client:build from the terminal, or, open the Run Configurations…​ window and update the Goal to client:build and click Run.

This goal typically takes a couple of minutes to complete and may vary depending on the system. Once the goal is executed successfully, the native image can be run by executing client:run goal.

3.3.6. Create & install an Android native image

This part is only applicable for Linux.

Now we are ready for deploying the same application on an Android device.

Right-click on the project and open Maven → Select Maven Profiles…​ window. Check android checkbox and uncheck all others. This will activate the pre-existing android profile. All the client goals will now target android platform instead of desktop.

Run mvn -Pandroid client:build from the terminal, or alternatively, execute the client:build goal again, this time the android profile should be selected by default, else add it to the profiles field.

Once the native image is created, we need to package it into an apk before we can install it on a physical Android device.

Execute mvn -Pandroid client:package goal to create an apk. Once the goal is executed successfully, connect a physical device and install the native image by executing client:install goal.

Once the goal is executed successfully, connect a physical device and install the native image by executing the mvn -Pandroid client:install goal.

When the installation ends successfully, run mvn -Pandroid client:run or find the application on your device and open it up:

Android app

3.3.7. Create & install an iOS native image

This part is only applicable for MacOS.

Please check the prerequisites for iOS.

Now we are ready for deploying the same application on an iOS device.

Right-click on the project and open Maven → Select Maven Profiles…​ window. Check ios checkbox and uncheck all others. This will activate the pre-existing ios profile. All client goals will now target iOS platform.

Run mvn -Pios client:build from the terminal, or alternatively, execute the client:build goal again, this time the ios profile should be selected by default, else add it to the profiles field.

Once the goal is executed successfully, connect a physical device and run the native image on it by executing mvn -Pios client:run.

iOS app

4. The Gluon Client plugin for Maven

Any Java(FX) project can be easily integrated with Gluon. This section details the specifics of the Gluon Client Maven plugin and shows how to add it to your project, to create native applications.

Open Source

Client Maven plugin is open sourced, and licensed under the BSD-3 license. Its source code is hosted under Gluon organization in Github.

4.1. Apply the plugin

Edit your pom file and add the plugin:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>0.1.35</version>
    <configuration>
        <mainClass>your.mainClass</mainClass>
    </configuration>
</plugin>

The plugin allows some options that can be set in <configuration> to override the default settings.

4.2. Goals

The client plugin introduces the following goals:

  • client:compile

  • client:link

  • client:build

  • client:package

  • client:install

  • client:run

4.2.1. client:compile

This goal performs AOT compilation by executing the native-image command and builds the shared object file. It is a very intensive and lengthy task (several minutes, depending on your project and CPU), so it should be called only when the project is ready and runs fine on a VM.

The results will be available at target/client/$arch-$os/gvm.

Links the object file to create a native executable file or shared library.

The results will be available at target/client/$arch-$os/$AppName.

4.2.3. client:build

This goal simply combines client:compile and client:link.

4.2.4. client:package

Packages the executable or shared library into a target specific package that includes all the necessary dependencies.

The list of platform specific packages are as follows:

  • Linux - deb, rpm

  • macOS - app, pkg, deb

  • Windows - exe, msi

  • iOS - app, ipa

  • Android - apk

At the moment, this goal is only supported for iOS and Android.

4.2.5. client:install

Installs the package on the host system or attached device.

At the moment, this goal is only intended for Android.

4.2.6. client:run

Runs either the executable generated by client:link on the host system or runs the application that was installed on the device (iOS or Android).

4.3. Run the samples

There are many samples available in the Gluon-samples project on GitHub that make use of the Gluon Client plugin for Maven.

These docs only mention the getting started HelloWorld samples.

For the rest of the samples, see Gluon documentation website.

Before proceeding, make sure to set the JAVA_HOME environment variable to the same location as GRAALVM_HOME.

4.3.1. HelloWorld

Let’s run HelloWorld with the Gluon Client plugin for Maven.

HelloWorld.java
package hello;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World!!");
        System.exit(0);
    }
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gluonhq.samples</groupId>
    <artifactId>helloworld</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>helloworld</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>s
        <maven.compiler.release>11</maven.compiler.release>
        <client.maven.plugin.version>3.8.1</client.maven.plugin.version>
        <main.class>hello.HelloWorld</main.class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>

            <plugin>
                <groupId>com.gluonhq</groupId>
                <artifactId>client-maven-plugin</artifactId>
                <version>0.1.35</version>
                <configuration>
                    <target>${client.target}</target>
                    <mainClass>${main.class}</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>desktop</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <client.target>host</client.target>
            </properties>
        </profile>
        <profile>
            <id>ios</id>
            <properties>
                <client.target>ios</client.target>
            </properties>
        </profile>
        <profile>
            <id>android</id>
            <properties>
                <client.target>android</client.target>
            </properties>
        </profile>
    </profiles>
</project>
Target: Linux

On a Linux machine, run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ helloworld ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for x86_64-linux-linux. This may take some time.
[INFO] [SUB] [hello.helloworld:4902]    classlist:   2,318.99 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:4902]        (cap):   1,459.76 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:4902]        setup:   3,223.67 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:4902]     (clinit):     368.34 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]   (typeflow):  11,305.59 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]    (objects):  12,607.81 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]   (features):   1,637.68 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]     analysis:  26,435.19 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]     universe:     604.95 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]      (parse):   1,945.81 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:4902]     (inline):   3,643.57 ms,  4.15 GB
[INFO] [SUB] [hello.helloworld:4902]    (compile):  14,496.02 ms,  4.90 GB
[INFO] [SUB] [hello.helloworld:4902]      compile:  21,376.96 ms,  4.90 GB
[INFO] [SUB] [hello.helloworld:4902]        image:   2,728.50 ms,  4.90 GB
[INFO] [SUB] [hello.helloworld:4902]        write:     669.48 ms,  4.90 GB
[INFO] [SUB] [hello.helloworld:4902]      [total]:  57,617.30 ms,  4.90 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hello.helloworld.o is created and can be found under target/client/x86_64-linux/gvm/tmp/SVM-*/hello.helloworld.o.

Run the client:link goal to produce the native image.

As a result, target/client/x86_64-linux/helloWorld is created. It can be executed directly from a terminal or with mvn client:run.

HelloWorld Linux Link Output
Target: macOS

On Mac OS X, Run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ helloworld ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for x86_64-apple-darwin. This may take some time.
[INFO] [SUB] [hello.helloworld:1829]    classlist:   1,741.21 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:1829]        (cap):   1,604.12 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:1829]        setup:   3,875.26 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:1829]     (clinit):     512.37 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]   (typeflow):  13,551.23 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]    (objects):  14,683.72 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]   (features):   1,897.64 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]     analysis:  31,396.24 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]     universe:     927.88 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]      (parse):   3,290.32 ms,  3.21 GB
[INFO] [SUB] [hello.helloworld:1829]     (inline):   4,121.45 ms,  4.11 GB
[INFO] [SUB] [hello.helloworld:1829]    (compile):  24,163.64 ms,  5.02 GB
[INFO] [SUB] [hello.helloworld:1829]      compile:  33,199.90 ms,  5.02 GB
[INFO] [SUB] [hello.helloworld:1829]        image:   4,121.24 ms,  5.02 GB
[INFO] [SUB] [hello.helloworld:1829]        write:     697.29 ms,  5.02 GB
[INFO] [SUB] [hello.helloworld:1829]      [total]:  76,352.27 ms,  5.02 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hello.helloworld.o is created and can be found under target/client/x86_64-darwin/gvm/tmp/SVM-*/hello.helloworld.o.

Run the client:link goal to produce the native image. As a result, target/client/x86_64-darwin/helloWorld is created. It can be executed directly from a terminal or with mvn client:run.

HelloWorld macOS Link Output
Target: Windows

On a Windows machine, run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ helloworld ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for x86_64-microsoft-windows. This may take some time.
[INFO] [SUB] Warning: Ignoring server-mode native-image argument --no-server.
[INFO] [SUB] [hello.helloworld:7112]    classlist:   4,153.91 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:7112]        (cap):   5,358.45 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:7112]        setup:   8,726.61 ms,  0.96 GB
[INFO] [SUB] WARNING GR-10238: VarHandle for static field is currently not fully supported. Static field private static volatile java.lang.System$Logger jdk.internal.event.EventHelper.securityLogger is not properly marked for Unsafe access!
[INFO] [SUB] [hello.helloworld:7112]     (clinit):     418.10 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]   (typeflow):  24,301.38 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]    (objects):  22,462.70 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]   (features):   1,743.55 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]     analysis:  49,567.10 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]     universe:     790.38 ms,  1.92 GB
[INFO] [SUB] [hello.helloworld:7112]      (parse):   6,963.45 ms,  1.82 GB
[INFO] [SUB] [hello.helloworld:7112]     (inline):   6,342.01 ms,  2.09 GB
[INFO] [SUB] [hello.helloworld:7112]    (compile):  32,776.16 ms,  2.09 GB
[INFO] [SUB] [hello.helloworld:7112]      compile:  47,301.62 ms,  2.09 GB
[INFO] [SUB] [hello.helloworld:7112]        image:   2,534.79 ms,  2.09 GB
[INFO] [SUB] [hello.helloworld:7112]        write:     227.90 ms,  2.09 GB
[INFO] [SUB] [hello.helloworld:7112]      [total]: 113,730.60 ms,  2.09 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hello.helloworld.obj is created and can be found under target\client\x86_64-windows\gvm\tmp\SVM-*\hello.helloworld.obj.

Run the client:link goal to produce the native image. As a result, target\client\x86_64-windows\helloWorld.exe is created. It can be executed directly or with mvn client:run.

HelloWorld Windows Link Output
Target: iOS

This requires an iOS device that has to be plugged in at the run phase. A valid provisioning profile is required as well, either by enrolling to the Apple Developer program or by using free provisioning. See iOS Deployment.

Use the ios profile to run mvn -Pios client:compile on Mac OS X:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ helloworld ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for arm64-apple-ios. This may take some time.
[INFO] [SUB] [hello.helloworld:2619]    classlist:   1,796.27 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:2619]        (cap):     394.97 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:2619]        setup:   2,588.74 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:2619]     (clinit):     501.74 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]   (typeflow):  12,836.03 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]    (objects):  14,296.39 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]   (features):   1,699.59 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]     analysis:  30,090.48 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]     universe:     956.49 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]      (parse):   3,090.59 ms,  3.18 GB
[INFO] [SUB] [hello.helloworld:2619]     (inline):   3,993.41 ms,  4.11 GB
[INFO] [SUB] [hello.helloworld:2619]    (compile):  28,280.20 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]    (bitcode):   1,210.45 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]    (prelink):   4,441.72 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]       (llvm):  52,067.97 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]   (postlink):   3,286.67 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]      compile:  96,697.84 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]        image:   4,741.68 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]        write:   1,412.67 ms,  4.95 GB
[INFO] [SUB] [hello.helloworld:2619]      [total]: 138,660.24 ms,  4.95 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hello.helloworld.o is created and can be found under target/client/arm64-ios/gvm/tmp/SVM-*/hello.helloworld.o.

Note that the process takes some time. There will be performance improvements, but either way, it is convenient to test first on desktop (and with HotSpot) as much as possible (i.e. with mvn javafx:run), so client:compile doesn’t have to be repeated due to avoidable errors.

Run the mvn -Pios client:link to produce the native image. As a result, target/client/arm64-ios/helloWorld.app is created.

HelloWorld iOS Link Output

Now it can be deployed to a plugged iOS device with mvn -Pios client:run.

HelloWorld iOS deployed
[INFO] --- client-maven-plugin:0.1.35:run (default-cli) @ helloworld ---
[INFO] ==================== RUN TASK ====================
[INFO] [SUB] [....] Waiting for iOS device to be connected
[INFO] [SUB] [....] Using abcdef01234567890abcdef01234567890abcdef (D221AP, iPhone X, iphoneos, arm64) a.k.a. 'iPhone'.
[INFO] [SUB] ------ Install phase ------
[INFO] [SUB] [  0%] Found abcdef01234567890abcdef01234567890abcdef (D221AP, iPhone X, iphoneos, arm64) a.k.a. 'iPhone' connected through USB, beginning install
[INFO] [SUB] [  5%] Copying ~/development/client-samples/Maven/HelloWorld/target/client/arm64-ios/helloWorld.app/META-INF/ to device
...
[INFO] [SUB] [ 90%] SandboxingApplication
[INFO] [SUB] [ 95%] GeneratingApplicationMap
[INFO] [SUB] [100%] Installed package ~/development/client-samples/Maven/HelloWorld/target/client/arm64-ios/helloWorld.app
...
[INFO] [SUB] ------ Debug phase ------
[INFO] [SUB] Starting debug of abcdef01234567890abcdef01234567890abcdef (D221AP, iPhone X, iphoneos, arm64) a.k.a. 'iPhone' connected through USB...
[INFO] [SUB] [  0%] Looking up developer disk image
[INFO] [SUB] [ 95%] Developer disk image mounted successfully
[INFO] [SUB] [100%] Connecting to remote debug server
[INFO] [SUB] -------------------------
...
[INFO] [SUB] determineCpuFeaures
[INFO] [SUB] Hello World!!
[INFO] [SUB] Process 23642 exited with status = 0 (0x00000000)
[INFO] result = true
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Even though there is no UI for this application, we can still see the message being printed to the console.

Target: Android

This requires an Android device that has to be plugged in at the run phase.

On a Linux machine, use the android profile and run mvn -Pandroid client:compile:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ helloworld ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for aarch64-linux-android. This may take some time.
[INFO] [SUB] [hello.helloworld:8258]    classlist:   1,474.74 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:8258]        (cap):     306.46 ms,  0.96 GB
[INFO] [SUB] [hello.helloworld:8258]        setup:   2,078.26 ms,  0.96 GB
[INFO] [SUB] WARNING GR-10238: VarHandle for static field is currently not fully supported. Static field private static volatile java.lang.System$Logger jdk.internal.event.EventHelper.securityLogger is not properly marked for Unsafe access!
[INFO] [SUB] [hello.helloworld:8258]     (clinit):     493.75 ms,  2.29 GB
[INFO] [SUB] [hello.helloworld:8258]   (typeflow):  11,436.20 ms,  2.29 GB
[INFO] [SUB] [hello.helloworld:8258]    (objects):  12,928.51 ms,  2.29 GB
[INFO] [SUB] [hello.helloworld:8258]   (features):   1,578.28 ms,  2.29 GB
[INFO] [SUB] [hello.helloworld:8258]     analysis:  27,065.74 ms,  2.29 GB
[INFO] [SUB] [hello.helloworld:8258]     universe:     909.78 ms,  2.30 GB
[INFO] [SUB] [hello.helloworld:8258]      (parse):   2,156.71 ms,  2.30 GB
[INFO] [SUB] [hello.helloworld:8258]     (inline):   3,780.53 ms,  3.68 GB
[INFO] [SUB] [hello.helloworld:8258]    (compile):  19,433.23 ms,  4.72 GB
[INFO] [SUB] [hello.helloworld:8258]      compile:  26,877.82 ms,  4.72 GB
[INFO] [SUB] [hello.helloworld:8258]        image:   4,022.56 ms,  4.72 GB
[INFO] [SUB] [hello.helloworld:8258]        write:     409.92 ms,  4.72 GB
[INFO] [SUB] [hello.helloworld:8258]      [total]:  63,290.80 ms,  4.72 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hello.helloworld.o is created and can be found under target/client/aarch64-android/gvm/tmp/SVM-*/hello.helloworld.o.

Note that the process takes some time. There will be performance improvements, but either way, it is convenient to test first on desktop (and with HotSpot) as much as possible (i.e. with mvn javafx:run), so client:compile doesn’t have to be repeated due to avoidable errors.

Run mvn -Pandroid client:link to produce the native image. As a result, target/client/aarch64-android/libhelloWorld.so is created.

Finally, run mvn -Pandroid client:package to bundle the application into an Android APK that can be installed on a device.

It produces the following output:

[INFO] --- client-maven-plugin:{client-maven-plugin-version}:package (default-cli) @ helloworld ---
[INFO] ==================== PACKAGE TASK ====================
[INFO] Default Android manifest generated in ~/Gluon/gluon-samples/HelloWorld/target/client/aarch64-android/gensrc/android/AndroidManifest.xml.
Consider copying it to ~/Gluon/gluon-samples/HelloWorld/src/android/AndroidManifest.xml before performing any modification
[INFO] Default Android resources generated in ~/Gluon/gluon-samples/HelloWorld/target/client/aarch64-android/gensrc/android/res.
Consider copying them to ~/Gluon/gluon-samples/HelloWorld/src/android/res before performing any modification
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

This creates the apk package which is available at target/client/aarch64-android/gvm/helloworld.apk.

HelloWorld Android apk

Now we are ready to install and run the application on a plugged-in Android device. Run mvn -Pandroid client:install client:run to install and launch the application on the device.

HelloWorld Android Deployed
[INFO] --- client-maven-plugin:0.1.35:install (default-cli) @ helloworld ---
[INFO] ==================== INSTALL TASK ====================
[INFO]
[INFO] --- client-maven-plugin:0.1.35:run (default-cli) @ helloworld ---
[INFO] ==================== RUN TASK ====================
...
[INFO] [SUB] E/GraalGluon(11146):
[INFO] [SUB] E/GraalGluon(11146):
[INFO] [SUB] E/GraalGluon(11146): determineCpuFeaures
[INFO] [SUB] D/GraalCompiled(11146): Hello World!!
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Even though there is no UI for this application, we can still see the message being printed to the console.

4.3.2. HelloFX

Now let’s run HelloFX, a simple JavaFX application.

HelloFX.java
public class HelloFX extends Application {

    public void start(Stage stage) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        Label label = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");

        ImageView imageView = new ImageView(new Image(HelloFX.class.getResourceAsStream("/hellofx/openduke.png")));
        imageView.setFitHeight(200);
        imageView.setPreserveRatio(true);

        VBox root = new VBox(30, imageView, label);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 640, 480);
        scene.getStylesheets().add(HelloFX.class.getResource("styles.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gluonhq.samples</groupId>
    <artifactId>hellofx</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>hellofx</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>11</maven.compiler.release>
        <javafx.version>15.0.1</javafx.version>
        <client.maven.plugin.version>3.8.1</client.maven.plugin.version>
        <main.class>hellofx.HelloFX</main.class>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>

            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.5</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                </configuration>
            </plugin>

            <plugin>
                <groupId>com.gluonhq</groupId>
                <artifactId>client-maven-plugin</artifactId>
                <version>${client.maven.plugin.version}</version>
                <configuration>
                    <target>${client.target}</target>
                    <mainClass>${main.class}</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>desktop</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <client.target>host</client.target>
            </properties>
        </profile>
        <profile>
            <id>ios</id>
            <properties>
                <client.target>ios</client.target>
            </properties>
        </profile>
        <profile>
            <id>android</id>
            <properties>
                <client.target>android</client.target>
            </properties>
        </profile>
    </profiles>

</project>

Run the application to make sure everything works with Java 11:

mvn clean javafx:run
Target: Linux

On a Linux machine, run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ hellofx ---
[INFO] ==================== COMPILE TASK ====================
[INFO] [SUB] [hellofx.hellofx:4286]    classlist:   4,465.09 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:4286]        (cap):   1,578.18 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:4286]        setup:   3,635.99 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:4286]     (clinit):   1,148.59 ms,  4.02 GB
[INFO] [SUB] [hellofx.hellofx:4286]   (typeflow):  20,732.40 ms,  4.02 GB
[INFO] [SUB] [hellofx.hellofx:4286]    (objects):  22,106.29 ms,  4.02 GB
[INFO] [SUB] [hellofx.hellofx:4286]   (features):   3,153.63 ms,  4.02 GB
[INFO] [SUB] [hellofx.hellofx:4286]     analysis:  49,730.80 ms,  4.02 GB
[INFO] [SUB] [hellofx.hellofx:4286]     universe:   4,497.82 ms,  5.10 GB
[INFO] [SUB] [hellofx.hellofx:4286]      (parse):   4,921.35 ms,  5.10 GB
[INFO] [SUB] [hellofx.hellofx:4286]     (inline):   7,389.24 ms,  5.48 GB
[INFO] [SUB] [hellofx.hellofx:4286]    (compile):  26,684.64 ms,  5.79 GB
[INFO] [SUB] [hellofx.hellofx:4286]      compile:  41,273.83 ms,  5.78 GB
[INFO] [SUB] [hellofx.hellofx:4286]        image:   4,323.81 ms,  5.78 GB
[INFO] [SUB] [hellofx.hellofx:4286]        write:     761.30 ms,  5.78 GB
[INFO] [SUB] [hellofx.hellofx:4286]      [total]: 109,023.72 ms,  5.78 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hellofx.hellofx.o is created and can be found under target/client/x86_64-linux/gvm/tmp/SVM-*/hellofx.hellofx.o.

Run the client:link goal to produce the native image. As a result, target/client/x86_64-linux/hellofx is created. It can be executed directly or with mvn client:run.

HelloFX Linux Link Output
HelloFX Linux running
Target: macOS

On Mac OS X, run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ hellofx ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for x86_64-apple-darwin. This may take some time.
[INFO] [SUB] [hellofx.hellofx:2309]    classlist:   3,410.78 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:2309]        (cap):   1,509.86 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:2309]        setup:   3,863.89 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:2309]     (clinit):   1,257.74 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:2309]   (typeflow):  24,813.75 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:2309]    (objects):  25,161.07 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:2309]   (features):   3,090.12 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:2309]     analysis:  56,425.44 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:2309]     universe:   2,060.83 ms,  5.06 GB
[INFO] [SUB] [hellofx.hellofx:2309]      (parse):   6,364.53 ms,  5.06 GB
[INFO] [SUB] [hellofx.hellofx:2309]     (inline):   7,150.96 ms,  5.84 GB
[INFO] [SUB] [hellofx.hellofx:2309]    (compile):  43,871.39 ms,  5.93 GB
[INFO] [SUB] [hellofx.hellofx:2309]      compile:  61,332.73 ms,  5.93 GB
[INFO] [SUB] [hellofx.hellofx:2309]        image:   8,034.36 ms,  5.89 GB
[INFO] [SUB] [hellofx.hellofx:2309]        write:   1,337.65 ms,  5.89 GB
[INFO] [SUB] [hellofx.hellofx:2309]      [total]: 136,878.31 ms,  5.89 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hellofx.hellofx.o is created and can be found under target/client/x86_64-darwin/gvm/tmp/SVM-*/hellofx.hellofx.o.

Run the client:link goal to produce the native image. As a result, target/client/x86_64-darwin/hellofx is created. It can be executed directly or with mvn client:run.

HelloFX macOS Link Output
HelloFX macOS running
Target: Windows

On a Windows machine, run the client:compile goal. It produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ hellofx ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for x86_64-microsoft-windows. This may take some time.
[INFO] [SUB] Warning: Ignoring server-mode native-image argument --no-server.
[INFO] [SUB] [hellofx.hellofx:1872]    classlist:   4,129.20 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:1872]        (cap):   2,432.83 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:1872]        setup:   4,741.30 ms,  0.96 GB
[INFO] [SUB] WARNING GR-10238: VarHandle for static field is currently not fully supported. Static field private static volatile java.lang.System$Logger jdk.internal.event.EventHelper.securityLogger is not properly marked for Unsafe access!
[INFO] [SUB] [hellofx.hellofx:1872]     (clinit):     913.99 ms,  2.33 GB
[INFO] [SUB] [hellofx.hellofx:1872]   (typeflow):  47,466.16 ms,  2.33 GB
[INFO] [SUB] [hellofx.hellofx:1872]    (objects):  30,559.65 ms,  2.33 GB
[INFO] [SUB] [hellofx.hellofx:1872]   (features):   2,596.89 ms,  2.33 GB
[INFO] [SUB] [hellofx.hellofx:1872]     analysis:  83,244.37 ms,  2.33 GB
[INFO] [SUB] [hellofx.hellofx:1872]     universe:   2,483.86 ms,  2.31 GB
[INFO] [SUB] [hellofx.hellofx:1872]      (parse):  12,910.54 ms,  2.07 GB
[INFO] [SUB] [hellofx.hellofx:1872]     (inline):  21,259.04 ms,  2.61 GB
[INFO] [SUB] [hellofx.hellofx:1872]    (compile):  51,522.36 ms,  2.83 GB
[INFO] [SUB] [hellofx.hellofx:1872]      compile:  88,166.40 ms,  2.83 GB
[INFO] [SUB] [hellofx.hellofx:1872]        image:   5,539.33 ms,  2.78 GB
[INFO] [SUB] [hellofx.hellofx:1872]        write:     461.00 ms,  2.78 GB
[INFO] [SUB] [hellofx.hellofx:1872]      [total]: 189,148.96 ms,  2.78 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hellofx.hellofx.obj is created and can be found under target\client\x86_64-windows\gvm\tmp\SVM-*\hellofx.hellofx.obj.

Run the client:link goal to produce the native image. As a result, target\client\x86_64-windows\hellofx.exe is created. It can be executed directly or with mvn client:run.

HelloFX Windows Link Output
HelloFX Windows running
Target: iOS

This requires an iOS device that has to be plugged in at the run phase. A valid provisioning profile is required as well, either by enrolling to the Apple Developer program or by using free provisioning. See iOS Deployment.

On Mac OS X, use the ios profile and run mvn -Pios client:compile, that produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ hellofx ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for arm64-apple-ios. This may take some time.
[INFO] [SUB] [hellofx.hellofx:3374]    classlist:   3,413.81 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:3374]        (cap):     415.26 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:3374]        setup:   2,741.93 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:3374]     (clinit):   1,281.87 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]   (typeflow):  23,706.05 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]    (objects):  23,952.72 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]   (features):   2,950.68 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]     analysis:  54,112.42 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]     universe:   1,903.63 ms,  4.75 GB
[INFO] [SUB] [hellofx.hellofx:3374]      (parse):   6,064.08 ms,  5.06 GB
[INFO] [SUB] [hellofx.hellofx:3374]     (inline):  13,531.61 ms,  5.78 GB
[INFO] [SUB] [hellofx.hellofx:3374]    (compile):  52,150.30 ms,  5.71 GB
[INFO] [SUB] [hellofx.hellofx:3374]    (bitcode):   3,122.43 ms,  5.71 GB
[INFO] [SUB] [hellofx.hellofx:3374]    (prelink):  14,080.96 ms,  5.71 GB
[INFO] [SUB] [hellofx.hellofx:3374]       (llvm): 112,011.52 ms,  5.65 GB
[INFO] [SUB] [hellofx.hellofx:3374]   (postlink):   7,265.98 ms,  5.65 GB
[INFO] [SUB] [hellofx.hellofx:3374]      compile: 208,869.95 ms,  5.65 GB
[INFO] [SUB] [hellofx.hellofx:3374]        image:  12,751.53 ms,  5.56 GB
[INFO] [SUB] [hellofx.hellofx:3374]        write:   2,411.88 ms,  5.56 GB
[INFO] [SUB] [hellofx.hellofx:3374]      [total]: 286,614.27 ms,  5.56 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hellofx.hellofx.o is created and can be found under target/client/arm64-ios/gvm/tmp/SVM-*/hellofx.hellofx.o.

Run mvn -Pios client:link to produce the native image. As a result, target/client/arm64-ios/hellofx.app is created.

HelloFX iOS Link Output

Now it can be deployed to a plugged iOS device with mvn -Pios client:run.

HelloFX iOS deployed
Target: Android

This requires an Android device that has to be plugged in at the run phase.

On a Linux machine, use the android profile, and run mvn -Pandroid client:compile, that produces the following output:

[INFO] --- client-maven-plugin:0.1.35:compile (default-cli) @ hellofx ---
[INFO] ==================== COMPILE TASK ====================
[INFO] We will now compile your code for aarch64-linux-android. This may take some time.
[INFO] [SUB] [hellofx.hellofx:9119]    classlist:   3,089.42 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:9119]        (cap):     328.79 ms,  0.96 GB
[INFO] [SUB] [hellofx.hellofx:9119]        setup:   2,140.79 ms,  0.96 GB
[INFO] [SUB] WARNING GR-10238: VarHandle for static field is currently not fully supported. Static field private static volatile java.lang.System$Logger jdk.internal.event.EventHelper.securityLogger is not properly marked for Unsafe access!
[INFO] [SUB] [hellofx.hellofx:9119]     (clinit):     991.74 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]   (typeflow):  16,071.52 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]    (objects):  17,823.50 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]   (features):   2,687.95 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]     analysis:  38,896.58 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]     universe:   1,839.74 ms,  4.62 GB
[INFO] [SUB] [hellofx.hellofx:9119]      (parse):   6,003.11 ms,  4.96 GB
[INFO] [SUB] [hellofx.hellofx:9119]     (inline):   8,276.27 ms,  5.90 GB
[INFO] [SUB] [hellofx.hellofx:9119]    (compile):  43,421.83 ms,  5.86 GB
[INFO] [SUB] [hellofx.hellofx:9119]      compile:  60,386.00 ms,  5.86 GB
[INFO] [SUB] [hellofx.hellofx:9119]        image:   7,353.86 ms,  5.90 GB
[INFO] [SUB] [hellofx.hellofx:9119]        write:     593.95 ms,  5.90 GB
[INFO] [SUB] [hellofx.hellofx:9119]      [total]: 114,908.43 ms,  5.90 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And as a result, hellofx.hellofx.o is created and can be found under target/client/aarch64-android/gvm/tmp/SVM-*/hellofx.hellofx.o.

Note that the process takes some time. There will be performance improvements, but either way, it is convenient to test first on desktop (and with HotSpot) as much as possible (i.e. with mvn javafx:run), so client:compile doesn’t have to be repeated due to avoidable errors.

Run mvn -Pandroid client:link to produce the native image. As a result, target/client/aarch64-android/libhellofx.so is created.

Finally, run mvn -Pandroid client:package to bundle the application into an Android APK that can be installed on a device.

It produces the following output:

[INFO] --- client-maven-plugin:{client-maven-plugin-version}:package (default-cli) @ hellofx ---
[INFO] ==================== PACKAGE TASK ====================
[INFO] Default Android manifest generated in ~/Gluon/gluon-samples/HelloFX/target/client/aarch64-android/gensrc/android/AndroidManifest.xml.
Consider copying it to ~/Gluon/gluon-samples/HelloFX/src/android/AndroidManifest.xml before performing any modification
[INFO] Default Android resources generated in ~/Gluon/gluon-samples/HelloFX/target/client/aarch64-android/gensrc/android/res.
Consider copying them to ~/Gluon/gluon-samples/HelloFX/src/android/res before performing any modification
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

This creates the apk package which is available at target/client/aarch64-android/gvm/hellofx.apk.

HelloWorld Android apk

Now we are ready to install and run the application on a plugged-in Android device. Run mvn -Pandroid client:install client:run to install and launch the application on the device.

HelloFX Android Running

4.4. Configuration

This is for advanced users.

The plugin allows some customization to modify the default settings, which are:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>0.1.35</version>
    <configuration>
        <target>host</target>
        <mainClass>your.mainClass</mainClass>
        <bundlesList></bundlesList>
        <resourcesList></resourcesList>
        <reflectionList></reflectionList>
        <jniList></jniList>
        <attachList></attachList>
        <nativeImageArgs></nativeImageArgs>
        <verbose>false</verbose>
        <graalvmHome></graalvmHome>
        <javaStaticSdkVersion>11_ea+1</javaStaticSdkVersion>
        <javafxStaticSdkVersion>11_ea+1</javafxStaticSdkVersion>
        <enableSWRendering>false</enableSWRendering>
        <releaseConfiguration>
            <!-- Android -->
            <appLabel></appLabel>
            <versionCode>1</versionCode>
            <versionName>1.0</versionName>
            <providedKeyStorePath>${android-keystore-path}</providedKeyStorePath>
            <providedKeyStorePassword>${android-keystore-password}</providedKeyStorePassword>
            <providedKeyAlias>${android-key-alias}</providedKeyAlias>
            <providedKeyAliasPassword>${android-key-password}</providedKeyAliasPassword>
            <!-- iOS -->
            <bundleName></bundleName>
            <bundleVersion>1.0</bundleVersion>
            <bundleShortVersion>1.0</bundleShortVersion>
            <providedSigningIdentity></providedSigningIdentity>
            <providedProvisioningProfile></providedProvisioningProfile>
            <skipSigning>false</skipSigning>
        </releaseConfiguration>
    </configuration>
</plugin>

4.4.1. target

A string that defines the target platform. The default is host, which refers to the platform that currently hosts the process. It can be set also to ios to create native images for iOS devices (Aarch64), or android to create native images for Android devices (Aarch64).

Default: host

4.4.2. bundlesList

List of fully qualified resource bundles that will be added to the default list of resource bundles. By default, the list already includes the JavaFX bundles, like:

  • com/sun/javafx/scene/control/skin/resources/controls

  • com/sun/javafx/scene/control/skin/resources/controls-nt

  • com.sun.javafx.tk.quantum.QuantumMessagesBundle

For more advanced usage, read the Resource bundles section.

4.4.3. resourcesList

List of additional resource patterns or extensions that will be added to the default resource list that already includes:

  • png, jpg, jpeg, gif, bmp, ttf, raw

  • xml, fxml, css, gls, json, dat,

  • license, frag, vert, obj

We keep adding extensions to this list. Please check the source code for the latest list.

For more advanced usage, read the Resources section.

4.4.4. reflectionList

List of additional full qualified classes that will be added to the default reflection list, that already includes most of the JavaFX classes.

Note: The current list is added to a file that can be found under target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json.

For more advanced usage, read the JNI and Reflection section.

4.4.5. jniList

List of additional full qualified classes that will be added to the default jni list, that already includes most of the JavaFX classes.

Note: The current list is added to a file that can be found under target/client/$arch-$os/gvm/jniconfig-$arch-$os.json.

For more advanced usage, read the JNI and Reflection section.

4.4.6. attachList

If you want to include Gluon Attach services to your project, you can use attachList to including the name of the services, like:

<!-- dependencies -->
<dependency>
    <groupId>com.gluonhq.attach</groupId>
    <artifactId>display</artifactId>
    <version>4.0.9</version>
</dependency>

<!-- plugin -->
<configuration>
    <attachList>
        <list>display</list>
    </attachList>
</configuration>

By default the attachVersion is 4.0.9.

After saving the changes, the dependencies for the defined services will be resolved to include those for defined target, and when the native compile goal is executed, the native services implementations will be added to the reflection and JNI lists.

Note: Attach platform implementations will be added only to the goals of the Client plugin, but not to the JavaFX plugin. It is convenient to use Maven profiles to overcome this issue.

4.4.7. nativeImageArgs

List of additional arguments that will be added to the native image creation.

4.4.8. verbose

Set to true will generate a more verbose output to the console, useful when having errors, to identify possible issues.

Also, you can get a more verbose output for these goals running with -X:

mvn -X client:compile

Default: false

Note: Regardless the verbose value, the full logs can be found under target/client/$arch-$os/gmv/log.

4.4.9. graalvmHome

Path to GraalVM installation directory. This is only required when GRAALVM_HOME is not set.

Since: 0.1.3

4.4.10. javaStaticSdkVersion

The version of the Java static libraries. These will be located under: ~/.gluon/substrate/javaStaticSdk/$javaStaticSdkVersion/$target-$arch/labs-staticjdk/lib.

Default: 11_ea+1

4.4.11. javafxStaticSdkVersion

The version of the JavaFX SDK and its static libraries. These will be located under: ~/.gluon/substrate/javafxStaticSdk/$javaStaticSdkVersion/$target-$arch/sdk/lib.

Default: 15_ea+gvm24

4.4.12. Software Rendering

JavaFX applications can use software rendering when the graphics hardware on a system is insufficient to support hardware accelerated rendering. To enable software rendering set the enableSWRendering parameter to true in plugin configuration.

Note: Enabling software rendering will result in a longer compile time and larger native image.

Default: false

Since: 0.1.14

4.5. Config files

Some configuration options can alternatively be defined in configuration files instead of the configuration section of the plugin.

All the configuration files detailed below must be placed in the same folder named src/main/resources/META-INF/substrate/config.

When building the application, the plugin will also inspect every jar dependency for the existence of these configuration files. The files in the jar should be placed in the same folder: META-INF/substrate/config.

4.5.1. JNI and Reflection

For every class that is defined in jniList or reflectionList, it is included in the configuration files target/client/$arch-$os/gvm/jniconfig-$arch-$os.json and target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json respectively, with a definition like the following:

[
  {
    "name" : "package.name.ClassName",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true
  }
]

This has an impact on both compilation time and memory footprint, because all methods and fields are opened. Ideally, it is better to open only the minimal list of fields and methods that are required for your application to run.

You can create the following files:

  • jniconfig.json and/or reflectionconfig.json: these are applied to all targets

  • jniconfig-$arch-$os.json and/or reflectionconfig-$arch-$os.json: these are only applied to targets that match both the architecture and operating system. For example, specifying reflection configuration for linux 64bit can be achieved by creating a file called reflectionconfig-x86_64-linux.json.

The files must contain the fully qualified name of classes, together with some attributes (like allDeclaredFields), methods and/or fields that will be invoked via reflection and that are not already part of the list.

For instance, the configuration below only adds the method named valueOf of the MaterialDesignIcon class to the final reflection configuration:

[
  {
    "name":"com.gluonhq.charm.glisten.visual.MaterialDesignIcon",
    "methods":[
      {"name":"valueOf","parameterTypes":["java.lang.String"] }
    ]
  }
]

For more information, see Reflection use in Native Images.

4.5.2. Resources

The resourcesList configuration option can also be defined in a configuration file:

  • resourceconfig.json: applied to all targets

  • resourceconfig-$arch-$os.json: only applied to targets that match the given architecture and operating system

For more information, see Accessing resources in Native Images.

For instance, the configuration below includes a pattern to load all *.txt files found:

[
  {
  "resources": [
    {"pattern": ".*\\.txt$"}
  ]
  }
]

4.5.3. Resource bundles

The bundlesList configuration option can also be defined in a configuration file:

  • resourcebundles.json: applied to all targets

  • resourcebundles-$arch-$os.json: only applied to targets that match the given architecture and operating system

For instance, below are the fully qualified name of two given resource bundles that will be included:

com.mycompany.myproject.mybundle1
com.mycompany.myproject.mybundle2

4.5.4. Build time initialization

If you need to set a class or list of classed to be initialized at build time, you can add their fully qualified name to the following configuration files:

  • initbuildtime: applied to all targets

  • initbuildtime-$arch-$os: only applied to targets that match the given architecture and operating system

This can be especially convenient for enums that have a large list of values.

For more information, see Class initialization in Native Image.

4.6. Release Configuration

These options allow setting release parameters for Android and iOS.

4.6.1. Android

appLabel

A user-visible short name for the app, if not set, the Maven project’s name will be used.

versionCode

A positive integer used as an internal version number, by default is set to 1.

versionName

A string used as the version number shown to users, like <major>.<minor>.<point>. By default is 1.0.

Signing for release:

If these 4 parameters are not set, the app will be signed for debug only.

  • providedKeyStorePath: A string with the path to a keystore file that can be used to sign the Android apk

  • providedKeyStorePassword: A string with the password of the provide keystore file

  • providedKeyAlias: A string with an identifying name for the key

  • providedKeyAliasPassword: A string with a password for the key

Note that it can be convenient to define the values for these options in the ~/.m2/settings.xml file, using Maven profiles.

4.6.2. iOS

bundleName

A user-visible short name for the bundle. If not set, the Maven project’s name will be used.

bundleVersion

The version of the build that identifies an iteration of the bundle. A string composed of one to three period-separated integers, containing numeric characters (0-9) and periods only. Default 1.0.

bundleShortVersion

A user-visible string for the release or version number of the bundle. A string composed of one to three period-separated integers, containing numeric characters (0-9) and periods only. Default 1.0.

Signing for development or distribution
  • providedSigningIdentity: String that identifies a valid certificate that will be used for iOS development or iOS distribution.

  • providedProvisioningProfile: String with the name of the provisioning profile created for iOS. When not provided, the plugin will be selected from all the valid identities found installed on the machine from any of these types:

    iPhone Developer|Apple Development|iOS Development|iPhone Distribution

and that were used by the provisioning profile development or distribution of the given app. When not provided, the plugin will try to find a valid installed provisioning profile that can be used to sign the app, including wildcards.

skipSigning

Boolean that can be used to skip signing iOS apps. This will prevent any deployment, but can be useful to run tests without an actual device.

5. Platforms

Gluon applications can run on various platforms. These applications can run directly on the JVM on all desktop and embedded platforms without any additional requirement.

Gluon applications can also be converted to native-image to target a specific platform. Few native-image targets have a dependency on the platform on which they can be built. For example, iOS images can currently be only produced on a MacOS.

Native-images can be created for desktop and mobile platforms. Embedded platform support will be added soon.

In this section, we will discuss the requirements, procedure, and restrictions for development and deployment of Gluon applications across platforms.

Gluon builds for a specific client platform are required to run a specific host platform (e.g. Windows native build has to run on Windows, an iOS native build have to run on macOS). This hurdle can be overcome by using a build automation tool like Github Actions, which provides build 'runners' for every major OS. Each platform section below contains an example Github Actions Workflow for that platform. You can also have a look at the Hello Gluon CI Sample which combines a workflow for all supported platforms.

5.1. Linux

Gluon applications can run on JVM without any additional requirement. However, to create native-images for your Gluon application, you will need the latest version of GraalVM.

The latest version of GraalVM can be downloaded from https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-20.2.0.

Set the GRAALVM_HOME environment variable to point to the GraalVM directory:

export GRAALVM_HOME=/path/to/graalvm-ce-java11-20.2.0

In addition to GraalVM, the following packages are also required:

  • gcc version 6 or higher

  • ld version 2.26 or higher

5.1.1. CentOS

Required packages

Execute the following command to install the required yum packages:

sudo yum install pkgconfig gtk3-devel libXtst-devel
CentOS 6/7: Developer Toolset

By default, CentOS 6 and 7 bundle gcc version 4.4.x and 4.8.x respectively. The easiest way to install a more recent version is to install Developer Toolset 6 or higher using Software Collections. The installation process is described in detail at: https://www.softwarecollections.org/en/scls/rhscl/devtoolset-8/

5.1.2. Linux native builds using Github Actions

Using this Github workflow, you can checkout, build, and upload the native binary as a build artifact. The steps are described in code comments:

jobs:
build:
    runs-on: ubuntu-latest
    steps:
        # Checkout your code
        - uses: actions/checkout@v2

        # Make sure the latest GraalVM is installed.
        # after this step env.JAVA_HOME will point to the GraalVM location
        - name: Setup GraalVM environment
            uses: DeLaGuardo/setup-graalvm@master
            with:
            graalvm-version: 20.2.0.java11

        # Install extra required packaged on top of ubuntu-latest
        - name: Install libraries
            run: sudo apt install libasound2-dev libavcodec-dev libavformat-dev libavutil-dev libgl-dev libgtk-3-dev libpango1.0-dev libxtst-dev

        # Create a staging directory where the binary will be copied into
        - name: Make staging directory
            run: mkdir staging

        # Install the Gluon License (optional)
        # Using this step requires you to set a GLUON_LICENSE secret in the secret configuration of your repo. Have a look at https://gluonhq.com/products/mobile/buy for more information about obtaining a license.
        - name: Gluon License
            uses: gluonhq/gluon-build-license@v1
            with:
            gluon-license: ${{ secrets.GLUON_LICENSE }}


        # Build your project using Maven
        # The desktop profile is used, which means a native build will be created for the host platform (in this case Linux) itself.
        - name: Gluon Build
            run: mvn -Pdesktop client:build client:package
            env:
            GRAALVM_HOME: ${{ env.JAVA_HOME }}


        # Copy the native binary to the staging directory
        - name: Copy native client to staging
            run: cp -r target/client/x86_64-linux/HelloGluon staging


        # Upload the staging directory as a build artifact. You will be able to download this after the build finishes.
        - name: Upload
            uses: actions/upload-artifact@v2
            with:
            name: Package
            path: staging

You can see this workflow in action in the Hello Gluon CI Sample.

5.2. Mac OS

Gluon applications can run on JVM without any additional requirement. However, to create native-images for your Gluon application, you will need the latest version of GraalVM.

The latest version of GraalVM can be downloaded from https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-20.2.0.

Set the GRAALVM_HOME environment variable to point to the GraalVM directory:

export GRAALVM_HOME=/path/to/graalvm-ce-java11-20.2.0/Contents/Home

5.2.1. Mac OS native builds using Github Actions

Using this Github workflow, you can checkout, build, and upload the native binary as a build artifact. The steps are described in code comments:

jobs:
  build:
    runs-on: macOS-latest
    steps:
      # Checkout your code
      - uses: actions/checkout@v2

      # Configure the Xcode version
      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: '11.7.0'

      # Make sure the latest GraalVM is installed.
      # after this step env.JAVA_HOME will point to the GraalVM location
      - name: Setup GraalVM environment
        uses: DeLaGuardo/setup-graalvm@master
        with:
          graalvm-version: 20.2.0.java11

      # Create a staging directory where the binary will be copied into
      - name: Make staging directory
        run: mkdir staging

      # Install the Gluon License (optional)
      # Using this step requires you to set a GLUON_LICENSE secret in the secret configuration of your repo. Have a look at https://gluonhq.com/products/mobile/buy for more information about obtaining a license.
      - name: Gluon License
        uses: gluonhq/gluon-build-license@v1
        with:
          gluon-license: ${{ secrets.GLUON_LICENSE }}

      # Build your project using Maven
      # The desktop profile is used, which means a native build will be created for the host platform (in this case Mac OS) itself.
      - name: Gluon Build
        run: mvn -Pdesktop client:build client:package
        env:
          GRAALVM_HOME: ${{ env.JAVA_HOME }}

      # Copy the native binary to the staging directory
      - name: Copy native client to staging
        run: cp -r target/client/x86_64-darwin/HelloGluon* staging

      # Upload the staging directory as a build artifact. You will be able to download this after the build finishes.
      - name: Upload
        uses: actions/upload-artifact@v2
        with:
          name: Package
          path: staging

You can see this workflow in action in the Hello Gluon CI Sample.

5.3. Windows

Gluon applications can run on JVM without any additional requirement. However, to create native-images for your Gluon application, you will need the latest version of GraalVM.

The latest version of GraalVM can be downloaded from https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-20.2.0.

Set the GRAALVM_HOME environment variable to point to the GraalVM directory:

set GRAALVM_HOME=C:\path\to\graalvm-ce-java11-20.2.0

In addition to GraalVM, Microsoft Visual Studio 2019 is also required. The Community Edition is sufficient and can be downloaded from https://visualstudio.microsoft.com/downloads/.

Please make sure to choose the English Language Pack. GraalVM might not be able to detect the installed compiler version in other languages.

During the installation process, make sure to select at least the following individual components:

  • Choose the English Language Pack

  • C++/CLI support for v142 build tools (14.25 or later)

  • MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.25 or later)

  • Windows Universal CRT SDK

  • Windows 10 SDK (10.0.19041.0 or later)

All build commands, be it with Maven or Gradle, must be executed in a Visual Studio 2019 Command Prompt called x64 Native Tools Command Prompt for VS 2019. A shortcut can be found in the "Start Menu", or you can search the application in the search box. Read the Microsoft documentation for more information.

Alternatively, you can run cmd.exe /k "<path to VS2019>\VC\Auxiliary\Build\vcvars64.bat from any other terminal before you can start using the build commands.

5.3.1. Windows native builds using Github Actions

Using this Github workflow, you can checkout, build, and upload the native binary as a build artifact. The steps are described in code comments:

  build:
    runs-on: windows-latest
    steps:
      # Checkout your code
      - uses: actions/checkout@v2

      # Setup the Windows build environment
      - name: Add msbuild to PATH
        uses: microsoft/setup-msbuild@v1.0.2

      - name: Visual Studio shell
        uses: egor-tensin/vs-shell@v1


      # Make sure the latest GraalVM is installed.
      # after this step env.JAVA_HOME will point to the GraalVM location
      - name: Setup GraalVM environment
        uses: DeLaGuardo/setup-graalvm@master
        with:
          graalvm-version: 20.2.0.java11

      # Create a staging directory where the binary will be copied into
      - name: Make staging directory
        run: mkdir staging

      # Install the Gluon License (optional)
      # Using this step requires you to set a GLUON_LICENSE secret in the secret configuration of your repo. Have a look at https://gluonhq.com/products/mobile/buy for more information about obtaining a license.
      - name: Gluon License
        uses: gluonhq/gluon-build-license@v1
        with:
          gluon-license: ${{ secrets.GLUON_LICENSE }}

      # Build your project using Maven
      # The desktop profile is used, which means a native build will be created for the host platform (in this case Windows) itself.
      - name: Gluon Build
        run: mvn -Pdesktop client:build client:package
        env:
          GRAALVM_HOME: ${{ env.JAVA_HOME }}

      # Copy the native binary to the staging directory
      - name: Copy native client to staging
        run: cp -r target/client/x86_64-windows/HelloGluon.exe staging

      # Upload the staging directory as a build artifact. You will be able to download this after the build finishes.
      - name: Upload
        uses: actions/upload-artifact@v2
        with:
          name: Package
          path: staging

You can see this workflow in action in the Hello Gluon CI Sample.

5.4. Android

Currently, Android can be built only on Linux OS (or from Windows WSL2). Alternatively you can use a GitHub Actions workflow.

5.4.1. Android development

In addition to platform specific GraalVM, Android SDK/NDK is required to build applications for the android platform.

Both SDK/NDK will be downloaded automatically by the "Gluon Client" plugin and configured with the required packages.

But if you already have a local installation of the Android SDK/NDK, you can override this behaviour by defining environment variables:

  • ANDROID_SDK: points to the Android SDK folder on your system

  • ANDROID_NDK: points to the Android NDK folder on your system

Please make sure you have installed the following required packages:

  • platform-tools

  • platforms;android-29

  • build-tools;29.0.3

  • ndk-bundle

  • extras;android;m2repository

  • extras;google;m2repository

To target android devices, <target>android</target> needs to be added to the Gluon Client plugin configuration:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>${client.plugin.version}</version>
    <configuration>
        <target>android</target>
        <mainClass>${mainClassName}</mainClass>
    </configuration>
</plugin>

Alternatively, a Maven profile can be used:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>${client.plugin.version}</version>
    <configuration>
        <target>${client.target}</target>
        <mainClass>${mainClassName}</mainClass>
    </configuration>
</plugin>
<profiles>
    <profile>
        <id>android</id>
        <properties>
            <client.target>android</client.target>
        </properties>
    </profile>
</profiles>

The project can be built using mvn -Pandroid client:build. This will run the compilation phase and link the compiled objects into an android executable.

To install the application to a connected android device, run mvn -Pandroid client:install.

Finally, you can call mvn -Pandroid client:run to launch the application on the device. The client plugin will also start adb logcat to print out debugging information from the device to the console.

5.4.2. Android distribution

Build your application

Run mvn -Pandroid client:package to generate an Android Application Package (APK) that can be installed on any Android device.

To make sure you’re using the correct signing key and keystore, you can set these using the releaseConfiguration settings:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    ...
    <configuration>
        ...
        <releaseConfiguration>
            <versionCode>1</versionCode>
            <providedKeyStorePath>${android-keystore-path}</providedKeyStorePath>
            <providedKeyStorePassword>${android-keystore-password}</providedKeyStorePassword>
            <providedKeyAlias>${android-key-alias}</providedKeyAlias>
            <providedKeyAliasPassword>${android-key-password}</providedKeyAliasPassword>
        </releaseConfiguration>
...
   </configuration>
</plugin>
versionCode is the unique build number of your app. You need to increase it for every upload to the Play Store.

Please follow these steps in the Android documentation to create a keystore.

Override default Android setting

If you need to set specific Android settings in AndroidManifest.xml, copy the file from target/client/aarch64-android/gensrc/android/AndroidManifest.xml to

src/android/AndroidManifest.xml
Override the default icon

By default, an icon is generated in target/client/aarch64-android/gensrc/android/rest/. If you’re distributing your app, you most certainly will want to use a custom icon.

Please refer to the Android docs for more information.

However, if you would just like to override the icon, there are nice online generators as well (e.g. https://appicon.co/)

To use the generated icon follow these steps:

  • then remove src/android/res

  • then copy the generated Android icon files (starting with mipmap-) to src/android/res

5.4.3. Android builds using Github Actions

The following sections show how to use Github Actions to build and release your app automatically using Github infrastructure. Using this Github workflow, you can develop your JavaFX application anywhere you like and the Github workflow will make your application available for testing in the Play Store on push.

5.4.4. Github Workflow File

Using this Github workflow, you can checkout, build, and upload the native binary to the Play Store. The steps are described in code comments and more details information can be found in the next sections.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Checkout your project
      - uses: actions/checkout@v2

      # Setup the latest GraalVM
      - name: Setup GraalVM environment
        uses: DeLaGuardo/setup-graalvm@master
        with:
          graalvm-version: 20.2.0.java11

      # Install extra requirements on top of ubuntu-latest
      - name: Install libraries
        run: sudo apt install libasound2-dev libavcodec-dev libavformat-dev libavutil-dev libgl-dev libgtk-3-dev libpango1.0-dev libxtst-dev


      # Setup the Android keystore, based on a repo secret. See the section 'Setup Android Keystore' below.
      - name: Setup Android Keystore
        id: android_keystore_file
        uses: timheuer/base64-to-file@v1
        with:
          fileName: 'my.keystore'
          encodedString: ${{ secrets.GLUON_ANDROID_KEYSTORE_BASE64 }}

      # Install the Gluon License (optional)
      # Using this step requires you to set a GLUON_LICENSE secret in the secret configuration of your repo. Have a look at https://gluonhq.com/products/mobile/buy for more information about obtaining a license.
      - name: Gluon License
        uses: gluonhq/gluon-build-license@v1
        with:
          gluon-license: ${{ secrets.GLUON_LICENSE }}

      # Build your project using Maven
      # The android profile is used, which means a native build will be created for the client target android.
      # This step also uses some env variables taken from the repo secrets. See the section 'Setup Android Keystore' below.
      - name: Gluon Build
        run: mvn -Pandroid client:build client:package
        env:
          GRAALVM_HOME: ${{ env.JAVA_HOME }}
          GLUON_ANDROID_KEYSTOREPATH: ${{ steps.android_keystore_file.outputs.filePath }}
          GLUON_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.GLUON_ANDROID_KEYSTORE_PASSWORD }}
          GLUON_ANDROID_KEYALIAS: ${{ secrets.GLUON_ANDROID_KEYALIAS }}
          GLUON_ANDROID_KEYALIAS_PASSWORD: ${{ secrets.GLUON_ANDROID_KEYALIAS_PASSWORD }}

      # Create staging directory in which the apk will be copied
      - name: Make staging directory
        run: mkdir staging

      # Copy the apk to the staging directory
      - name: Copy native clients to staging
        run: cp -r target/client/aarch64-android/gvm/*.apk staging

      # Upload the staging directoy as a build artifact
      - name: Upload
        uses: actions/upload-artifact@v2
        with:
          name: Package
          path: staging

      # Upload the apk to the Google Play Store. See the section below.
      - name: Upload to Google Play
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GLUON_ANDROID_SERVICE_ACCOUNT_JSON }}
          packageName: com.gluonhq.samples.hellogluon
          releaseFiles: target/client/aarch64-android/gvm/HelloGluon.apk
          track: beta

You can see this workflow in action in the Hello Gluon CI Sample.

Changes to your build

Since this workflow uploads the build to the Play Store, it needs a unique buildnumber each time. If you have overriden the AndroidManifest.xml, you must make sure to increment the android:versionCode attribute. If you didn’t override AndroidManifest.xml, you can use this client configuration:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
...
    <configuration>
...
        <releaseConfiguration>
            <versionCode>${env.GITHUB_RUN_NUMBER}</versionCode>
            ...
        </releaseConfiguration>
...
   </configuration>
</plugin>

Using env.GITHUB_RUN_NUMBER, Github Actions will set the versionCode to an incremental build number.

Setup Android Keystore

Before you can upload your .apk file to the Google Play Store, you need to sign it. Please follow these steps in the Android documentation.

The result of those steps should be:

  • a keystore file: you need to base64 encode that file and configure it in the repo secret GLUON_ANDROID_KEYSTORE_BASE64

  • the keystore will have a password, configure that in the repo secret GLUON_ANDROID_KEYSTORE_PASSWORD

  • a key alias: configure that in the repo secret GLUON_ANDROID_KEYALIAS

  • that key will have a password, configure that in the repo secret GLUON_ANDROID_KEYALIAS_PASSWORD

Upload the apk to the Google Play Store

The actual upload to the Google Play store is performed by the Github Action r0adkll/upload-google-play. Have a look at its documenation.

The most important part to configure in this case is the GLUON_ANDROID_SERVICE_ACCOUNT_JSON. To configure a service account for upload to the Play Store, follow these steps in the Android documentation.

5.5. iOS

Currently, iOS can be built only on Mac OS X. Alternatively you can use a GitHub Actions workflow.

In addition to GraalVM for Mac OS, the following packages are required and can be installed using Homebrew:

  • brew install --HEAD libusbmuxd

  • brew install --HEAD libimobiledevice

Xcode version 11 or higher is also required. Xcode can be installed from the Mac App Store. Once installed, open it and accept the license terms.

5.5.1. iOS Development

If you want to test and deploy your app on a local iOS device, you’ll need to enroll in the Apple Developer Program. If you’re not ready for that or just want to test our your personal device, you can also use a "free provisioning profile".

Either way, first of all you will need a unique Apple ID.

Creating an Apple ID

In case you don’t have it yet, create an Apple ID.

Create Apple ID

You will need a valid email account, and you will receive and email to validate and activate the Apple ID.

In case you are going to use free provisioning, this Apple ID must not be connected to the Apple Developer Program.

Once you have your Apple ID verified and activated, open Xcode, go to Preferences → Accounts. Press the + button at the bottom left to add an account, select `Apple ID, and include the email address used to create the Apple ID.

Create account
Xcode project

The first time you start deploying to an iOS device, you need to follow these steps, so Xcode configures your device properly for future iOS deployment.

Note: If you are using free provisioning, you will have to follow this procedure every time you create a new project.

Create an empty Xcode project

Open Xcode and go to File → New → Project…​, and select a simple template for iOS, like Single View App.

Single View App
Name and bundle

Provide a name and a bundle ID to the project.

Note: if you are using free provisioning, the bundle ID must match the one defined in your Java project (or the other way around).

Project data

Press next, provide a location and create the project.

Signing the project

Once the project is created, it will be opened and you will have access to the General tab with the identity and signing information.

At this point you should plug your iOS device, and select it from the top drop-down list of build schemes that list your iOS device and iOS simulators.

Project general

Make sure you have selected Automatically manage signing, and select a valid Team from the drop-down list.

If you get the error like: The app ID "hellofx.HelloFX" cannot be registered to your develoment team, this means that the proposed app ID is already in use or has already been registered by the Apple servers, and cannot longer be used. The only solution is to change the app ID until you get a valid one:

Valid app ID

Note that the provisioning profile generated by Xcode for that app ID can be found in your ~/Library/MobileDevice/Provisioning Profiles folder.

For free provisioning, this profile will expire after one week.

Deploying to your device

Once everything is in place (there are no visible errors in the General screen), press the Play button that will build and run the current scheme.

Note: Using free provisioning it is possible that the run fails with a message like: Could not launch "HelloFX", that prompts you to verify the Developer App certificate is trusted on your device, as by default the certificate is untrusted. Go to your iPhone, Settings → General → Device Management, and select the certificate to trust it. Then go back to Xcode and run again. This time, the app will launch and you will get a white screen.

Press stop to finish and quit Xcode.

Build your application

Back to your Java project, you can run mvn client:build to build and link your native binary.

If you have already done the AOT compilation phase (client:compile), you need to run mvn client:link. This will generate the src/ios/Default-Info.plist file and the src/ios/assets folder with a default iconset.

You can modify the default values and iconset.

If you are using free provisioning or an explicit provisioning profile, edit the src/ios/Default-Info.plist file and set the exact same bundle ID as the one in the profile (note it is case sensitive):

<dict>
    <key>CFBundleIdentifier</key>
    <string>hello.fx.HelloFX</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
...

If you are using a wildcard, make sure the domain (if any) matches.

Note: the bundle identifier key doesn’t need to match your main class.

If you have modified the plist file, save it, and run again mvn client:link.

Using <verbose>true</verbose> in the client plugin can be of help to trace which provisioning profile is used, and to check that a valid one was found:

CONSIDER provprofile ProvisioningProfile [type=Development, file=/Users/<user>/Library/MobileDevice/Provisioning Profiles/caae4d7d-b5d2-4************.mobileprovision, uuid=caae4d7d-b5d2-4************, name=iOS Team Provisioning Profile: hello.fx.HelloFX, appIdName=XC hello fx HelloFX, appIdPrefix=D6QST****, appId=D6QST****.hello.fx.HelloFX, creationDate=2019-06-18, expirationDate=2019-06-25, certFingerprints=[DA3D9B8C7640*******]]

YES, we have a MATCH!!
Run your application

If the link task finishes successfully, you can deploy. With your iOS device plugged in, run: mvn client:run.

Note: If you are using free provisioning, this will deploy your Java app replacing the Xcode app.

At the end of the process you should get your app running on your iOS device.

5.5.2. iOS Distribution

To distribute your app using the App Store, you (as individual or as a company) must enroll in the Apple Developer Program.

For iOS distribution, you need to generate:

  • A valid signing identity

  • A valid provisioning profile

The following steps describe how these can be generated.

iOS Distribution Certificate

Go to the Developer portal, and access Certificates, Identifiers & Profiles. Select Certificates from the left, and press +, to create a Development certificate (for testing). Later on you will need a Distribution certificate for distribution through the App Store.

New Certificate

To generate a certificate, you need a Certificate Signing Request (CSR) file from your Mac.

Certificate Signing Request

According to Apple Help, these are the steps to generate the CSR:

  • Launch the Keychain Access application, located in /Applications/Utilities.

  • Choose Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority.

  • Enter an email address in the User Email Address field.

  • Enter a name for the key.

  • Leave the CA Email Address field empty.

  • Choose Saved to disk, and click Continue.

  • Save the CSR file.

Upload CSR

Return to the Developer portal and upload the CSR:

Upload CSR
Download and install the certificate

Once the CSR is uploaded, a certificate is generated. Download this certificate to your Mac.

Download Certificate

Then double-click the .cer file to install in Keychain Access.

Identifiers

Go to the Developer portal, and access Certificates, Identifiers & Profiles. Select Identifiers from the left, and press +, to create a new app identifier.

You can enable app services when you create the App ID or modify these settings later on.

New App Identifier

Provide a name and make sure you use the exact Bundle ID from your app, the one listed in your Default-Info.plist file under the CFBundleIdentifier key.

App ID

Alternatively, you can use a wildcard that will let you use a single App ID to match multiple apps. It is recommended to use your domain name, to filter out other apps.

Wildcard

Select any of the required services (if you are using an explicit Bundle ID), and click continue to create the App ID.

Devices

To create a provisioning profile for development, the devices that are allowed to run the application have to be identified.

To add your testing devices, you need to provide their UDID.It can be found from Xcode → Window → Devices and Simulators. Plug your device, select it from Connected devices, and then select its identifier and copy it.

Device identifier

Go to the Developer portal, and access Certificates, Identifiers & Profiles. Select Devices from the left, and press +, to add a new device.

Add a name and paste the device’s identifier.

Add Device
Profiles

Go to the Developer portal, and access Certificates, Identifiers & Profiles. Select Profiles from the left, and press +, to add a new provisioning profile.

To add a Development profile (later on you will need a Distribution one), select iOS App Development, and press continue.

iOS App Development

From the drop down list, select a valid App ID:

  • If you plan to use services such as Game Center, In-App Purchase, and Push Notifications, or want a Bundle ID unique to a single app, use an explicit App ID.

  • If you want to create one provisioning profile for multiple apps or don’t need a specific Bundle ID, select a wildcard App ID.

iOS App ID

Press continue and select one or more certificates that should be included in this provisioning profile.

Press continue and select one or more devices that should be include in this provisioning profile.

Press continue and finally, give a name to the provisioning profile, it will be used to identify the profile in the portal. Then press Generate.

Provisioning profile

When finished, download it and install it by double clicking on the file.

Build your application

You can run mvn client:build client:package to build and package your native application.

To make sure you’re using the correct signing identify and provisioning profile, you can set these using the releaseConfiguration settings:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
...
    <configuration>
...
        <releaseConfiguration>
            <bundleVersion>1</bundleVersion>
            <bundleShortVersion>1.0</bundleShortVersion>
            <providedSigningIdentity>iPhone Distribution: Gluon Software BVBA (**********)</providedSigningIdentity>
            <providedProvisioningProfile>my-provisioning-profile</providedProvisioningProfile>
        </releaseConfiguration>
...
   </configuration>
</plugin>
bundleVersion is the unique build number of your app. You need to increase it for every upload to TestFlight.
Note that you can omit providedSigningIdentity and providedProvisioningProfile when running in a Github Actions workflow, since there will be only 1 valid identity, that one will be used be default.
Override default iOS settings

If you need to set specific iOS settings in Default-Info.plist, copy the file from target/client/arm64-ios/gensrc/ios/Default-Info.plist to

src/ios/Default-Info.plist
Override the default icon

By default, an icon is generated in target/client/arm64-ios/gensrc/ios/Assets.xcassets/AppIcon.appiconset. If you’re distributing your app, you most certainly will want to use a custom icon.

Please refer to the Apple docs for more information.

However, if you would just like to override the AppIcon, there are nice online generators as well (e.g. https://appicon.co/)

To use the generated icon follow these steps:

  • copy target/client/arm64-ios/gensrc/ios/Assets.xcassets to src/ios

  • then remove src/ios/assets/Assets.xcassets/AppIcon.appiconset

  • then copy the generated AppIcon.appiconset to src/ios/Assets.xcassets

5.5.3. iOS builds using Github Actions

The following sections show how to use Github Actions to build and release your app automatically using Github infrastructure. You don’t need a Mac, except for a one-time config step to setup the distribution certificate for the App Store. Using this Github workflow, you can develop your JavaFX application anywhere you like and the Github workflow will make your application available for testing in TestFlight on push.

Github Workflow File

Using this Github workflow, you can checkout, build, and upload the native binary to TestFlight. The steps are described in code comments and more details information can be found in the next sections.

jobs:
  build:
    runs-on: macOS-latest
    steps:
      # Setup Xcode
      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: '11.7.0'

      # Checkout your code
      - uses: actions/checkout@v2

      # Make sure the latest GraalVM is installed.
      # after this step env.JAVA_HOME will point to the GraalVM location
      - name: Setup GraalVM environment
        uses: DeLaGuardo/setup-graalvm@master
        with:
          graalvm-version: 20.2.0.java11

      # Setup the signing identify. See the section 'Configuring the signing identity'
      - uses: Apple-Actions/import-codesign-certs@v1
        with:
          p12-file-base64: ${{ secrets.GLUON_IOS_CERTIFICATES_FILE_BASE64 }}
          p12-password: ${{ secrets.GLUON_IOS_CERTIFICATES_PASSWORD }}

      # Download the appropriate provisining profile using Apple's Appstore Connect API.  See the section 'Using the Appstore Connect API' below
      - uses: Apple-Actions/download-provisioning-profiles@v1
        with:
          bundle-id: com.gluonhq.hello.HelloGluonApp
          issuer-id: ${{ secrets.GLUON_IOS_APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.GLUON_IOS_APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.GLUON_IOS_APPSTORE_PRIVATE_KEY }}

      # Install the Gluon License (optional)
      # Using this step requires you to set a GLUON_LICENSE secret in the secret configuration of your repo. Have a look at https://gluonhq.com/products/mobile/buy for more information about obtaining a license.
      - name: Gluon License
        uses: gluonhq/gluon-build-license@v1
        with:
          gluon-license: ${{ secrets.GLUON_LICENSE }}

      # Build your project using Maven
      # The ios profile is used, which means a native build will be created for the client target ios.
      - name: Gluon Build
        run: mvn -Pios client:build client:package
        env:
          GRAALVM_HOME: ${{ env.JAVA_HOME }}

      # Upload the build .ipa file to TestFlight using the Appstore Connect API.
      - uses: Apple-Actions/upload-testflight-build@master
        with:
          app-path: target/client/arm64-ios/HelloGluon.ipa
          issuer-id: ${{ secrets.GLUON_IOS_APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.GLUON_IOS_APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.GLUON_IOS_APPSTORE_PRIVATE_KEY }}

You can see this workflow in action in the Hello Gluon CI Sample.

Changes to your build

Since this workflow uploads the build to TestFlight, it needs a unique build number each time. If you have overriden the Default-Info.plist settings you must make sure to increment the CFBundleVersion. If you didn’t override the Default-Info.plist, you can use this client configuration:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
...
    <configuration>
...
        <releaseConfiguration>
            <bundleVersion>${env.GITHUB_RUN_NUMBER}</bundleVersion>
            ...
        </releaseConfiguration>
...
   </configuration>
</plugin>

Using env.GITHUB_RUN_NUMBER, Github Actions will set the bundleVersion to an incremental build number.

Configuring the signing identity

The workflow step Apple-Actions/import-codesign-certs above uses 2 configurations from your repo’s secrets configuration:

  • GLUON_IOS_CERTIFICATES_FILE_BASE64

  • GLUON_IOS_CERTIFICATES_PASSWORD

Please refer to the Github Actions docs for more information on how to set them.

To generate the values for these secrets, you’ll need a Mac for this one time configuration and follow the steps described in the iOS Distribution Certificate section.

Once you imported the certificate in Keychain Access, follow these steps:

  • Make sure to expand the certificate so you can also see the private key that was used to sign it.

  • Select both the certificate and private key and choose File→Export items

Keychain Export
  • Export it to a .p12 file and choose a password. Set this password as GLUON_IOS_CERTIFICATES_PASSWORD

  • Base64 encode the .p12 file. Set this value as GLUON_IOS_CERTIFICATES_FILE_BASE64

Next to the signing identify, you will also need to create a provisining profile that can be used for distribution. However, it does not have to be downloaded. A workflow step will download it using the Appstore Connect API. (see next section)

Using the Appstore Connect API

The above workflow uses the Appstore Connect API to automate these 2 steps:

  • Download the provisioning profile

  • Upload the .ipa file to TestFlight, from where it can be tested and moved to production.

To generate an Appstore Connect API key, follow these steps as described by Apple.

Following that, you can set these repo secrets:

  • GLUON_IOS_APPSTORE_ISSUER_ID: see image arrow 1

  • GLUON_IOS_APPSTORE_KEY_ID: see image arrow 2

  • GLUON_IOS_APPSTORE_PRIVATE_KEY: the text contents of the file you downloaded while creating the API key.

Appstore Connect API

5.6. JavaFX on Embedded

5.6.1. Setting up the Raspberry Pi 4

There are plenty of configurations with different hardware and operating systems that require different versions of binaries, linked to different libraries.

The distribution of JavaFX 16-ea for embedded is tested on a Raspberry Pi 4, using the Raspberry Pi OS distribution.

JavaFX is capable of running on a large number of other hardware and operating systems, and you can contact us if you want information about a specific configuration.

The Raspberry Pi site contains clear documentation about the product itself. Installing an operating system for the Raspberry Pi is described at https://www.raspberrypi.org/documentation/installation/installing-images/README.md.

The following documentation has been tested to work with Raspberry Pi OS (32-bit) with desktop and recommended software, an image based on Debian Buster, that can be downloaded from https://www.raspberrypi.org/downloads/raspberry-pi-os/. However, it is highly recommended to use the Raspberry Pi Imager (available for Windows, Mac OS and Linux), to download and install the image to the SDCard.

Once the image is installed, the first boot will be on X11, and a configuration dialog will show up. Follow the instructions to configure your language, keyboard settings and WiFi. Apply the changes and reboot.

Once you boot again, from the top menu, select Preferences → Raspberry Pi Configuration

  • From the System tab, select Boot to CLI. Note that the hostname is set to raspberrypi (change it if needed).

  • From Interfaces tab, enable SSH.

  • From Performance tab, it is recommended to set 512 MB for GPU Memory, depending of course on how graphics-intensive versus how cpu-intensive your application is.

Click OK and reboot.

Once the Pi is back online, it is convenient to check the SSH access from your machine:

ssh pi@raspberrrypi.local

If you encounter any issues, check these instructions.

5.6.2. Java

The Raspberry Pi OS already contains an up-to-date JDK 11 distribution. You can verify that by running java -version:

pi@raspberrypi:~ $ java -version
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment (build 11.0.9+11-post-Raspbian-1deb10u1)
OpenJDK Server VM (build 11.0.9+11-post-Raspbian-1deb10u1, mixed mode)

5.6.3. JavaFX

Install JavaFX

JavaFX 16 Early Access (16-ea) builds with support for DRM on Raspberry Pi 4 can be downloaded from https://gluonhq.com/products/javafx/#ea

Browse for JavaFX Linux/arm32-drm SDK (for Raspberry Pi 4), download the SDK to your machine and then copy it to the Pi, like:

scp ~/Downloads/openjfx-16-ea+4_linux-arm32-drm-sdk.zip  pi@raspberrypi.local:/home/pi/Downloads

Now back to your Pi (via SSH or directly), move the SDK to /opt and unzip it:

pi@raspberrypi:~ $ sudo mv ~/Downloads/openjfx-16-ea+4_linux-arm32-drm-sdk.zip /opt
$ cd /opt
$ sudo unzip openjfx-16-ea+4_linux-arm32-drm-sdk.zip
$ sudo rm openjfx-16-ea+4_linux-arm32-drm-sdk.zip

Note that you should have the SDK under the folder /opt/arm32fb-sdk.

Support for DRM is a commercial extension from Gluon.

You can enable it by setting the environment variable ENABLE_GLUON_COMMERCIAL_EXTENSIONS, either if your application is non-commercial or if you obtained a valid license from Gluon.

To enable it on your current session, run:

pi@raspberrypi:~ $ export ENABLE_GLUON_COMMERCIAL_EXTENSIONS=true

And if you want to export it permanently, you can add the following line to your .bashrc file:

ENABLE_GLUON_COMMERCIAL_EXTENSIONS=true
Testing JavaFX
JavaFX on embedded works with X11 as well, but first we want to demonstrate it works using DRM driver to manage the framebuffer, without X.

Create a JavaFX sample, or clone the OpenJFX samples.

Compile:

pi@raspberrypi:~ $ cd ~/Downloads/samples/CommandLine/Modular/CLI/hellofx
$ javac --module-path /opt/arm32fb-sdk/lib --add-modules=javafx.controls src/hellofx/HelloFX.java -d dist

And finally run:

pi@raspberrypi:~ $ sudo -E java -Dmonocle.platform=EGL -Djava.library.path=/opt/arm32hfb-sdk/lib -Dmonocle.egl.lib=/opt/arm32fb-sdk/lib/libgluon_drm.so --module-path /opt/arm32fb-sdk/lib --add-modules javafx.controls -cp dist/. hellofx.HelloFX

You should see on your HDMI display something like:

HelloFX app

From your SSH session, you can click Ctrl+C to finish the process.

X11

If you are running with X11, the command line should be instead:

pi@raspberrypi:~ $ sudo -E java -Dglass.platform=gtk --module-path /opt/arm32fb-sdk/lib --add-modules javafx.controls -cp dist/. hellofx.HelloFX
HelloFX app on X11

5.6.4. Run demo app

As a very first demo, we will show how you can run the MaryHadALittleLambda application on a recent Pi, with Java 11 and JavaFX 16-ea.

This application was created in 2014, in order to demonstrate the use of lambda’s. The author, Stephen Chin, wrote a blogpost about this. The application was demonstrated on embedded devices as well.

The original code of the application is at https://github.com/steveonjava/MaryHadALittleLambda. We forked it and made some minor modifications in order to make it work with the JavaFX 11+ and Java 11+ APIs. The new code is at https://github.com/gluonhq/MaryHadALittleLambda.

It is important to mention that you would typically do all your development on your development system (desktop or laptop). You can use your IDE and common tools to create, compile and test applications. Once you are happy with the result, you can transfer your class files over to the embedded device, and run them there.

Download code

The first step is to get the code to your development system and to compile the application.

it is highly recommended doing this on your desktop/laptop, and not on your Raspberry Pi, as it requires much more resources than typically available on embedded devices. However, the Raspberry Pi 4 is a much more powerful device than its predecesors, and it can run the same desktop tasks.
git clone https://github.com/gluonhq/MaryHadALittleLambda.git
cd MaryHadALittleLambda
mvn clean compile

It is good practice to test your application on desktop or laptop before transfering the class files to your device. You can run the application on desktop using:

mvn javafx:run

You should see the application running now.

Sample on desktop

Quit the application, and check if the class files are in the target/classes directory. There should be about 3 class files in the sample package.

5.6.5. Install the class files on the device

You can now copy the classes from your development system to the Raspberry Pi, e.g.

cd target
scp -r classes pi@raspberrypi.local:/home/pi/Downloads
Run the application on the device

Now, log in via ssh to your Raspberry Pi and run the application.

cd ~/Downloads
sudo -E java -Dmonocle.platform=EGL -Djava.library.path=/opt/arm32hfb-sdk/lib -Dmonocle.egl.lib=/opt/arm32fb-sdk/lib/libgluon_drm.so --module-path /opt/arm32fb-sdk/lib --add-modules javafx.controls -cp classes/ sample.Main
X11

If you use X11, then the demo can be run with:

cd ~/Downloads
sudo -E java -Dglass.platform=gtk --module-path /opt/arm32fb-sdk/lib --add-modules javafx.controls -cp classes/ sample.Main

6. User Interface (UI)

Glisten is the UI component of Gluon which offers cross platform behavior, but a platform specific look and feel. Based on Material Design Specification: Glisten comes with a set of cross platform JavaFX controls.

6.1. Glisten APIs

6.1.1. MobileApplication

MobileApplication class extends from Application, hence for Glisten-based applications it will be considered the base class.

Let’s have a look at the hierarchy of components added to generate the user interface.

GlassPane

GlassPane is the root node added automatically by Glisten to the primary scene.

It can be seen as the invisible container for all the nodes that will be added: views, layers, dialogs or toolbars.

Usually, there won’t be any need to access this container directly. But it can be retrieved with MobileApplication.getInstance().getGlassPane(), if needed.

The first children of the GlassPane container will always be a View and the AppBar.

MobileApp

The nodes on top will be layers (like a side menu), popups or dialogs.

In between, there is a semi-transparent node that will ensure that the node on top appears distinct from the content beneath it, whenever this is shown. Otherwise it won’t be visible.

MobileAppLayer
AppBar

AppBar is a special node acting as a toolbar for branding, navigation, search and other actions.

It is placed at the top of the layout and is generally made up of some buttons (nav icon and action items), a title and a menu.

The content (in terms of nodes and their respective actions) will be managed directly by each of the different views, so the AppBar will be like a toolbar placeholder for each one of them.

Typically, the developer sets up the AppBar for a given View by overriding its updateAppBar method:

public class HomeView extends View {

    public HomeView() {
        FloatingActionButton fab = new FloatingActionButton();
        fab.showOn(this);
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("nav icon")));
        appBar.setTitleText("The AppBar");
        appBar.getActionItems().addAll(
            MaterialDesignIcon.SEARCH.button(e -> System.out.println("search")),
            MaterialDesignIcon.FAVORITE.button(e -> System.out.println("fav")));
        appBar.getMenuItems().addAll(new MenuItem("Settings"));
    }
}
TheAppBar

If the developer doesn’t require the AppBar for a given view, it can be hidden by setting its visibility to false: appBar.setVisible(false).

Another way to update the AppBar will be adding a listener to each view’s showingProperty:

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> {
            View view = new View(new Label("Hello Glisten!"));
            view.showingProperty().addListener((obs, oldValue, newValue) -> {
                if (newValue) {
                    AppBar appBar = MobileApplication.getInstance().getAppBar();
                    appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("nav icon")));
                }
            });
            return view;
        });
    }
}
TheAppBar2
View

View is a Glisten container that will allow adding nodes in its top, left, right, bottom, and center positions.

Usually a View instance is created by providing a node for its content. This instance is added to a factory of views with a name, so the Glisten UI can load and unload them on demand.

Home View

By default, the home view will be the first view displayed when the stage is shown.

It has no predefined content, so this view has to be designed by the developer, but its name is already assigned: MobileApplication.HOME_VIEW.

This short snippet will create a very simple mobile application with a single view:

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new View(new Label("Hello Glisten!")));
    }

}
FirstView

6.1.2. How it works

In order to understand how Glisten works, let’s have a look at the initial JavaFX application lifecycle. Whenever an application is launched, the JavaFX runtime does the following:

  • Constructs an instance of the specified Application class

  • Calls the init() method

  • Calls the start(javafx.stage.Stage) method

Typically, the developer of a Glisten application will only override the init() method, in order to add one or more View objects, and provide them as factories that can be called on-demand. The MobileApplication implementation will take care of the rest: when start() is called, it will create a Scene, adding a root node to it. Finally, it will put the scene in the primary stage and show it.

When the runtime calls init(), we just provide a Supplier<View> for HOME_VIEW, but the view is not instantiated at this point yet.

Then start(Stage) is called. At this moment, MobileApplication creates an instance of Scene, sets the root an empty instance of a GlassPane and adds the scene to the primary stage.

After some internal settings, like adding the default Swatch, it calls switchView("HOME_VIEW"). This is the moment when an instance of the home view is created and added to the pane.

Finally, the stage is shown.

Changing the default Swatch

What if we want to modify some scene settings before it is shown?

Before showing the stage, there’s a call to the postInit(Scene) method, that can be used by the developer for one time initialization.

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new View(new CheckBox("I like Glisten")));
    }

    @Override
    public void postInit(Scene scene) {
        Swatch.GREEN.assignTo(scene);
    }

}
FirstViewGreen

In case the developer wants to change the swatch for each view, this can be done by listening to the changes in the viewProperty of a MobileApplication instance, and assigning the corresponding Swatch:

public class MyApp extends MobileApplication {

    private static final String OTHER_VIEW = "other";

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new View(new Label("Hello Glisten!")) {
            {
                FloatingActionButton fab = new FloatingActionButton(MaterialDesignIcon.ADD_BOX.text, e -> switchView(OTHER_VIEW));
                fab.showOn(this);
            }
        });
        addViewFactory(OTHER_VIEW, () -> new View(new CheckBox("I like Glisten")) {
            {
                FloatingActionButton fab = new FloatingActionButton(MaterialDesignIcon.ADD_CIRCLE_OUTLINE.text, e -> switchView(HOME_VIEW));
                fab.showOn(this);
            }
        });

        viewProperty().addListener((obs, oldView, newView) -> {
            switch(newView.getName()) {
                case HOME_VIEW:
                    Swatch.INDIGO.assignTo(newView.getScene());
                    break;
                case OTHER_VIEW:
                    Swatch.GREEN.assignTo(newView.getScene());
                    break;
                default:
                    Swatch.getDefault().assignTo(newView.getScene());
            }
        });
    }
}

Another way to accomplish the same will be by adding a listener to each view’s showingProperty:

public class MyApp extends MobileApplication {

    private static final String OTHER_VIEW = "other";

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> {
            View view = new View(new RadioButton("Glisten style"));
            FloatingActionButton fab = new FloatingActionButton(MaterialDesignIcon.ADD_BOX.text, e -> switchView(OTHER_VIEW));
            fab.showOn(view);
            view.showingProperty().addListener((obs, oldValue, newValue) -> {
                if (newValue) {
                    Swatch.RED.assignTo(view.getScene());
                }
            });
            return view;
        });
        addViewFactory(OTHER_VIEW, () -> {
            View view = new View(new CheckBox("I like Glisten"));
            FloatingActionButton fab = new FloatingActionButton(MaterialDesignIcon.ADD_CIRCLE_OUTLINE.text, e -> switchView(HOME_VIEW));
            fab.showOn(view);
            view.showingProperty().addListener((obs, oldValue, newValue) -> {
                if (newValue) {
                    Swatch.TEAL.assignTo(view.getScene());
                }
            });
            return view;
        });
    }
}
FirstViewRed
Creating a View

As we have already seen in the previous code snippets, there are several possible ways to create a View.

Extending View

A custom view can be created by extending View. This allows for more complex views, for adding a view transition, customizing the AppBar…​

public class MyApp extends MobileApplication {

    private static final String OTHER_VIEW = "other";

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new HomeView());
        addViewFactory(OTHER_VIEW, () -> new View(new CheckBox("I like Glisten")));
    }

    @Override
    public void postInit(Scene scene) {
        Swatch.LIGHT_GREEN.assignTo(scene);
    }

    class HomeView extends View {

        public HomeView() {
            setCenter(new Label("Hello Glisten!"));
        }

        @Override
        protected void updateAppBar(AppBar appBar) {
            appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> switchView(OTHER_VIEW)));
            appBar.setTitleText("Home View");
        }
    }
}
HomeView
Adding content

A View can be created by using one of its constructors, where the content is set. Any Node will work as content for the View.

public class MyApp extends MobileApplication {

    private static final String OTHER_VIEW = "other";

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new View(new Home()) {
            @Override protected void updateAppBar(AppBar appBar) {
                appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> switchView(OTHER_VIEW)));
                appBar.setTitleText("Home View");
            }
        });
    }

    class Home extends VBox {

        public Home() {
            setSpacing(30);
            setAlignment(Pos.CENTER);
            getChildren().addAll(new Label("Hello Glisten!"), new Button("Click me"));
        }
    }
}
VBoxView
Creating Views with FXML

View can be used within FXML, by adding the proper import.

<?xml version="1.0" encoding="UTF-8"?>

<?import com.gluonhq.charm.glisten.mvc.View?>
<?import javafx.scene.control.Button?>

<View fx:id="home" prefHeight="400.0" prefWidth="600.0" styleClass="mainFxmlClass" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
   <center>
      <Button fx:id="button" onAction="#onClick" text="Click me!" />
   </center>
</View>

Load the fxml file using the FXMLLoader. Assuming the home.fxml file is under the same package as the application, but in the 'resources' directory:

public class MyApp extends MobileApplication {

    private static final String OTHER_VIEW = "other";

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> {
            try {
                return (View)FXMLLoader.load(getClass().getResource("views/home_1.fxml"));
            } catch (Exception ex) { }
            return null;
        });
        addViewFactory(OTHER_VIEW, () -> new View(new CheckBox("I like Glisten")));
        viewProperty().addListener((obs, ov, nv) -> {
            AppBar appBar = MobileApplication.getInstance().getAppBar();
            switch(nv.getName()) {
                case HOME_VIEW:
                    appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> switchView(OTHER_VIEW)));
                    appBar.setTitleText("Home View");
                    Swatch.TEAL.assignTo(appBar.getScene());
                    break;
                case OTHER_VIEW:
                    appBar.setVisible(false);
                    break;
            }
        });
    }
}
FXMLView
Using Gluon’s Scene Builder

View is available under the Gluon titled pane on the top left of Scene Builder.

SceneBuilder

Note: Prior to Scene Builder 8.3.0, charm-glisten-6.0.5.jar should be imported into the Scene Builder.

Afterburner framework

Adding the dependency to the build.gradle script, allows using this MVP framework from Adam Bien.

dependencies {
    compile 'com.airhacks:afterburner.mfx:1.6.3'
}

For every view, these files are required:

In the code folder:

  • The view i.e. HomeView. An empty class that just extends FXMLView.

  • The presenter. I.e. HomePresenter.

  • A service (optional), i.e. HomeService.

And in the resources folder:

  • The fxml, i.e. home.fxml

  • The css (optional), i.e. home.css.

In the presenter, controls can be added to the view, like a Button to trigger some action when the user clicks on it.

A transition can be set for switching views from a large list of available transitions (see the Charm JavaDoc). In case no transition is desired, the developer can select NoTransition().

<View fx:id="home" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1" fx:controller="HomePresenter">
    <center>
        <StackPane>
            <Button text="Click!" onAction="#onClick"/>
        </StackPane>
    </center>
</View>

The presenter can be created to directly interact with the nodes in the FXML.

public class HomePresenter implements Initializable {

    @FXML
    private View home;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        FloatingActionButton fab = new FloatingActionButton();
        fab.setOnAction(e -> {
            System.out.println("FAB click");
        });
        fab.showOn(home);

        home.setShowTransitionFactory(v -> new FadeInLeftBigTransition(v));

        home.showingProperty().addListener((obs, ov, nv) -> {
            if (nv) {
                final AppBar appBar = MobileApplication.getInstance().getAppBar();
                appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("menu")));
                appBar.setTitleText("The Home View");
                appBar.getActionItems().addAll(
                        MaterialDesignIcon.SEARCH.button(),
                        MaterialDesignIcon.FAVORITE.button());
                appBar.getMenuItems().addAll(new MenuItem("Settings"));

                Swatch.PURPLE.assignTo(home.getScene());
            }
        });
    }

    @FXML
    public void onClick() {
       System.out.println("click");
    }
}

Then, a view can be created:

public class HomeView extends FXMLView {

}

This View can be loaded in a MobileApplication:

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> {
            final HomeView homeView = new HomeView();
            return (View) homeView.getView();
        });
    }
}
Afterburner

In order to perform some actions while the transition is running or when it finishes, a custom ActionEvent can be provided to the end of the transition.

This can be done as well based on the LifecycleEvent events. The following code snippet will set the view transparent to mouse clicks just during the time it is added to the scene and the transition ends.

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> {
            final HomeView homeView = new HomeView();
            final View view = (View) homeView.getView();
            view.addEventHandler(LifecycleEvent.SHOWING, e ->
                view.setMouseTransparent(true));
            view.addEventHandler(LifecycleEvent.SHOWN, e ->
                view.setMouseTransparent(false));
            return view;
        });
    }
}

6.1.3. Layers

A Layer is an overlay that can be shown above any View.

Layers can be provided as factories by using MobileApplication.getInstance().addLayerFactory(), and can be shown on demand by using MobileApplication.getInstance().showLayer().

public class MyApp extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new View(new Label("Hello Glisten!")) {
            @Override
            protected void updateAppBar(AppBar appBar) {
                appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> showLayer("New Layer")));
            }
        });

        addLayerFactory("New Layer", () -> {
            final StackPane stackPane = new StackPane(new Button("Click"));
            stackPane.setStyle("-fx-background-color: white; -fx-padding: 10;");
            SidePopupView sidePopupView = new SidePopupView(stackPane);
            return sidePopupView;
        });
    }
}

Layers can also be be shown and hidden, without the use of factories, by calling show() and hide() respectively.

SidePopupView sidePopupView = new SidePopupView(stackPane);
sidePopupView.show();

By calling setBackgroundFade(double) the developer will be able to fade the background of a layer to a darker color, obscuring the main application and drawing focus to the popup.

Layer1
Creating a Layer

Developers can create custom Layer nodes. This is a minimal implementation, including a faded background when the layer is shown.

public class MyApp extends MobileApplication {

    @Override
    public void init() {

        addViewFactory(HOME_VIEW, () -> new View(new Label("Hello Glisten!")) {
            @Override
            protected void updateAppBar(AppBar appBar) {
                appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> showLayer("New Layer")));
            }
        });

        addLayerFactory("New Layer", () -> new Layer() {
            private final Node root;
            private final double size = 150;
            final GlassPane glassPane = MobileApplication.getInstance().getGlassPane();

            {
                setBackgroundFade(0.5);
                root = new StackPane(new Button("A custom layer"));
                root.setStyle("-fx-background-color: white;");
                getChildren().add(root);
            }

            @Override
            public void layoutChildren() {
                super.layoutChildren();
                root.setVisible(isShowing());
                if (!isShowing()) {
                    return;
                }
                root.resize(size, size);
                resizeRelocate((glassPane.getWidth() - size)/2, (glassPane.getHeight()- size)/2, size, size);
            }
        });
    }
}

Layers can also be created and shown without using the factory methods.

public class MyApp extends MobileApplication {

    private Layer layer;

    @Override
    public void init() {

        addViewFactory(HOME_VIEW, () -> new View(new Label("Hello Glisten!")) {
            @Override
            protected void updateAppBar(AppBar appBar) {
                appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> {
                    if (layer == null) {
                        layer = createLayer();
                    }
                    layer.show();
                }));
            }
        });
    }

    private Layer createLayer() {
        return new Layer() {
            private final Node root;
            private final double size = 150;
            final GlassPane glassPane = MobileApplication.getInstance().getGlassPane();

            {
                setBackgroundFade(0.5);
                root = new StackPane(new Button("A custom layer"));
                root.setStyle("-fx-background-color: white;");
                getChildren().add(root);
            }

            @Override
            public void layoutChildren() {
                super.layoutChildren();
                root.setVisible(isShowing());
                if (!isShowing()) {
                    return;
                }
                root.resize(size, size);
                resizeRelocate((glassPane.getWidth() - size)/2, (glassPane.getHeight()- size)/2, size, size);
            }
        };
    }
}
CustomLayer

However, Glisten already provides a series of built-in layers, so creating custom layers won’t be necessary in most of the occasions.

Using built-in layers

Glisten uses layers to show additional information. The most commonly used layers are:

  • MenuPopupView

  • MenuSidePopupView

  • PopupView

  • SidePopupView

For a description of each layer, check the API documentation: Glisten layers.

Many Glisten controls use layers internally, including FloatingActionButton, NavigationDrawer, Snackbar and Toast.

6.1.4. UI Controls

Glisten comes with a collection of cross platform UI controls based on the Material Design Specification: Glisten Controls.

6.2. Glisten CSS Overview

An overview of the CSS style classes and properties that are available to users of the Gluon Charm Glisten library. Users of Glisten CSS should be familiar with the JavaFX CSS functionality.

6.2.1. Glisten Themes

Glisten ships with two CSS themes: light and dark. Despite the connotation, the amount of CSS required to specify these themes is incredibly minimal - and as a courtesy is reproduced here:

Light Theme
.root {
  -fx-background: white;
  -fx-text-fill: -text-light;
  -fx-prompt-text-fill: rgba(0,0,0,.3);

  -text-disabled: -text-disabled-light;
  -background-disabled:rgba(0,0,0,.12);
  -background-fill: -text-light;

  -flat-button-hover-background: rgba(#999999,.2);
  -flat-button-pressed-background: rgba(#999999,.4);
  -flat-button-disabled-fill: rgba(#000000,.26);
}
Dark Theme
.root {
  -fx-background: #333333;
  -fx-text-fill: -text-dark;
  -fx-prompt-text-fill: rgba(255,255,255,.3);

  -text-disabled: -text-disabled-dark;
  -background-disabled:rgba(255,255,255,.12);
  -background-fill: -text-dark;

  -flat-button-hover-background: rgba(#CCCCCC,.15);
  -flat-button-pressed-background: rgba(#CCCCCC,.25);
  -flat-button-disabled-fill: rgba(#FFFFFF,.3);
}

6.2.2. Glisten CSS Properties

There are a number of global properties that can be used by any application that uses Glisten. These properties allow for custom controls to be styled in a way that should relatively closely match up with the Glisten-styled controls. What follows is a list of such properties.

Swatches

Perhaps most importantly, Glisten supports swatches, where each swatches populates a series of pre-specified CSS properties which can be used within your own CSS. The properties that are populated for each swatch are the following:

-primary-swatch-50
-primary-swatch-100
-primary-swatch-200
-primary-swatch-300
-primary-swatch-400
-primary-swatch-500
-primary-swatch-600
-primary-swatch-700
-primary-swatch-800
-primary-swatch-900
-alternate-swatch-100
-alternate-swatch-200
-alternate-swatch-400
-alternate-swatch-700

Each of these properties is simply a color along a certain color spectrum. All swatches provided by Glisten are based on the Material Design color style guide. The Glisten CSS files do not typically make use of all swatch colors, so it is also advised that any use of these colors be restrained, outside of the -primary-swatch-500, and possibly -primary-swatch-200, -primary-swatch-600, and -primary-swatch-700.

An example of how such a property could be used is shown in the sample code below:

.button {
  -fx-background-color: -primary-swatch-500;
}

.button:hover {
  -fx-background-color:-primary-swatch-600;
}
Other Properties

There are a number of other properties that are available to use. Some of these are custom Glisten properties, but many are standard JavaFX CSS properties that Glisten simply modifies to the appropriate value. Some notable properties include the following (refer to the JavaFX CSS Reference Guide for more details of any that start with -fx-) :

  • -text-light: This is a light-colored text, best used on a dark background.

  • -text-dark: This is a dark-colored text, best used on a light background.

  • -fx-text-fill: This is populated based on the theme that is currently set (i.e. light or dark).

  • -text-disabled: This is used in controls where the control is disabled.

Text Fill

It is important to understand how text fill works in Glisten, as the underlying JavaFX CSS engine is significantly more powerful than normal CSS engines. In particular, when it comes to deciding what color text fill to use, you can use JavaFX CSS laddering to decide. In other words, it is never a good idea to use -fx-text-fill directly, as -fx-text-fill does not take into account the background color behind the text (so it is possible that the text is unreadable).

In cases where a custom text fill is desired, it is much better to do something such as the following:

.button {
  -fx-background-color: -primary-swatch-500;
  -fx-text-fill: ladder(-primary-swatch-500, -text-dark 49%, -text-light 50%)
}

This code states to use -text-dark or -text-light, depending on the darkness of the first value, in this case -primary-swatch-500. The result of this is that when -primary-swatch-500 is a very light color (such as a bright yellow), the -fx-text-fill will be -text-light (and therefore appear as a dark color). When -primary-swatch-500 is a very dark color, -text-dark is used, and is therefore a light color that is readable on a dark background.

6.2.3. Glisten Style Classes

By default, when a UI control is instantiated in Glisten, the control does not receive any additional style classes over what is provided by the underlying JavaFX technology. However, depending on the use case for each individual control, it can make sense to apply additional style classes to have Glisten apply additional styling to these controls.

There exists a class called GlistenStyleClasses that is located in the com.gluonhq.glisten.visual package. This class provides a number of predefined style classes that can be applied to a control, via one of two methods:

1. JavaFX StyleClass List API:
final ToggleButton toggleVisibilityButton = new ToggleButton("Toggle Visibility");

/*
 * TOGGLE_BUTTON_SWITCH is a static import from GlistenStyleClasses.
 * It is simply a String consisting of 'switch'.
 */
toggleVisibilityButton.getStyleClass.add(TOGGLE_BUTTON_SWITCH);
2. GlistenStyleClasses Convenience API:
final ToggleButton toggleVisibilityButton = new ToggleButton("Toggle Visibility");

/*
 * Both applyStyleClass and TOGGLE_BUTTON_SWITCH are static
 * imports from GlistenStyleClasses
 */
applyStyleClass(toggleVisibilityButton, TOGGLE_BUTTON_SWITCH);

Both approaches are more or less equivalent, but the second approach is recommended.

Once these additional style classes have been applied, both Glisten CSS and your own CSS can be applied as applicable. For example, a button that has been styled to be flat (using GlistenStyleClasses.BUTTON_FLAT) will be able to receive alternate styling via css such as the following:

.button.flat {
  ...
}

Of course, as noted, any styling of UI controls based on style classes from the GlistenStyleClasses class will be styled differently by Glisten CSS, so there is no need (unless desired) to apply additional styling.

What follows is a list of additional CSS style classes that are available to some UI controls:

Button

Buttons have the CSS style class .button. There are the following additional style classes that are available in Glisten:

Style Class

GlistenStyleClasses Property

Description

flat

BUTTON_FLAT

The flat style class results in a button that is represented with a visually 'flat' style

round

BUTTON_ROUND

The round style class causes the button to be rounded (for example, think of the commonly-used 'floating action button' that is present in the Material Design documentation).

Toggle Button

Toggle buttons have the CSS style class .toggle-button.

Style Class

GlistenStyleClasses Property

Description

switch

TOGGLE_BUTTON_SWITCH

The switch style class results in toggle button that matches the Material Design Switch representation.

6.2.4. Other Questions

If this document does not answer all your questions regarding Glisten CSS functionality, please reach out to Gluon staff and ask any questions. They will help us to improve future versions of this document.

6.3. Release Notes

This page provides information about the releases of Charm Glisten, including known issues, limitations, and general advisories. Please review the notes before using the library.

To discuss issues or ideas with other developers, please reach out to us.

6.0.0

Glisten 6.0.0 comes with JavaFX 11 support.

5.0.0

Glisten 5.0.0 release has a significant number of breaking changes. For a detailed document on how to migrate to 5.0.0, please refer Migration Guide to Gluon Mobile 5.

New Features
  • MobileApplication exposes a new method getDrawer() which should be used to access the application wide NavigationDrawer

  • New control Snackbar replaces SnackbarPopupView

  • MobileTransition has been added to pause and resume animation along with the change in the life-cycle of the mobile application

  • View has defined center as the default property, which enables setting a center child in fxml without using the <center> tag

  • Layer is managed using life-cycle. They are four life-cycle events: showing, shown, hiding, hidden.

  • Layers can now be shown on MobileApplication without adding them to GlassPane or View.

  • NavigationDrawer has been improved to automatically show and hide without the need to add it to a SidePopupView.

  • NavigationDrawer closes automatically when an item is selected

  • Support for notch in iPhone X devices.

Package Changes
  • FAB has been moved from com.gluonhq.charm.glisten.layout.layer to com.gluonhq.charm.glisten.control

API changes
  • MobileLayoutPane has been removed

  • GlassPane and View extend from BorderPane instead of MobileLayoutPane

  • GlassPane and View no longer expose the list of layers. The method getLayers() has been removed

  • View no longer exposes a name property

  • View’s show transition factory has been changed from Function<View,Transition> to Function<View,MobileTransition>

  • Layers can be shown using the new API. In order to show and hide a layer, call show() and hide() respectively

  • ActionItems in BottomNavigation only accept instances of BottomNavigationButton instead of Node

  • Deprecated method createButton() has been removed from BottomNavigation

  • New constructor has been added to most of the Transitions to help developers define custom duration

  • PopupView gives more control to the developer by allowing them to control the side and padding of the popup. Additionally, height and width of the popup can now be changed using the pref*, min* and max* properties.

  • PopupView has dropped autofix property. AutoFix is now set to true for all usage of PopupView.

  • AutoCompleteTextField has a new converter for easy conversions between strings and objects.

  • FAB can bind to a View using showOn(View view)

  • New methods have been added to NavigationDrawer to open and close the drawer - open() and close()

Bug Fixes
  • MenuPopupView and AutoCompletePopup support text wrapping

  • AutoCompleteTextField selection of a node from the popup, converts the selected object to a string and shows it in the TextField.

  • Animation duration for PopupView and SidePopupView has been reduced to match Material Design specification

  • Floating text translates to the top when TextField has text, irrespective of state of focus property

  • Nag window no longer throws exception when no home view has been defined

  • ToggleButtonGroup minWidth should never outgrow prefWidth

  • SettingsPane is updated after any change in the options list

  • Layer has been updated to fix multiple issues

  • Javadoc has been updated to fix multiple issues

7. Device Interface

Open Source

Attach is open source, and licensed under the GPL license. Its source code is hosted under Gluon organization in Github.

Gluon Attach addresses the integration with low-level platform APIs. Using Attach, you write code that accesses device and hardware features using a uniform, platform-independent API. At runtime, the appropriate implementation (desktop, android, ios) makes sure the platform specific code is used to deliver the functionality.

The following sections provide a high-level overview of the current Attach features.

More information can be found in the Gluon website, and you can find different samples using different Attach services.

This library is being actively extended, so if you think an important feature is missing, feel free to log an issue or contribute a pull request at our Attach repository.

Attach project is split up into different services. Each service takes care of implementing a certain hardware or device feature, like access to general device information, information about the display, outputs of different sensors that are found on the device, etc.

The full list of services can be found here.

7.1. Using Attach

7.1.1. Prerequisites

Attach prerequisites are in line with those required by each Platform for developing Gluon application.

7.1.2. Adding Attach to the project

Configuring your project to include an Attach service is done via adding the dependency to the project and the name of the service to attachList configuration.

When creating a project with the Gluon IDE plugin, the attachList is already available and pre-configured with the following four services: display, lifecycle, statusbar and storage.

The list of services can be extended with any of the available services.

For instance, the Position service, that allows accessing the device’s GPS, can be added as follows:

<dependency>
    <groupId>com.gluonhq.attach</groupId>
    <artifactId>position</artifactId>
    <version>${attach.version}</version>
</dependency>
...
<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>${client.maven.plugin.version}</version>
    <configuration>
        <target>${client.target}</target>
        <attachList>
            ...
            <list>position</list>
        </attachList>
        <mainClass>${main.class}</mainClass>
    </configuration>
</plugin>

7.1.3. Using Services

Each Attach service provides the functionality to access a given feature in every platform where it is supported.

Each service can be accessed via its create() method, which returns an Optional which contains an instance of the requested service.

The returned optional object might be empty if the runtime platform has no available implementation for that specific service.

PositionService positionService = PositionService.create().orElseThrow(() -> new RuntimeException("PositionService not available."));
positionService.positionProperty().addListener((obs, ov, nv) -> {
    System.out.println("Latest known GPS coordinates from device: " + nv.getLatitude() + ", " + nv.getLongitude());
});
positionService.start();

Another useful class is the enum com.gluonhq.attach.util.Platform, which provides an easy way to detect what platform the application is currently running on: ANDROID, IOS or DESKTOP.

7.2. Services Overview

For an overview of all the available services, we refer you to the services package overview page in the Attach javadoc:

7.2.1. Service Requirements

Each service has certain requirements that need to be addressed before deploying the mobile applications, either on Android, or on iOS, or both.

Attach javadoc provides detailed information on these cases.

Android

For instance, the Dialer service requires a CALL_PHONE permission included in the Android manifest file:

<manifest ...>
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    ...
</manifest>

The AndroidManifest.xml file is generated for the project with the client:package goal, and it is available at target/client/aarch64-android/gensrc/android.

The Client plugin takes care of most of these modifications, and usually, there is no need to modify this manifest.

Only in case you need to make any change, copy this file to src/android and make any modification that might be needed. For every new run of client:package, the manifest found at src/android will be used.

iOS

For instance, the Position service requires a few keys included in the Default-Info.plist file:

<plist ...>
<dict>
    ...
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>This app needs location services</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>This app needs location services</string>
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>This app needs bluetooth services</string>
</dict>
</plist>

The Default-Info.plist file is generated for the project with the client:link goal, and it is available at target/client/arm64-ios/gensrc/ios.

In case you need to make any change, copy this file to src/ios and make any modification that might be needed. For every new run of client:link, the plist found at src/ios will be used.

Learn more about Gluon Attach: Gluon Attach Documentation.

7.3. Building Attach

The following sections are intended only for developers who want to create a native service or understand how the Attach native services are created.

If you want to build Attach from code, you need JDK 11+, Xcode 11+, the Android SDK 29 and the Android NDK.

Clone the repository, and from Mac OS X, run:

export ANDROID_SDK=$ANDROID_HOME
export ANDROID_NDK=$ANDROID_HOME/ndk-bundle
./gradlew clean build publishToMavenLocal

It will build all services for all possible platforms (desktop, iOS and Android), and it will install them to your .m2 local repository.

You can try to build from Linux or Windows, however the iOS implementation won’t be built.

Single service builds can be done as well. For instance, to build and install locally the Display service, run:

./gradlew :display:publishToMavenLocal

7.3.1. Platform builds

The tasks under mavenPublish.gradle and native-build.gradle perform the native build for desktop, iOS and Android.

The goal is to produce an artifact that can be used as dependency by the Client plugin when creating a native image that will be deployed to the target platform.

Such artifact (a jar file), bundles Java classes, configuration files and native libraries.

The builds are done with the nativeBuild task. For instance, to build the native libraries for the three platforms, run:

./gradlew :display:nativeBuild

or in case you want to build a given platform, run:

./gradlew :display:iosBuild
Desktop

For a given service, its Java code (GraalVM) is compiled against JDK 11+, and with the desktopJar task it is added to the ${service}-${version}-desktop.jar file.

Reflection and jni configuration json files are added under META-INF/substrate/config (see config files).

Native code, if present, will be compiled, and the native library will be added to the jar to a native folder.

iOS

For a given service, its Java code (GraalVM) is compiled against JDK 11+, and with the iosJar task it is added to the ${service}-${version}-ios.jar file.

Reflection and jni configuration json files are added under META-INF/substrate/config (see config files).

Native code using Objective-C and found for each service under:

def osSources = []
osSources = "$projectDir/src/main/native/ios/${name}.m"

is compiled and linked, using the iOS SDK with certain flags and iOS frameworks, to create a native library under:

def linkerOutput = "$buildDir/native/ios/$arch/lib${name}.a"

The native library is then added to the jar to a native folder.

Android

For a given service, its Java code (GraalVM) is compiled against JDK 11+, and with the androidJar task it is added to the ${service}-${version}-android.jar file.

Reflection and jni configuration json files are added under META-INF/substrate/config (see config files).

Native code using C and found for each service under:

def nativeSourcesDir = []
nativeSourcesDir = "$projectDir/src/main/native/android/c/*.c"

is compiled and linked, using the Android SDK with certain flags, to create a native library under:

def linkerOutput = "$buildDir/native/android/lib${name}.a"

The native library is then added to the jar to a native folder.

Finally, Java code for Android (Dalvik) is compiled against JDK 1.7, using the Android SDK. An Android library project is generated and bundled under META-INF/substrate/dalvik/${name}.aar. It will contain mainly a jar with classes and an AndroidManifest.xml file with the service requirements (like permissions or activities).

7.3.2. Attach with Client plugin

An Attach service is added to a project as regular dependency but it has also to be included in the attachList:

<!-- dependencies -->
<dependency>
    <groupId>com.gluonhq.attach</groupId>
    <artifactId>display</artifactId>
    <version>4.0.9</version>
</dependency>

<!-- plugin -->
<configuration>
    <attachList>
        <list>display</list>
    </attachList>
</configuration>

If needed, the platform classifier can be used to run the project on HotSpot with the JavaFX Maven plugin:

<!-- dependencies -->
<dependency>
    <groupId>com.gluonhq.attach</groupId>
    <artifactId>display</artifactId>
    <version>4.0.9</version>
</dependency>
<dependency>
    <groupId>com.gluonhq.attach</groupId>
    <artifactId>display</artifactId>
    <version>4.0.9</version>
    <classifier>desktop</classifier>
    <scope>runtime</scope>
</dependency>

This can be done with the Maven profiles, so these platform dependencies are only activated for the correct profile.

When running the Client plugin goals, for each Attach service, the plugin will apply the correct classifier based on the target and resolve the artifact.

Then, for every ${service}-${version}-${platform}.jar file found in the classpath, the plugin will:

  • add the Java classes (GraalVM) to the native-image classpath to be compiled with client:compile,

  • merge the configuration json files with others found in the classpath,

  • extract the native libraries into target/client/$arch-$os/gvm/lib/lib${name}.a, so they can be linked with client:link,

  • and when targeting Android, extract the Android libraries (.aar) into target/client/$arch-$os/gvm/android_project/libs. These libraries can be added as regular dependencies for the Android project when creating the apk in the client:package goal.

8. Data Binding

Open Source

Connect is open source, and licensed under the BSD-3 license. Its source code is hosted under Gluon organization in Github.

Gluon Connect is a client-side library that simplifies binding your data from any source and format to JavaFX UI controls. It works by retrieving data from a data source and converting that data from a specific format into JavaFX observable lists and objects. It is designed to allow developers to easily add support for custom data sources and data formats.

By default, Gluon Connect already provides easy-to-use implementations for the most commonly used data sources:

  • File provider

  • REST provider

8.1. File provider

The File provider enables reading from and writing to a file that is located on the file system of the device. You can find the complete sample from our gluon-samples repository on GitHub: https://github.com/gluonhq/gluon-samples. Inside the repository, look for the folder named gluon-connect-file-provider.

8.1.1. Retrieving a list

Let’s assume that we have a file on the local file system with the following JSON content:

languages.json
[
  {"name":"Java","ratings":20.956},
  {"name":"C","ratings":13.223},
  {"name":"C++","ratings":6.698},
  {"name":"C#","ratings":4.481},
  {"name":"Python","ratings":3.789}
]

And the following POJO that will map the JSON objects from the file above to a Java object:

Language.java
public class Language {
    private String name;
    private double ratings;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getRatings() {
        return ratings;
    }

    public void setRatings(double ratings) {
        this.ratings = ratings;
    }
}

We can then assign a list of programming language objects to a JavaFX ListView control with the following code:

// create a FileClient to the specified File
FileClient fileClient = FileClient.create(new File("languages.json"));

// create a JSON converter that converts the nodes from a JSON array into language objects
InputStreamIterableInputConverter<Language> converter = new JsonIterableInputConverter<>(Language.class);

// retrieve a list from a ListDataReader created from the FileClient
GluonObservableList<Language> languages = DataProvider.retrieveList(fileClient.createListDataReader(converter));

8.1.2. Retrieving an object

Retrieving a single object from a file resource looks similar to retrieving a list. Assume that we have a user.json file with the following JSON content:

user.json
{"name":"Duke","subscribed":true}

And the following POJO for mapping the JSON object to a Java object:

User.java
public class User {
    private String name;
    private boolean subscribed;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isSubscribed() {
        return subscribed;
    }

    public void setSubscribed(boolean subscribed) {
        this.subscribed = subscribed;
    }
}

The user object can then be retrieved from the JSON file with the following code:

// create a FileClient to the specified File
FileClient fileClient = FileClient.create(new File("user.json"));

// create a JSON converter that converts a JSON object into a user object
InputStreamInputConverter<User> converter = new JsonInputConverter<>(User.class);

// retrieve an object from an ObjectDataReader created from the FileClient
GluonObservableObject<User> user = DataProvider.retrieveObject(fileClient.createObjectDataReader(converter));

8.1.3. Storing an object

We can use the same file and POJO again to show you how to store an object into a file:

// create an instance of a User to store
User user = new User();
user.setName("Duchess");
user.setSubscribed(false);

// create a FileClient to the specified File
FileClient fileClient = FileClient.create(new File("user.json"));

// create a JSON converter that converts the user object into a JSON object
OutputStreamOutputConverter<User> outputConverter = new JsonOutputConverter<>(User.class);

// store an object with an ObjectDataWriter created from the FileClient
GluonObservableObject<User> gluonUser = DataProvider.storeObject(user, fileClient.createObjectDataWriter(converter));

8.2. REST provider

The REST provider enables reading from and writing to an HTTP URL resource. For demonstrating this we will make use of the public StackExchange API. You can find the complete sample from our gluon-samples repository on GitHub: https://github.com/gluonhq/gluon-samples. Inside the repository, look for the folder named gluon-connect-rest-provider.

8.2.1. Retrieving a list

The URL we are going to use for retrieving a list, is the error REST endpoint. Here is how you would retrieve a list from a URL with Gluon Connect:

// create a RestClient to the specific URL
RestClient restClient = RestClient.create()
        .method("GET")
        .host("https://api.stackexchange.com")
        .path("/2.2/errors");

// retrieve a list from the DataProvider
GluonObservableList<Error> errors = DataProvider.retrieveList(restClient.buildListDataReader(Error.class));

// create a JavaFX ListView and populate it with the retrieved list
ListView<Error> lvErrors = new ListView<>(errors);

As you can see, we didn’t specify any Converter. That is because the RestClient will try to find a suitable Converter based on the Content-Type response header. If the response header is not specified, it assumes that the response will be formatted in JSON. Sometimes, the automatic detection of the Converter fails or you need to use a custom Converter. In that case, you use the second method for creating a ListDataReader where you provide the converter that you want to use.

// create a RestClient to the specific URL
RestClient restClient = RestClient.create()
        .method("GET")
        .host("https://api.stackexchange.com")
        .path("/2.2/errors");

// create a custom converter
InputStreamIterableInputConverter<Error> converter = new ItemsIterableInputConverter<>(Error.class);

// retrieve a list from the DataProvider using the custom converter
GluonObservableList<Error> errors = DataProvider.retrieveList(restClient.createListDataReader(converter));

8.2.2. Retrieving an object

For retrieving a single object, we will use the questions REST endpoint.

// create a RestClient to the specific URL
RestClient restClient = RestClient.create()
        .method("GET")
        .host("https://api.stackexchange.com")
        .path("/2.2/questions/36243147")
        .queryParam("order", "desc")
        .queryParam("sort", "activity")
        .queryParam("site", "stackoverflow");

// retrieve an object from the DataProvider
GluonObservableObject<Question> question = DataProvider.retrieveObject(restClient.createObjectDataReader(Question.class));

// show information on the retrieved object
Label lbTitle = new Label();
question.initializedProperty().addListener((obs, ov, nv) -> {
    if (nv && question.get() != null) {
        lbTitle.textProperty().bind(question.get().titleProperty());
    }
});

8.3. Deep Dive

In the following section we will dive deeper into the concepts of Gluon Connect and what the relation is between these concepts. The two implementations that we talked about above also make use of the same concepts, but they hide them for the user in the internal implementation. If you want to know a bit more about the inner workings of these providers or if you are interested in adding support for a custom DataSource, you should continue reading the following sections.

8.3.1. Concepts

Gluon Connect consists of the following basic concepts:

  • DataSource: specifies where the data is read from and/or written to

  • Converter: converts the read data into objects and vice versa converts objects into data to be written

  • DataProvider: takes care of mapping the data into usable observable lists and objects

DataSource

The DataSource defines where Gluon Connect should read and/or store the data. The DataSource makes use of the standard java.io.InputStream and java.io.OutputStream classes. It is split up into two parts: the InputDataSource that provides an InputStream to read the data from; the OutputDataSource that provides an OutputStream to write the data into. For convenience purposes, there is also an IODataSource that has access to both the InputStream and the OutputStream.

Gluon Connect includes by default two DataSource implementations:

  • FileDataSource: reads and writes data from a File that is accessible from the local system

  • RestDataSource: reads and writes data from an HTTP URL resource

Converter

The Converter is able to convert data from an Object into a specific data format or convert data from a certain data format back into an Object. The Converter is split up into three different parts:

All three interfaces do not specify where the data is coming from. This allows for maximum extensibility. Gluon Connect provides an abstract implementation for these Converters. The InputConverter and IterableInputConverter both have an implementation where the data to convert is taken from an InputStream. The OutputConverter has an analogous implementation that converts the object by writing into an OutputStream.

Gluon Connect provides Converter implementations for JSON and String out of the box.

DataProvider

The DataProvider is the class that will ultimately provide the observable lists or objects that you can use with your JavaFX UI controls. Gluon Connect provides a custom observable list and observable object called GluonObservableList and GluonObservableObject respectively.

The DataProvider itself has four methods:

All these methods will return the GluonObservableList or GluonObservableObject instances immediately. The actual process of retrieving, storing or removing the list or object happens asynchronously in a background thread. For example, when retrieving a list or object you can listen for the initialized property to know when the list or object is fully initialized by the provided reader.

If we for instance look at the retrieveObject method, we see that it has an ObjectDataReader as parameter. The ObjectDataReader is an interface that is able to create a new instance of GluonObservableObject and read an object of a certain type. The source where the object is read from and the format of the data is completely left open to the actual implementing classes. Usually though, you will combine a DataSource with a Converter to implement this functionality. The most basic DataProvider that you can use as an ObjectDataReader is the InputStreamObjectDataReader. This class takes an InputSource as the DataSource and an InputStreamConverter as the Converter. When an instance of InputStreamObjectDataReader is passed into the Dataprovider.retrieveObject method, the object will be retrieved by getting the InputStream from the InputSource and then pass the InputStream to the InputStreamConverter, which will read the data from the InputStream and convert it into the desired object.

8.3.2. Sample

The following sample will use the three concepts by providing a DataSource that reads from a classpath resource, a Converter that converts JSON into objects and a DataProvider that combines the DataSource with the Converter to get access to GluonObservable objects. The complete sample for this can be found in our gluon-samples repository on GitHub: https://github.com/gluonhq/gluon-samples. Inside the repository, look for the folder named gluon-connect-basic-usage.

Retrieving a list

Let’s presume we have the following simple languages.json file that is accessible from the root of the classpath.

/languages.json
[
  {"name":"Java","ratings":20.956},
  {"name":"C","ratings":13.223},
  {"name":"C++","ratings":6.698},
  {"name":"C#","ratings":4.481},
  {"name":"Python","ratings":3.789}
]

And the following POJO that will map the JSON objects from the list above to a Java object:

Language.java
public class Language {
    private String name;
    private double ratings;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getRatings() {
        return ratings;
    }

    public void setRatings(double ratings) {
        this.ratings = ratings;
    }
}

We can then assign a list of programming language objects to a JavaFX ListView control with the following code:

// create a DataSource that loads data from a classpath resource
InputDataSource dataSource = new BasicInputDataSource(Main.class.getResourceAsStream("/languages.json"));

// create a Converter that converts a json array into a list
InputStreamIterableInputConverter<ProgrammingLanguage> converter = new JsonIterableInputConverter<>(ProgrammingLanguage.class);

// create a ListDataReader that will read the data from the DataSource and converts
// it from json into a list of objects
ListDataReader<ProgrammingLanguage> listDataReader = new InputStreamListDataReader<>(dataSource, converter);

// retrieve a list from the DataProvider
GluonObservableList<ProgrammingLanguage> programmingLanguages = DataProvider.retrieveList(listDataReader);

// create a JavaFX ListView and populate it with the retrieved list
ListView<ProgrammingLanguage> lvProgrammingLanguages = new ListView<>(programmingLanguages);
Retrieving an object

Retrieving an object looks somewhat similar to retrieving a list. However, instead of converting a JSON array into a list, we will convert a JSON object directly into a Java object. Below we have the user.json file and the POJO that will hold the information in the JSON object:

/user.json
{"name":"Duke","subscribed":true}
User.java
public class User {
    private StringProperty name = new SimpleStringProperty();
    private BooleanProperty subscribed = new BooleanProperty();

    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public StringProperty nameProperty() {
        return name;
    }

    public boolean isSubscribed() {
        return subscribed.get();
    }

    public void setSubscribed(boolean subscribed) {
        this.subscribed.set(subscribed);
    }

    public BooleanProperty subscribedProperty() {
        return subscribed;
    }
}

As you can see, we use JavaFX properties here instead of native Java types. This allows us to bind the properties to a JavaFX UI control, e.g. a Label. To retrieve the object from the JSON file, we use the following code:

// create a DataSource that loads data from a classpath resource
InputDataSource dataSource = new BasicInputDataSource(Main.class.getResourceAsStream("/user.json"));

// create a Converter that converts a json object into a java object
InputStreamInputConverter<User> converter = new JsonInputConverter<>(User.class);

// create an ObjectDataReader that will read the data from the DataSource and converts
// it from json into an object
ObjectDataReader<User> objectDataReader = new InputStreamObjectDataReader<>(dataSource, converter);

// retrieve an object from the DataProvider
GluonObservableObject<User> user = DataProvider.retrieveObject(objectDataReader);

// when the object is initialized, bind its properties to the JavaFX UI controls
Label lbName = new Label();
CheckBox cbSubscribed = new CheckBox("Subscribed?");
user.initializedProperty().addListener((obs, oldValue, newValue) -> {
    if (newValue) {
        lbName.textProperty().bind(user.get().nameProperty());
        cbSubscribed.selectedProperty().bindBidirectional(user.get().subscribedProperty());
    }
});

Gluon CloudLink enables enterprise and mobile developers to easily connect their disparate services and applications together, enabling bi-directional communications between mobile apps, enterprise infrastructure, and cloud systems.

cloudlink and sdks

Gluon CloudLink is a Mobile Back-end as a Service (MBaaS) providing:

  • Data storage, synchronization (to the back-end or across devices) and connectors to other back-ends.

  • User management, including login methods for popular identity providers.

  • Usage analytics

  • Push notifications to Android and iOS devices.

9.1. Application Registration

The first step in enabling Gluon CloudLink in your application is to sign up for a Gluon CloudLink account. If you already have a Gluon CloudLink subscription you can safely skip this part. If not, go to the Gluon CloudLink Product page and choose a subscription that matches your needs. There is also an option to sign up for a 30-day trial account. Enter your billing information or login with your existing account when you have signed up for one of our other products before.

Once you are successfully signed up, a Gluon CloudLink application will be created for you automatically. You will also receive an email that contains a link to access the Gluon Dashboard.

9.1.1. Gluon Dashboard

The Gluon Dashboard is a web application from which you can configure and monitor your Gluon CloudLink application. Point your browser to https://gluon.io and sign in with the same account credentials that you used when subscribing for Gluon CloudLink at gluonhq.com.

login

Select any of the items from the menu on the left to begin configuring your Gluon Application.

Login method

9.1.2. Before you begin

The chapters that follow below will describe all the functional components that are provided by Gluon CloudLink. Each component talks about the required steps on how to set up, configure and use them from the perspective of your enterprise application as well as from the mobile client application. To be able to use Gluon CloudLink you will need to make sure everything is set up correctly.

Server Configuration

Communication between your enterprise application and Gluon CloudLink is done by using the designated Gluon CloudLink Enterprise REST endpoints. Each of these endpoints is described in the appropriate sections below. For your convenience, we also provide a Java Client that calls on these same endpoints. The Java Client currently has two implementations that you can choose from. The Java EE client is most suited when your enterprise application is running inside a Java EE application container. The Spring client is more suited for enterprise applications that are running inside the Spring framework.

JavaDocs for the Gluon CloudLink Enterprise SDK can be found at the following URL:

Content Encoding

All data that is written to Gluon CloudLink by using the enterprise REST endpoints, either directly or by use of the Java Client must be encoded in UTF-8. This is the only encoding that is supported by Gluon CloudLink. If your data you sent looks garbled, please verify that you correctly applied UTF-8 encoding.

Dependencies

The following maven dependencies should be added to your enterprise project to make use of the Gluon CloudLink Enterprise SDK. Note that the Eclipse Yasson dependency is only required when you are running inside a Java EE 7 container. Java EE 8 containers should automatically ship with an implementation of the JSON-Binding API.

Java EE
Maven
<dependencies>
    <dependency>
        <groupId>com.gluonhq</groupId>
        <artifactId>cloudlink-enterprise-sdk-javaee</artifactId>
        <version>1.2.1</version>
    </dependency>
    <!-- only required when running inside a Java EE 7 container -->
    <dependency>
        <groupId>org.eclipse</groupId>
        <artifactId>yasson</artifactId>
        <version>1.0</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
gradle
repositories {
    mavenCentral()
}

dependencies {
    compile 'com.gluonhq:cloudlink-enterprise-sdk-javaee:1.2.1'

    // only required when running inside a Java EE 7 container
    runtime 'org.eclipse:yasson:1.0'
}
Spring
Maven
<dependencies>
    <dependency>
        <groupId>com.gluonhq</groupId>
        <artifactId>cloudlink-enterprise-sdk-spring</artifactId>
        <version>1.2.1</version>
    </dependency>
</dependencies>
gradle
repositories {
    mavenCentral()
}

dependencies {
    compile 'com.gluonhq:cloudlink-enterprise-sdk-spring:1.2.1'
}
Authentication

The REST endpoints themselves require an HTTP Authorization header that is used to authenticate the requests to Gluon CloudLink. The value of the Authorization header is as follows:

Authorization: Gluon MHwwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The SERVER_KEY needs to be replaced with the actual server key of your Gluon CloudLink application and can be found in the Gluon Dashboard, in the Server tab of the Credentials section.

credentials server
JavaEE

When using the Java Client, authentication is done by providing the server key when you create an instance of the Java Client:

Manual instantiation
CloudLinkClientConfig config = new CloudLinkClientConig("MHwwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
CloudLinkClient client = new CloudLinkClient(config);

Alternatively, when running inside a CDI aware environment, you can inject an instance of the JavaEE client as follows:

Using injection
@Inject
@CloudLinkConfig(serverKey = "MHwwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
private CloudLinkClient client;
Spring

Authentication with the Spring Client is done in the same way as the JavaEE Client:

Manual instantiation
CloudLinkClientConfig config = new CloudLinkClientConig("MHwwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
CloudLinkClient client = new CloudLinkClient(config);

When you are using Spring Boot, you can make use of autowiring to inject an instance of the Spring Client. An application property is then required that contains the server key of your Gluon CloudLink Application.

application.properties
gluon.cloudlink.serverKey=MHwwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Using autowiring
private CloudLinkClient client;

@Autowired
public MyService(CloudLinkClient client) {
    this.client = client;
}
Client Configuration

The Gluon Mobile CloudLink Client is the component that is used for handling the communication between your Mobile Client application and Gluon CloudLink. It is automatically added as a dependency to the main Gluon Mobile artifact, but can also be added separately.

Gradle Configuration

Gluon Mobile projects use gradle as the build tool for building and provisioning a Gluon Mobile application. The gradle project makes use of the JavaFX Mobile plugin and the associated build.gradle file needs to be configured with the following dependencies and Charm Down configuration.

repositories {
    maven {
        url 'https://nexus.gluonhq.com/nexus/content/repositories/releases/'
    }
}

dependencies {
    compile 'com.gluonhq:charm:4.3.7'
}

jfxmobile {
    downConfig {
        plugins 'device', 'push-notifications', 'storage'
    }
}
Authentication

Gluon Mobile uses the application specific credentials to sign all requests that are made to Gluon CloudLink. This allows Gluon CloudLink to know on behalf of which application the request is initiated.

To apply the client credentials to your Gluon Mobile project, create a JSON configuration file called gluoncloudlink_config.json and save it under the directory src/main/resources. This file will automatically be picked up by Gluon Mobile when needed. Insert the content below, making sure that you replace the values for the applicationKey and applicationSecret with the correct credentials for your application:

{
  "gluonCredentials": {
    "applicationKey": "b916XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "applicationSecret": "9c11XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
  }
}

From the Gluon Dashboard, select the Credentials item from the menu on the left. You will find the key and secret tokens of your Gluon CloudLink application inside the Client tab. You can either manually copy and paste them in the configuration file or you can directly download the configuration file by clicking the download button.

credentials client

9.2. Data Storage

Applications often use data that needs to be stored. Some data is only relevant to a specific instance of the application (i.e. local settings), other data is relevant to all instances of the application (i.e. chat data) and some data is relevant to some instances (i.e. user preferences for a user who has a number of devices).

Gluon CloudLink Client is the component that manages data on the client. It provides an API that allows users to store and synchronize data and provides a number of options for developers to choose from for each data entity:

  • should the data be stored on the device only?

  • should the data be stored in Gluon CloudLink?

  • should changes to data on the device propagate to Gluon CloudLink?

  • in case the data in Gluon CloudLink changes, does the local device copy needs to be synchronized immediately?

Gluon CloudLink Client communicates via an efficient protocol with Gluon CloudLink to achieve the different goals. Gluon CloudLink provides a scalable and persistent data store that is used to store data and to update clients when data changes.

Communication between your Gluon Mobile application and Gluon CloudLink works in two directions:

  • your mobile data is stored in the cloud when you want to, and

  • your mobile data is updated on the device whenever it is updated in the cloud.

In addition, Gluon CloudLink provides the ability to link your Gluon Mobile application with another back-end or cloud infrastructure by configuring one or more Connectors.

Gluon CloudLink makes sure your data is consistent across the different devices. Depending on your preferences, changes on one device propagate immediately to other devices. Or they might only be stored in Gluon CloudLink and the devices will manually request the latest version whenever they want.

DataClient

The DataClient class is the access point to the Data Storage. You can get a reference to a DataClient instance by using the DataClientBuilder:

DataClient dataClient = DataClientBuilder.create().build();
Operation Mode

When working with the DataClient, there are currently three operation modes you can choose from:

  • CLOUD_ONLY: data will only be persisted on and retrieved from the Gluon CloudLink service

  • LOCAL_ONLY: data will only be persisted on and retrieved from the local file system of the device where the application is running on

  • CLOUD_FIRST: data will be persisted on and retrieved from the Gluon CloudLink service as the primary data source, but a local copy will be kept in sync

The default operation mode is CLOUD_FIRST, but can be explicitly specified when creating the DataClient instance:

// specify LOCAL_ONLY operation mode
DataClient dataClient = DataClientBuilder.create()
        .operationMode(OperationMode.LOCAL_ONLY)
        .build();
Authentication

You can configure the DataClient to link data with an authenticated user. The following authentication methods are currently available:

  • Facebook: authenticate users with Facebook Login

  • GitHub: authenticate users with GitHub

  • Google: authenticate users with Google Sign-In

  • Twitter: authenticate users with Twitter

  • Email and Password: authenticate users by letting them sign up and sign in with their personally chosen email address and password

See the documentation on User Management to read more about configuring user authentication.

To enable user authentication on the client, you need to provide a UserClient instance when building the DataClient. The provided UserClient will ensure that an authenticated user is present when the DataClient tries to access underlying data. Authentication is done by presenting the user with an authentication view from which one of the configured login methods can be chosen.

// enable user authentication
UserClient authenticationClient = new UserClient();
DataClient dataClient = DataClientBuilder.create()
        .authenticateWith(authenticationClient)
        .build();

// the next statement will trigger the authentication view
GluonObservableList<Note> notes = DataProvider.retrieveList(dataClient.createListDataReader("notes", Note.class));
Data Storage

Once you have a DataClient reference, you can start storing and retrieving data. There are two different types of data that can be managed in Gluon CloudLink:

  • single objects, and

  • lists of objects

Because these objects and lists are maintained remotely, we will call them remote entities. Every remote entity is identified by a unique string identifier.

Retrieving lists

For example, retrieving a remote list with the identifier notes can be done with the following code:

GluonObservableList<Note> notes = DataProvider.retrieveList(dataClient.createListDataReader("notes", Note.class));
ListView<Note> notesListView = new ListView<>(notes);

In its simplest form the createListDataReader method returns an instance of GluonObservableList without any synchronization flags.

You may have noticed that DataClient doesn’t provide a method for creating a new list. That is because a Gluon CloudLink list always exists. When a list is retrieved the first time, an empty list will automatically be created for you.

Retrieving objects

A remote object works a bit differently than a list, because in contrast to a list, an object does have the notion of existence. This explains why we have three methods for managing remote objects: createObjectDataReader, createObjectDataWriter and createObjectDataRemover.

// store the object
Note note = new Note();
note.setContent("This is the content for the note.");
GluonObservableObject<Note> gluonNote = DataProvider.storeObject(note, dataClient.createObjectDataWriter("a-single-note", Note.class));

// retrieve the object
GluonObservableObject<Note> gluonNote = DataProvider.retrieveObject(dataClient.createObjectDataReader("a-single-note", Note.class));

// remove the object
DataProvider.removeObject(gluonNote, dataClient.createObjectDataRemover());
GluonObservable ConnectState

All the operations on the DataProvider are asynchronous in nature and are executed in a separate background thread. You can listen for changes on the stateProperty of the returned GluonObservable object to monitor the progress of the background operation. To know when your object or list is ready to be used, you can also listen for the initializedProperty instead. Listed below you’ll find a number of common use cases when working with remote entities.

Create a remote object when it does not yet exist

When retrieving an object from Gluon CloudLink, you can detect if this object was already stored previously by using the following pattern:

GluonObservableObject<Note> gluonNote = DataProvider.retrieveObject(dataClient.createObjectDataReader("a-single-note", Note.class));
gluonNote.initializedProperty().addListener((observable, ov, nv) -> {
    if (nv) {
        if (gluonNote.get() == null) {
            // object not yet stored, initiate it now with a new object and store it
            gluonNote.set(new Note("This is some text for the note"));
            dataClient.push(gluonNote);
        } else {
            // object already stored previously
            Note note = gluonNote.get();
            System.out.println("Stored note: " + note.getContent());
        }
    }
});

Initialize an empty list with default objects

When you retrieve a list for the first time, an empty list will be created for you by the Gluon CloudLink service. Sometimes you wish to populate this empty list with default objects. You can do that with the following pattern:

GluonObservableList<Note> gluonNotes = DataProvider.retrieveList(dataClient.createListDataReader("notes", Note.class));
gluonNotes.initializedProperty().addListener((observable, ov, nv) -> {
    if (nv && gluonNotes.isEmpty()) {
        // initialize the list with some default notes
        gluonNotes.addAll(
                new Note("Text for note number 1."),
                new Note("Text for note number 2.")
        );
        dataClient.push(gluonNotes);
    }
});

Detecting failures when working with remote entities

If you notice that data isn’t stored or retrieved correctly, it might be that an exception occurred during the process. You can check the exceptionProperty on the GluonObservable object to see what exactly went wrong.

GluonObservableObject<Note> gluonNote = DataProvider.retrieveObject(dataClient.createObjectDataReader("a-single-note", Note.class));
gluonNote.stateProperty().addListener((observable, oldState, newState) -> {
    if (newState == ConnectState.FAILED) {
        if (gluonNote.getException() != null) {
            gluonNote.getException().printStackTrace();
        }
    }
});
Supported Data Types

The second parameter of the createListDataReader, createObjectDataReader and createObjectDataWriter methods on DataClient specifies the type of data that is stored in the remote entity. Gluon CloudLink supports three different data types: String, Map and custom classes. Inside a Map and for the fields of a custom class, the following field types can be used:

The JavaFX Property types are a requirement when a remote entity is retrieved in combination with the OBJECT_WRITE_THROUGH synchronization flag. See the Data Synchronization section for more information on these flags.

String

The String data type is the most basic of the three supported types. It simply stores and retrieves instances of string.

Map

The Map represents a convenient key/value store in which you can store arbitrary data. It is the most flexible data type, but is less type safe. The keys of the map must be strings, while the value can be any of the supported field types that are listed above. If the map contains a value that is not supported, those values will be ignored when storing or retrieving data.

Custom Class

As a final option, you can define your data structure inside a Custom Class. The DataClient will inspect the provided class for all declared fields of which the type matches any of the supported field types. Note that only non-static fields and non-final primitive fields will be considered. All other field declarations will be ignored when storing or retrieving data.

Data Synchronization

By default, no synchronization flags are configured when calling any of the methods we mentioned above. To enable synchronization, you can pass any of the SyncFlag enum constants to the method. There are four different types of SyncFlag:

  • OBJECT_READ_THROUGH: changes that occur on an object in Gluon CloudLink will be reflected to the fields on the local object

  • OBJECT_WRITE_THROUGH: changes on JavaFX Property fields will be automatically written to the object in Gluon CloudLink

  • LIST_READ_THROUGH: adding and removing objects on a list in Gluon CloudLink will be reflected to the local list

  • LIST_WRITE_THROUGH: adding and removing objects locally will automatically add and remove them to the list in Gluon CloudLink

Note that the OBJECT_READ_THROUGH and LIST_READ_THROUGH flags don’t have any effect when used in combination with the LOCAL_ONLY operation mode.

Also note that the OBJECT_WRITE_THROUGH flag will only work on non-static Observable fields of a Custom Class.

As an example, the code snippet below retrieves an instance of GluonObservableList that is configured with the list read through and list write through synchronization flags. This means that any changes that are done on the client, either adding or removing items, are propagated back to Gluon CloudLink. The reverse is true as well: all changes that occur in Gluon CloudLink will be reflected back to the local list on the device. Changes that occur on the objects inside the list won’t be propagated.

GluonObservableList<Note> notes = DataProvider.retrieveList(dataClient.createListDataReader("notes", Note.class, SyncFlag.LIST_READ_THROUGH, SyncFlag.LIST_WRITE_THROUGH));

9.2.2. Integration with Enterprise and Cloud Systems

One of the features of Gluon CloudLink is to link it with an existing back end infrastructure. There are two main ways for linking Gluon CloudLink with such a back end infrastructure.

  1. Call into the Gluon CloudLink REST endpoints from an enterprise system to retrieve and manage data. You can either directly call the REST endpoints or use an implementation of the Gluon CloudLink Enterprise SDK that best suits your existing enterprise environment.

  2. Use Connectors to let Gluon CloudLink automatically push and/or pull data to and from an enterprise system.

REST endpoints
Get object

Retrieve an object from the data service.

Path Parameters
Name Description

objectIdentifier

The unique identifier of the object to retrieve.

Curl sample
curl -X GET "https://cloud.gluonhq.com/3/data/enterprise/object/sample-object" \
     -H "Authorization: Gluon YOUR_SERVER_KEY"
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

SampleType sampleObject = client.getObject("sample-object", SampleType.class);
Add object

Add a new object into the data service with the defined payload.

Path Parameters
Name Description

objectIdentifier

The unique identifier of the object to add.

Body

The body of the request must be a UTF-8 encoded JSON string and will be used as the payload for the newly added object.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/object/sample-object/add" \
     -H "Authorization: Gluon YOUR_SERVER_KEY" \
     -H "Content-Type: application/json; charset=utf-8" \
     --data '{"text":"someField","number":123}'
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

SampleType sampleObject = new SampleType("someField", 123);
SampleType storedObject = client.addObject("sample-object", sampleObject);
Update object

Update an existing object in the data service with the defined payload.

Path Parameters
Name Description

objectIdentifier

The unique identifier of the object to update.

Body

The body of the request must be a UTF-8 encoded JSON string and will be used as the payload for the object that is being updated.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/object/sample-object/update" \
     -H "Authorization: Gluon YOUR_SERVER_KEY" \
     -H "Content-Type: application/json; charset=utf-8" \
     --data '{"text":"someOtherText","number":321}'
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

SampleType sampleObject = new SampleType("someField", 123);
SampleType storedObject = client.addObject("sample-object", sampleObject);
storedObject.setText("someOtherText");
storedObject.setNumber(321);
SampleType updateObject = client.updateObject("sample-object", storedObject);
Remove object

Remove an existing object from the data service.

Path Parameters
Name Description

objectIdentifier

The unique identifier of the object to remove.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/object/sample-object/remove" \
     -H "Authorization: Gluon YOUR_SERVER_KEY"
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

client.removeObject("sample-object");
Get list

Retrieve a list of objects from the data service.

Path Parameters
Name Description

listIdentifier

The unique identifier of the list to retrieve.

Curl sample
curl -X GET "https://cloud.gluonhq.com/3/data/enterprise/list/sample-list" \
     -H "Authorization: Gluon YOUR_SERVER_KEY"
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

List<SampleType> sampleList = client.getList("sample-list", SampleType.class);
Add object to list

Add an object to a list in the data service.

Path Parameters
Name Description

listIdentifier

The unique identifier of the list to add the object into.

objectIdentifier

The unique identifier of the object to add to the list.

Body

The body of the request must be a UTF-8 encoded JSON string and will be used as the payload for the object that is being added into the list.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/list/sample-list/add/sample-object-1" \
     -H "Authorization: Gluon YOUR_SERVER_KEY" \
     -H "Content-Type: application/json; charset=utf-8" \
     --data '{"text":"someField","number":123}'
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

SampleType sampleObject1 = new SampleType("someField", 123);
SampleType addedSampleObject1 = client.addToList("sample-list", "sample-object-1", sampleObject1);
Update object in list

Update an object in a list in the data service.

Path Parameters
Name Description

listIdentifier

The unique identifier of the list where the object to update is stored.

objectIdentifier

The unique identifier of the object to update in the list.

Body

The body of the request must be a UTF-8 encoded JSON string and will be used as the payload for the object that is being updated in the list.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/list/sample-list/update/sample-object-1" \
     -H "Authorization: Gluon YOUR_SERVER_KEY" \
     -H "Content-Type: application/json; charset=utf-8" \
     --data '{"text":"someOtherText","number":321}'
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

SampleType sampleObject1 = new SampleType("someField", 123);
SampleType addedSampleObject1 = client.addToList("sample-list", "sample-object-1", sampleObject1);
addedSampleObject1.setText("someOtherText");
addedSampleObject1.setNumber(321);
SampleType updatedSampleObject1 = client.updateInList("sample-list", "sample-object-1", addedSampleObject1);
Remove object from list

Remove an object from a list in the data service.

Path Parameters
Name Description

listIdentifier

The unique identifier of the list where the object to remove is stored.

objectIdentifier

The unique identifier of the object to remove from the list.

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/data/enterprise/list/sample-list/remove/sample-object-1" \
     -H "Authorization: Gluon YOUR_SERVER_KEY"
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new JavaEECloudLinkClient(config);

client.removeFromList("sample-list", "sample-object-1");
Connectors

A Connector can be used for automatic pushing and pulling of data from Gluon CloudLink to an enterprise back end system.

  • push: data that is updated in Gluon CloudLink is sent through to the enterprise back end

  • pull: data is requested from the enterprise back end by Gluon CloudLink

Different connector implementations are provided by Gluon CloudLink out of the box. Depending on the configured Connector, some extra code will be required on the back end application as well. E.g. when linking to Gluon CloudLink with the REST Connector, a handler must exist on the back end application that listens for HTTP requests that are called by Gluon CloudLink.

Configuring a Connector for a Gluon CloudLink Application is done in the Gluon Dashboard. The following Connectors are available for use within Gluon CloudLink. If you have a specific requirement for a custom Connector, please let us know.

Connector name

Description

REST Connector

This is the most generic connector and allows synchronization data to any enterprise or cloud system that can talk REST.

CloudLink Remote Function

This connector invokes a remote function that is defined in your CloudLink application.

Couchbase Connector

This connector specifically synchronizes data from and to your Couchbase server. Ideally suited if you have an existing Couchbase installation that you want to unlock to mobile devices.

REST Connector

The REST Connector sends and receives data over a network connection using the standard HTTP protocol.

The connector can be set from the Gluon Dashboard, Data Management, Connectors tab.

Data Storage - rest connector
Data Storage - rest connector

When using the REST Connector to link to a back end system, the Gluon CloudLink Application only needs to be configured with the URL where the requests from Gluon CloudLink need to be sent to.

Push endpoints

When a client application requests data to be added, updated or removed from Gluon CloudLink, those requests will be mapped with the REST Controller by making an HTTP request to one of the following six endpoints. Each of them should be implemented on the back end application to be able to handle the request.

Table 1. A new object is added

URL

/object/{objectIdentifier}/add

Method

POST

Request Body

JSON payload of the new object. If the object is a String, the payload will use a key named v.

Description

A new object is added to Gluon CloudLink. The objectIdentifier is the identifier that is passed in from the application client when retrieving or storing the object.

Table 2. An existing object is updated

URL

/object/{objectIdentifier}/update

Method

POST

Request Body

JSON payload of the updated object

Description

An existing object is updated in Gluon CloudLink. The objectIdentifier is the identifier that is passed in from the application client when retrieving or storing the object.

Table 3. An existing object is removed

URL

/object/{objectIdentifier}/remove

Method

POST

Request Body

The request body is empty

Description

An existing object is removed from Gluon CloudLink. The objectIdentifier is the identifier that is passed in from the application client when retrieving or storing the object.

Table 4. A new object is added to a list

URL

/list/{listIdentifier}/add/{objectIdentifier}

Method

POST

Request Body

JSON payload of object that is being added to the list

Description

A new object is added to a list. The objectIdentifier is an identifier that is assigned to the object specific to Gluon CloudLink, i.e. the client application is not aware of this identifier. This is in contrast to the listIdentifier, which is the identifier that is passed in from the application client when retrieving the list.

Table 5. An existing object is updated in a list

URL

/list/{listIdentifier}/update/{objectIdentifier}

Method

POST

Request Body

JSON payload of object that is being updated in the list

Description

An existing object in the list is updated. The objectIdentifier is an identifier that is assigned to the object specific to Gluon CloudLink, i.e. the client application is not aware of this identifier. This is in contrast to the listIdentifier, which is the identifier that is passed in from the application client when retrieving the list.

Table 6. An existing object is removed from a list

URL

/list/{listIdentifier}/remove/{objectIdentifier}

Method

POST

Request Body

The request body is empty

Description

An existing object is removed from a list. The objectIdentifier is an identifier that is assigned to the object specific to Gluon CloudLink, i.e. the client application is not aware of this identifier. This is in contrast to the listIdentifier, which is the identifier that is passed in from the application client when retrieving the list.

Pull endpoints

When a client application requests an object or a list that is not yet known inside Gluon CloudLink, Gluon CloudLink calls one of the following two endpoints on the back end application to retrieve the initial object or list information.

Table 7. An object is requested

URL

/object/{objectIdentifier}

Method

GET

Response Body

JSON payload of the object to retrieve

Description

A new object is being requested from the client application with the specified objectIdentifier.

Table 8. A list is requested

URL

/list/{listIdentifier}

Method

GET

Response Body

JSON payload of the list to retrieve. The payload is a JSON array containing a list of zero or more JSON objects. Each JSON object in the array defines two keys: id that defines the object identifier and payload which is the JSON payload of the object, represented as a JSON string.

Description

A new list is being requested from the client application with the specified listIdentifier.

The CloudLink Remote Function Connector sends data changes by invoking a Remote Function that is configured in your CloudLink application.

Data Storage - cloudlink remote function connector

When a client application requests data to be added, updated or removed from Gluon CloudLink, those requests will be provided with the remote function invocation as a JSON string. For instance, when invoking a REST Remote Function, the payload is provided as the raw body of a POST request.

Pushing Objects
A new object is added
{
    "operation": "objectAdded",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef",
    "payload" : "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"
}
An existing object is updated
{
    "operation": "objectUpdated",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef",
    "payload" : "{\"firstName\":\"Jane\",\"lastName\":\"Doe\"}"
}
An existing object is removed
{
    "operation": "objectRemoved",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef"
}
Pushing Lists
A new object is added to a list
{
    "operation": "itemAddedToList",
    "listIdentifier": "fedcba98-7654-3210-fedc-ba9876543210",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef",
    "payload" : "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"
}
An existing object is updated in a list
{
    "operation": "itemUpdatedInList",
    "listIdentifier": "fedcba98-7654-3210-fedc-ba9876543210",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef",
    "payload" : "{\"firstName\":\"Jane\",\"lastName\":\"Doe\"}"
}
An existing object is removed from a list
{
    "operation": "itemRemovedFromList",
    "listIdentifier": "fedcba98-7654-3210-fedc-ba9876543210",
    "objectIdentifier": "01234567-89ab-cdef-0123-456789abcdef"
}
Couchbase Connector

The Couchbase Connector is able to send data to an existing Couchbase Server. The only requirement on the Couchbase Server is an existing bucket that will hold the lists and/or objects from Gluon CloudLink.

couchbase server bucket

When activating the Couchbase Connector inside the Dashboard, you will need to provide the following information to let Gluon CloudLink be able to setup a connection with the Couchbase Server:

  • Nodes: a list of nodes that the Couchbase Client on Gluon CloudLink uses to setup the connection to a Couchbase Cluster. You can specify more than one node, by separating them with a semicolon.

  • Bucket Name: the name of the Couchbase bucket that will hold the lists and/or objects

  • Bucket Password: the password of that Couchbase bucket

Data Storage - couchbase connector
Pushing Objects

Objects are stored in the Couchbase bucket using a key named objects/{objectIdentifier}, where objectIdentifier is the identifier that is passed in by the client application when storing or retrieving the object.

The document itself will be a JSON document that represents the JSON payload of the object. An example of such an object can be seen below:

objects/notes-settings
{
  "fontSize": 10,
  "sortingId": 2,
  "ascending": true,
  "showDate": false
}
Pushing Lists

Lists are also stored as a document with a key named lists/{listIdentifier}, where listIdentifier is the identifier that is passed in by the client application when retrieving the list.

For each object in the list, the document will contain a key that matches the identifier of the object. The value that is mapped to that key is a JSON document that represents the JSON payload of the object. Below is an example of a list that contains two objects:

lists/notes
{
  "af52f4c6-a64b-4823-b9fb-3cbef79d7577": {
    "creationDate": 1463062055,
    "title": "new note",
    "text": "sample 2"
  },
  "f880774a-20e9-11e6-b67b-9e71128cae77": {
    "creationDate": 1463054952,
    "title": "another note",
    "text": "and also another sample text"
  }
}
Pulling Data

Gluon CloudLink can also pull data from the same Couchbase Server when a list and/or object is retrieved that is not yet known within the Gluon CloudLink data store. It will try to retrieve a list or object from the configured bucket, by using the same identifiers as described in the push section above: lists/{listIdentifier} for lists and objects/{objectIdentifier} for objects. The format of the documents stored inside the Couchbase bucket must also follow the same format as described in the previous section.

9.3. Remote Functions

Almost all mobile applications that exist today interact with a back end infrastructure for managing their data. In theory, an application can directly make requests to these back ends, but there are a number of drawbacks for doing this:

  • Mobile devices are not always connected to the internet. It takes lots of boilerplate code to check connectivity and handle rescheduling of requests when connectivity is restored.

  • Applications on mobile devices have specific lifecycles and need to behave according to specific policies. Some resources are conditionally available. The battery might be low, the application might be running in the background, the device is connected with a paid cellular network, etc. Depending on those conditions, an application must behave differently.

  • Mobile devices have less resources than the regular server, and those resources need to shared with other applications.

CloudLink provides Remote Functions to give the application a reliable and secure way for linking with existing back end systems.

9.3.1. Managing Remote Functions

Managing remote functions is done in the API Management section of the Gluon Dashboard. Each remote function is uniquely identified by a name. This name will be used in the client application when it makes a call to the defined remote function.

Sign in to the Gluon Dashboard and navigate to API Management. You will find the following sections:

  • Functions: this is where the remote functions are configured

  • Authentication: in here different authentication schemes can be created that are used when authentication is required by a remote function

  • Call Log: logging information for calls that are made to a remote function

Api Management - overview
Remote Function Types

We currently support the following types to choose from when creating a new remote function:

  • HTTP Request: executes an HTTP request based on the configured parameters

  • Amazon AWS Lambda: calls an Amazon AWS Lambda function

  • Azure Function: calls a Microsoft Azure Function

  • Fn Project Function: invoke a function on Fn

  • Gluon Function: invoke a function that is running on Gluon CloudLink

Create Remote Functions - HTTP Request

Click the + button to add a new remote function and choose HTTP Request. A dialog will be shown where the following components can be configured:

Api Management - remote function http request
Function Name

The function name is a unique identifier for the remote function. The name is used in the client application when a call needs to be triggered to the remote function.

Enable Mock Response

Mock responses can be enabled for testing purposes. When mocking is enabled, a call to the remote function will not create a request to the actual configured endpoint, but respond directly with the data that is defined for the mock response.

Method

The method defines what HTTP method to use when creating the request. The following methods are supported: GET, POST, PUT and DELETE.

Endpoint

The endpoint is the URI to use when creating the request.

Read Timeout

Specify the timeout, in milliseconds, for reading from the remote function endpoint. If the value is zero, the default timeout will be used.

Connect Timeout

Specify the timeout, in milliseconds, for connecting to the remote function endpoint. If the value is zero, the default timeout will be used.

Authentication Method

Specifies the authentication method that must be used when executing the request to the remote function. The authentication method can be selected from a list of authentication methods that are configured in the Authentication section.

Body Type

The body type can be specified when the POST or PUT method is selected. The body type defines what kind of data will be sent to the remote function. The following types are supported:

  • none: the request will be sent without a request body

  • x-www-form-urlencoded: for sending form url encoded requests

  • form-data: for sending form data multipart requests

  • raw: for sending raw text, i.e a json string

When the raw body type is chosen, two extra fields will be available to specify the data and the media type of the raw body content.

Caching Enabled

If caching is enabled, CloudLink caches each successful response from invocations to the remote function for one hour. CloudLink caching rules are based on standard HTTP caching mechanisms. In closer detail, two HTTP response headers are currently inspected:

  • ETag: if an entity tag is provided, it will be used in any subsequent request to the configured endpoint for cache validation.

  • Cache-Control: if cache control contains the words private or no-store, the response is not cached. If it contains public (default value) or a max-age value, the response is cached for the specified duration.

Both HTTP response headers can be combined to improve the caching mechanism.

Create Remote Functions - Amazon AWS Lambda

Before you can create a remote function for an AWS Lambda function, you need to add customer credentials that point to a valid Amazon AWS access key. Navigate to the Credentials section in Gluon Dashboard and choose the Customer tab. Add new credentials by clicking the + button and choose AWS Access Key. Enter the required information.

Credentials - aws access key

Navigate back to the API Management section and click the + button to add a new remote function while choosing Amazon AWS Lambda. A dialog will be shown where the following components can be configured:

Api Management - remote function aws lambda
Function Name

The function name is a unique identifier for the remote function. The name is used in the client application when a call needs to be triggered to the remote function.

Enable Mock Response

Mock responses can be enabled for testing purposes. When mocking is enabled, a call to the remote function will not create a request to the actual configured endpoint, but respond directly with the data that is defined for the mock response.

AWS Credentials

The AWS Access Key to use when listing the available AWS Lambda functions.

AWS Region

The AWS region where the AWS Lambda functions must be listed from.

AWS Lambda Function

The actual AWS Lambda function to link with this remote function.

AWS Lambda Version

An optional specific version of the AWS Lambda function that must be executed. Leaving this empty will use the default $LATEST version of the chosen AWS Lambda function.

Payload Type

An optional payload that should be sent along when executing the AWS Lambda function. The following types are supported:

  • none: no payload will be sent with the call

  • byte array: sends an array of raw bytes encoded as a Base64 string, useful when sending binary data from the client

  • string: useful for sending plain text

Payload Variable Name

When the selected payload type is string or byte array, you can specify the variable name of the payload. The payload is passed down to the AWS Lambda function as a JSON object containing the variable name as a key. When the payload type is string, the value mapped with the key will be the JSON value that is loaded from the Payload string that can be specified in the text area below. If the payload type is byte array, then the JSON value will be a Base64 encoded string of the passed in array of bytes.

Payload

When the string payload type is chosen, an extra text area will be available to specify the data for the string content. The resulting data string must be valid JSON. If this is not the case, the request to the remote function will be aborted with a Bad Request status.

Output Media Type

Defines the expected media type of the response from the AWS Lambda function. If left empty, the default value of text/plain will be used instead.

Read Timeout

Specify the timeout, in milliseconds, for reading from the remote function endpoint. If the value is zero, the default timeout will be used.

Connect Timeout

Specify the timeout, in milliseconds, for connecting to the remote function endpoint. If the value is zero, the default timeout will be used.

Create Remote Functions - Azure Function

Configure a remote function that can invoke a Microsoft Azure function with an http trigger. Click the + button to add a new remote function and choose Azure Function. A dialog will be shown in which the following components can be configured:

Api Management - remote function azure
Function Name

The function name is a unique identifier for the remote function. The name is used in the client application when a call needs to be triggered to the remote function.

Enable Mock Response

Mock responses can be enabled for testing purposes. When mocking is enabled, a call to the remote function will not create a request to the actual configured endpoint, but respond directly with the data that is defined for the mock response.

Method

The method defines what HTTP method to use when creating the request. The following methods are supported: GET, POST, PUT and DELETE.

Function URL

The Function URL that points to the Azure function.

API Key

Define the Function Key to use for authorizing the invocation to the Azure function. The key is passed down to the function using the x-functions-key HTTP header. Make sure that you copy the value of the Function Key and not its name. Azure functions using Anonymous authorization can leave this field empty.

Read Timeout

Specify the timeout, in milliseconds, for reading from the remote function endpoint. If the value is zero, the default timeout will be used.

Connect Timeout

Specify the timeout, in milliseconds, for connecting to the remote function endpoint. If the value is zero, the default timeout will be used.

Body Type

The body type can be specified when the POST or PUT method is selected. The body type defines what kind of data will be sent to the remote function. The following types are supported:

  • none: the request will be sent without a request body

  • x-www-form-urlencoded: for sending form url encoded requests

  • raw: for sending raw text, i.e a json string

When the raw body type is chosen, two extra fields will be available to specify the data and the media type of the raw body content.

Create Remote Functions - Fn Project Function

These are remote functions that are configured to run on the Fn Project platform. Your Fn function must be available from a public registry on Docker Hub. Click the + button to add a new remote function and choose Fn Project Function. A dialog will be shown in which the following components can be configured:

Api Management - remote function fn project function
Function Name

The function name is a unique identifier for the remote function. The name is used in the client application when a call needs to be triggered to the remote function.

Enable Mock Response

Mock responses can be enabled for testing purposes. When mocking is enabled, a call to the remote function will not create a request to the actual configured endpoint, but respond directly with the data that is defined for the mock response.

Docker Registry

The name of the docker image as it has been pushed to Docker Hub. You can optionally specify a tag for the image, for instance: gluonhq/helloworld:0.0.2.

Enable Input

When input is enabled, two extra fields will be available to specify the data and the media type of the input. The content of the text area will be passed down to the Fn Function during invocation.

Timeout

Specify the timeout, in seconds, for executing the remote function on the Fn Project platform. If the value is zero, the default timeout will be used.

Create Remote Functions - Gluon Function

Gluon Functions allow you to run Java functions inside a serverless platform that is managed by Gluon CloudLink itself. All you need is a zip file that bundles all the runtime jar files that are needed for running the function. The Gluon IDE plugins assist you with creating and deploying of your Gluon Remote Functions.

Click the + button to add a new remote function and choose Gluon Function. A dialog will be shown in which the following components can be configured:

Api Management - remote function gluon function
Function Name

The function name is a unique identifier for the remote function. The name is used in the client application when a call needs to be triggered to the remote function.

Enable Mock Response

Mock responses can be enabled for testing purposes. When mocking is enabled, a call to the remote function will not create a request to the actual configured endpoint, but respond directly with the data that is defined for the mock response.

Entrypoint

The entrypoint defines what method should be invoked when the Gluon Function is triggered. The syntax consists of the fully qualified class name and the method name to be invoked, separated with a double colon, i.e.: com.gluonhq.sample.function.GluonFunction::methodName

Java Runtime

Specify the Java Runtime environment that must be used for running the Gluon Function. You can choose between Java 8 and Java 9.

Memory

Define the maximum amount of memory that should be provided when running the Gluon Function. You can choose a value between 128MB and 1GB with intervals of 128MB.

Bundle

The bundle contains the actual classes that are needed for executing the Gluon Function. The bundle is a zip file containing one or more jar files that will be added to the classpath of the Gluon Function.

Read Timeout

Specify the timeout, in milliseconds, for reading from the remote function endpoint. If the value is zero, the default timeout will be used.

Connect Timeout

Specify the timeout, in milliseconds, for connecting to the remote function endpoint. If the value is zero, the default timeout will be used.

Parameters

Each remote function can be configured with additional parameters. Each parameter consists of a type, a name and optional default and test values. The name is used by the client application to define the value for the parameter that will be passed on when building the request for the remote function. In the image below, a query parameter is configured with the name tagged.

Double clicking inside the grid will activate the edit view for the selected function parameter.

Api Management - parameters

Four different types of parameters are currently supported.

Query

A query parameter will be sent as part of the query string. The query string is the part of the URI that comes after the question mark.

Note: only supported for remote functions of type HTTP Request

Form

A form parameter can be chosen for remote functions that are configured with the POST method. Form parameters are sent as form url encoded parameters in the body of the request.

Note: only supported for remote functions of type HTTP Request

Header

A header parameter can be used to send custom HTTP request headers when building the request for the remote function.

Note: only supported for remote functions of type HTTP Request

Variable

Variable parameters can be used to add custom variables to certain fields of a remote function. Remote functions of type HTTP Request can have custom variables in the Endpoint and Raw Data fields; Remote functions of type Amazon AWS Lambda can have custom variables in the Payload field. Also, each custom variable that is added to an Amazon AWS Lambda Remote Function will be passed along with the JSON payload as well.

For example, the following URI endpoint contains a single variable called userIdentifier: https://foo.bar/user/$userIdentifier. The variable will be replaced with the value that was passed on by the client application, before the actual request is executed.

9.3.2. Authentication

The endpoint of a remote function sometimes requires that the request is authenticated. The Authentication section provides three different authentication mechanisms that can be used together with a remote function.

Api Management - authentication
Basic Authentication

This will add a basic Authorization HTTP header when creating the request to the remote function. The username and password are both required.

OAuth 1.0 with Consumer Credentials

This authentication method will sign each request with the provided consumer key and secret using the HMAC-SHA1 signature method. See the OAuth 1.0 documentation on signing requests for more details: https://oauth.net/core/1.0a/#signing_process.

A token key and secret can be provided as well when necessary, but can be left empty if the endpoint only requires that the request must be signed with consumer credentials.

OAuth 2.0 with Password Credentials Grant

This authentication method will apply the Resource Owner Password Credentials authorization grant as defined in the OAuth 2.0 specificiation: https://tools.ietf.org/html/rfc6749#section-4.3.

When making a request to the defined endpoint of the remote function, it will first try to get an access token using the configuration details of the authentication method. The access token will then be passed along with the actual request to the endpoint of the remote function.

9.3.3. Calling Remote Functions

Testing

Each remote function can be tested from within Gluon Dashboard to ensure the configuration is valid. Each configured parameter has an optional test value that will be used when testing the remote function. When no test value is provided, the default value will be used instead.

When testing the remote function, the response of the endpoint will be printed so it can be verified against the expected value.

Call Log

Useful information from each call that is being invoked by a remote function is stored in the Call Log and can be accessed in Gluon Dashboard. Each request records the response code, the request and response timestamps and the body of the request and response. The body is capped at 8k.

Api Management - call log

For calling a remote function from the client application we use the RemoteFunctionBuilder. The RemoteFunctionBuilder can generate three different implementations of a RemoteFunction, each handling the response of the call to the remote function in their own way:

  • RemoteFunctionObject: the response is converted and wrapped into a GluonObservableObject

  • RemoteFunctionList: the response is converted into a list of objects, contained in a GluonObservableList

  • RemoteFunctionChunkedList: the response of the function is a continuous stream of chunks, where each chunk is converted and added to a GluonObservableList

Basic Remote Functions

To start calling remote functions, we first need to build an instance by using the RemoteFunctionBuilder builder class. Each built RemoteFunction instance can then be triggered by calling the call method.

// create an instance of remote function
RemoteFunctionList remoteFunction = RemoteFunctionBuilder.create("myFunction")
        .param("myParameter", "parameterValue")
        .list();

// call the remote function to fetch an observable list
GluonObservableList<MyResponse> responses = remoteFunction.call(MyResponse.class);
Local Caching

Every response that is returned by a call to a remote function will by default be cached locally in the private storage of the device. The next time the remote function is called, it will first load and return the cached data before making the actual call to the remote function in Gluon CloudLink. This allows the application to already present the user with data from the last time the remote function was called. When the response from the call to the actual remote function in Gluon CloudLink is completed, it will overwrite the cached data with the data from the new response.

Caching can be explicitly disabled when building the remote function:

// create an instance of remote function without caching locally
RemoteFunctionObject remoteFunction = RemoteFunctionBuilder.create("anotherFunction")
        .cachingEnabled(false)
        .object();

The cached data itself can also be cleared by using the clearCache() method:

// clear the locally cached data
remoteFunction.clearCache();

// the GluonObservableObject instance will no longer receive
// the cached data as it was cleared
GluonObservableObject<Sample> sample = remoteFunction.call(Sample.class);
Chunked Remote Functions

By default, Gluon CloudLink will close the call to the endpoint that is configured for a remote function after 60 seconds. However, the connection will be kept open when the remote function uses chunked transfer encoding. This is handled automatically, when the remote function specifies the Transfer-Encoding response header with the value chunked in its response. In other words, there is nothing special that needs to be configured in your Remote Function definition on Gluon Dashboard.

At the client side, you do need to use a different implementation of RemoteFunction that is able to handle chunked encoding: RemoteFunctionChunkedList.

// create an instance of a chunked remote function
RemoteFunctionChunkedList chunkedFunction = RemoteFunctionBuilder.create("chunkedFunction")
        .param("myParameter", "parameterValue")
        .chunkedList();

// call the remote function to fetch an observable list. Gluon CloudLink will add a
// new item to this list, for each chunk it receives from the remote function
GluonObservableList<MyChunk> responses = chunkedFunction.call(MyChunk.class);
Remote Functions with binary data

Writing binary data with a remote function can be done by defining the remote function in Gluon Dashboard with the raw body type. In the client application, a byte array is provided as the raw body when building the remote function.

// retrieve bytes from a classpath resource
byte[] rawBody = {};
try (ByteArrayOutputStream os = new ByteArrayOutputStream();
     InputStream is = Main.class.getResourceAsStream("/sample.wav")) {
    byte[] bytes = new byte[4096];
    int read;
    while ((read = is.read(bytes)) != -1) {
        os.write(bytes, 0, read);
    }
    rawBody = os.toByteArray();
} catch (IOException e) {
    // handle exception
}

// create an instance of remote function with raw bytes
RemoteFunctionObject playSoundFunction = RemoteFunctionBuilder.create("playSound")
        .rawBody(rawBody)
        .object();

// call the remote function passing on the raw bytes
GluonObservableObject<Void> response = playSoundFunction.call(Void.class);
response.stateProperty().addListener((obs, ov, nv) -> {
    if (nv == ConnectState.SUCCEEDED) {
        System.out.println("Sound uploaded successfully.");
    }
});

9.3.4. End to End Guide

You can find end to end guides on working with different cloud servers with Gluon Cloudlink below:

9.4. User Management

The User Management service enables user authentication in your Gluon Mobile application. It supports email and password based authentication and signing in with most popular identity providers like Facebook, Google and more. Each of these authentication types is called a Login Method in Gluon CloudLink.

Here is an overview of the login methods that are currently supported by Gluon CloudLink:

  • Facebook Login

  • Google Sign-In

  • Twitter

  • GitHub

  • Email and Password

9.4.1. Enabling Login Methods

Enabling the login methods that should be available for your application can be done from the Gluon Dashboard. Navigate to the User Management link, and select the Login Methods tab. From here you can add and configure one or more login methods.

View configured login methods

The login methods for identity providers all need a key and a secret from an application that is created on the respective identity provider. We provide a step-by-step guide to creating and configuring an application for each of the supported identity providers.

9.4.2. Applying Login Methods

UserClient

The UserClient class is the access point to the User Management service. It contains various methods for managing the authentication process. When a new instance is constructed, it will load the Login Methods that are enabled for the Gluon Mobile application and present them to the user when the authentication process is started.

Initiate authentication

A typical workflow to authenticate a user would be coded as follows:

UserClient userClient = new UserClient();
userClient.authenticate(user -> {
    System.out.println("User authenticated successfully: " + user.getName());
});

When no authenticated user exists yet, the authentication process will be started. This is handled by taking the user to the implementation of the AuthenticationView. The default AuthenticationView implementation looks like this:

App Sign in Methods

The user can select one of the presented login methods which will start the authentication flow for the selected login method. When a user was successfully authenticated, or when an authenticated user was already present, the provided consumer in the authenticate method will be called, passing in the authenticated user.

Handling failed or aborted authentication

In case the user aborted the authentication process or when an unexpected error occurred, you can use the authenticate variant which takes an extra failure consumer.

userClient.authenticate(user -> System.out.println("User authenticated successfully!"),
    failure -> System.out.println("Authentication failed or aborted: " + failure));
Signing out

Once a user is successfully authenticated, that user will not need to authenticate again the next time the application is started. To be able to restart the authentication process , you will first need to call the signOut method.

User

The authenticated user can be retrieved from the UserClient by calling the getAuthenticatedUser method. This returns an instance of User that has the following fields available:

  • key: the unique identifier of the user within the entire application

  • name: the full name of the user

  • nick: an optional nick name for the user

  • picture: an optional URL to the profile picture of the user

  • networkId: the identifier of the user at the identity provider that was used for authentication

Data Authentication

As mentioned in the Data Storage section, you can also use a UserClient to make sure that only authenticated users have access to data that is loaded by a DataClient. To enable this, you need to pass in an instance of UserClient when building the DataClient:

UserClient userClient = new UserClient();
DataClient dataClient = DataClientBuilder.create()
        .authenticateWith(userClient)
        .operationMode(OperationMode.CLOUD_FIRST)
        .build();

The first time that DataClient instance is used to access data, the authentication process will be initiated when an authenticated user was not yet present on the provided UserClient instance.

9.5. Media Management

The Media Management service is a central place to manage media resources that are used inside your mobile application. We distinguish the following different media types:

  • Media: refers to images, video and audio that can be used in the mobile application

  • Resource Bundles: refers to i18n resource bundle property files for translating application copy

9.5.1. Media

Each media resource is defined by a unique name. The client application will request the media resource by specifying that name. Each media is further made up of one or more media variants. The variant contains the actual media file, together with metadata to define for which platform the media should be made available. That way, it is for instance possible to define a different version of a media resource for Android and iOS.

Uploading Media

From the Gluon Dashboard, selecting the Media Management link will present you with a view that is divided into two grids. The top grid holds the media, while the bottom grid will show the media variants that are linked with the selected media from the top grid.

Add a new media resource by clicking the plus button from the top grid. This will create the media container as well as the first associated media variant. The following fields can be defined in the form dialog:

Media Name

This defines the unique name for the media resource. The client application will use this name to get a reference to the media resource.

Platform

Associates the media variant with a specific platform. When the client application requests a media resource and a specific variant exists that matches the platform where the application is running at, that media variant will be returned. Leave the platform empty to define the fallback resource that will be returned when no variant was found for a specific platform and/or platform version.

Platform Version

You can further specialize the media variant by defining a matching platform version. This media resource will only be returned when both the platform and the version of the platform where the application is running on match with the specified values.

Media File

This is the ultimate resource file that is linked with the media variant. It’s also this file that is returned to the client application in the form of an InputStream.

Loading Media Resources on the Mobile Client

The MediaClient is the class that you need to load media resources on the mobile client application. You only need to know the name of the media resource to load and call one of the two available functions:

  • loadMedia: loads an arbitrary media resource and returns it’s data as an InputStream

  • loadImage: loads an image media resource into a JavaFX Image

The following code snippet shows an example of how to show a media resource into a Glisten View:

public void initialize() {
    MediaClient mediaClient = new MediaClient();
    Image image = mediaClient.loadImage("backgroundImage");

    StackPane pane = new StackPane();
    pane.getChildren().add(image);

    setCenter(pane);
}

9.5.2. Resource Bundles

Resource Bundles are the standard way for providing i18n functionality in Java. Resources bundles are typically provided by one properties file or class for each supported language. These properties files or classes are then shipped together with your application package. With Gluon CloudLink we add the possibility to provide the resource bundles as an internet resource.

The resource bundle consists of a resource file for each supported locale. When adding a new resource bundle, you specify the locale that defines the associated language, country, script, etc. When the client application requests the resource bundle, it also passes down a locale so that Gluon CloudLink can return the resource bundle that matches the given locale.

Uploading Resource Bundles

Resource Bundles can be uploaded from the Gluon Dashboard. Navigate to Media Management and choose the Resource Bundles tab. Click the + button to add a new resource bundle. The resource bundles are grouped by their bundle name. The most common scenario is to create a resource bundle for each view in your mobile application.

Note: it is best practice to always provide a version of the resource bundle with an empty locale. This way, when no matching resource bundle could be found for a given locale, the resource bundle with the empty locale will be used as a fallback.

Loading Resource Bundles on the Mobile Client

We call the method loadResourceBundle on an instance of MediaClient to load a resource bundle from Gluon CloudLink. You can use the returned Resource Bundle to load it into your View.

public void initialize() {
    MediaClient mediaClient = new MediaClient();
    ResourceBundle resourceBundle = mediaClient.loadResourceBundle("com.gluonhq.sample.BasicView",
            Locale.getDefault());

    Label label = new Label(resourceBundle.getString("greeting"));

    StackPane pane = new StackPane();
    pane.getChildren().add(label);

    setCenter(pane);
}

9.6. Usage Analytics

The Usage Analytics service is responsible for gathering statistics on the devices that are running your Gluon Mobile application. That information can then be visualised and analysed from within the Gluon Dashboard web application.

The data that is being gathered contains information about the requests that the Gluon Mobile application makes to the Gluon CloudLink services. It also contains general information about the device itself, like the platform, the model, the version of the application, etc.

9.6.1. Applying Usage Analytics

UsageClient

To enable usage analytics in your Gluon Mobile application, you will need to call the enable method on the UsageClient. The method can be called at any time, but ideally it should be called as soon as the application is launched.

UsageClient usageClient = new UsageClient();
usageClient.enable();

This will trigger a call to the Gluon CloudLink Usage service that stores the general device information. The trigger will only be sent the first time, so any subsequent calls to the enable method will do nothing.

build.gradle

Don’t forget to add the Charm Down device plugin in the build.gradle file of your Gluon Mobile project.

...
jfxmobile {
    downConfig {
        version '3.6.0'
        plugins 'device', 'display', 'lifecycle', 'statusbar', 'storage'
    }
    ...
}

9.6.2. Gluon Dashboard

The Gluon Dashboard can be used to inspect the usage information that is being logged by the devices that are running your Gluon Mobile application. When the UsageClient is enabled as shown in the previous section, there is nothing else to configure in the Gluon Dashboard. By default, the data that is shown is gathered from the devices that were active during the past two weeks.

Usage Analytics Graph

9.7. Push Notifications

The Push Notifications service enables sending push notifications to the devices that installed your Gluon Mobile application. A push notification is a notification message that can be sent to the device, even when the application is not running. This can be used to unobtrusively notify a user that an application specific event has triggered.

9.7.1. Enabling Push Notifications

Gluon CloudLink uses Firebase Cloud Messaging (FCM) for handling push notifications over to Android devices and Apple Push Notification service (APNs) to get push notifications delivered to iOS devices. Both FCM and APNs require credentials for accessing their services.

9.7.2. Firebase Cloud Messaging

To be able to use push notifications in your Gluon Mobile application on android, you need the correct credentials for accessing Google’s FCM services.

Sign in to the Firebase Console with your Google Account and create a new project, or choose one of your existing projects to enable FCM for that project.

FCM - create or choose an app

Fill in the project name, click Continue.

FCM - set project name

The next step will allow enabling Google Analytics, which is optional. Press continue and wait until the project has been created.

FCM - project created

Press continue, and you will get to the project’s console.

FCM - project console

Let’s add Firebase support to an Android app, by selecting the Android icon. Fill in the package name of the android application. The package name should match the name of the package that is configured in Android Manifest of your Gluon Mobile application.

FCM - Android app

Click Register app to continue and then download the configuration file, it should be added to the src/main/resources folder of your project.

FCM - Configuration file

Press next twice (we don’t need to add the Firebase SDK, as Gluon Attach takes care of it), and finally press Continue to the console.

FCM - Continue

Back to the project’s console, in here you can configure which Google services should be enabled in the Google Application.

Select the settings icon to the right of Project Overview, and choose the Cloud Messaging service.

FCM - Cloud messaging credentials

A Server API Key and Sender ID are generated for the Android app.

Gluon Dashboard

Browse to the Gluon Dashboard, select the Push Notifications link and navigate to the Configuration tab. Paste the Server API Key into the textfield in the Android section at the top. Don’t forget to click the Save button to apply your changes.

GCM - enter credentials in Gluon Dashboard

9.7.3. iOS Push Notifications

There are two main steps required to enable push notifications on iOS devices: First, get a valid p12 certificate, that will be required by the Dashboard, and second, get a valid provisioning profile to sign the app that will receive the notifications.

In both cases you need a valid account in the Apple Developer portal. More information about iOS notifications can be found here.

Certificate p12

Go to the Apple Developer portal, sign in with your credentials, and create a certificate for your app following these steps:

  • Select Certificates, Identifiers and Profiles

  • Under Identifiers → App IDs, click the + button to register your app.

    • Under App ID Description: provide a valid name, i.e: Push Notes App.

    • Under App ID Prefix: you should see your team ID.

    • Under Explicit App ID, Bundle ID: you have to put the exact same bundle of your app as the one you’ll find in the info.plist file, i.e: com.gluonhq.pushnotes.PushNotes.

App ID Description and Bundle ID
  • Under App Services, select push notifications and click Continue.

App Services
  • Confirm you App ID: make sure your App ID information is correct, and click the Register button.

Confirm App ID
  • Registration complete. Click Done.

  • Under Identifiers → App IDs, now the created app ID it will be listed. Click on it, and at the end of the expanded info click on the Edit button.

Edit App Configuration
  • Go to the Push Notifications section and click on Create Certificate for develpment, production or both.

Create Certificate for Push Notifications
  • Follow the instructions on how to generate a Certificate Signing Request (CSR) file on your Mac.

    • Open Keychain Access app and select Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority.

    • Add your email, name, select Save to disk, click continue.

    • Save the file, i.e. CSR_PushNotes.certSigningRequest.

  • Upload the CSR file

    • At the Developer center again, click on Continue, click on the Choose File…​ button, and submit your file.

Submit CSR file
  • Download the certificate aps_development.cer and double click on it to install the certificate in Keychain Access.

Download cer file
  • Once Keychain Access shows the certificate, expand it, select it and your name, and right click, selecting Export 2 items, and save the file on your disk, i.e. PushNotes_Certificate.p12 providing a password for it.

Export Items in Keychain Access
  • Important note: This file and the password will be required later on the push notifications configuration tab of the Gluon Dashboard.

Add p12 to Dashboard
Provisioning profile

One last step is required to be able to sign your app: get a provisioning profile for it.

  • Back again to the Apple Developer Center, under Provisioning Profiles (Development or Distribution), click on the + button to add a new profile.

    • Select iOS App Development or App Store, and press Continue.

    • Select the App ID from the combobox, and press Continue.

Select App ID
  • Select the certificates you wish to include in this provisioning profile, and press Continue. These will be used later (iosSignIdentity).

  • Select the devices you wish to include in this provisioning profile, and press Continue.

  • Give a name to the profile, and press Continue

  • The provisioning profile is ready to be downloaded.

Download Provisioning Profile
Gluon Dashboard

When you’ve managed to prepare your application for both FCM and APNs, you can configure your Gluon Mobile application in the Gluon Dashboard. Select the Push Notifications link from the menu, go the Configuration tab, where you can enter the FCM Server API key and upload your APNs certificate.

Push Notifications Configuration

With the proper certificates, the Push Notifications tab can be used to send a push notification. Clicking on the + button will pop up a dialog from which you can enter the details of the notification.

Push Notifications Dialog

The checkbox labelled invisible can be selected to send silent push notifications to the user, without a visible notification. The Runtime Args service will be able to process it and execute certain action.

9.7.4. Using Push Notifications from Enterprise

REST endpoint
Send Push Notification

Send a new push notification to the defined target devices.

Form Parameters
Name Type Description Default

body

string

the body of the push notification

deliveryDate

long

Note: this parameter is currently ignored

0

expirationAmount

integer

the amount used in conjunction with the expiration type

4

expirationType

DAYS, HOURS, MINUTES, WEEKS

the type of the expiration amount

WEEKS

identifier

string

a custom identifier to be sent along with the push notification

invisible

boolean

specify true to send a silent push notification

false

priority

HIGH, NORMAL

The priority for the push notification.

HIGH

targetDeviceToken

string

The device token to use as the target for the push notification. Only used when the target type is SINGLE_DEVICE

targetTopic

string

The name of the topic to use as the target for the push notification. Only used when the target type is TOPIC.

targetType

ALL_DEVICES, SINGLE_DEVICE, TOPIC

The target of the push notification.

ALL_DEVICES

title

string

the title of the push notification

Curl sample
curl -X POST "https://cloud.gluonhq.com/3/push/enterprise/notification" \
     -H "Authorization: Gluon YOUR_SERVER_KEY"
     --data "body=Sample%20Body" \
     --data "title=Sample%20Title" \
     --data "expirationType=HOURS" \
     --data "expirationAmount=1" \
     --data "priority=HIGH" \
     --data "invisible=false" \
     --data "targetType=TOPIC" \
     --data "targetTopic=Topic1"
Java Client
CloudLinkConfig config = new CloudLinkConfig(YOUR_SERVER_KEY);
CloudLinkClient client = new CloudLinkClient(config);

PushNotification pushNotification = new PushNotification();
pushNotification.setBody("Sample Body");
pushNotification.setTitle("Sample Title");
pushNotification.setExpirationType(PushNotification.ExpirationType.HOURS);
pushNotification.setExpirationAmount(1);
pushNotification.setPriority(PushNotification.Priority.HIGH);
pushNotification.getTarget().setType(PushNotificationTarget.Type.TOPIC);
pushNotification.getTarget().setTopic("Topic1");
client.sendPushNotification(pushNotification);

9.7.5. Applying Push Notifications in Mobile Client

PushClient

To activate push notifications on your Gluon Mobile application, you will need a reference to the PushClient and call the enable method.

PushClient pushClient = new PushClient();
pushClient.enable(); // enable push notifications

This will trigger a message on an iOS device asking the user to confirm that push notifications are allowed for the application. On Android, if the user has not yet installed or activated Google Play services, a message will be shown to ask for permission to install and/or activate Google Play services on the device.

pom.xml

Don’t forget to add the Attach device, push-notifications and runtime-args services in the pom.xml file of your Gluon Mobile project.

pom.xml
<dependencies>
    ...
    <dependency>
        <groupId>com.gluonhq.attach</groupId>
        <artifactId>device</artifactId>
        <version>${attach.version}</version>
    </dependency>
    <dependency>
        <groupId>com.gluonhq.attach</groupId>
        <artifactId>push-notifications</artifactId>
        <version>${attach.version}</version>
    </dependency>
    <dependency>
        <groupId>com.gluonhq.attach</groupId>
        <artifactId>runtime-args</artifactId>
        <version>${attach.version}</version>
    </dependency>
    ...
</dependencies>
...
<plugin>
    <groupId>com.gluonhq</groupId>
    ...
    <configuration>
        ...
        <target>${clientTarget}</target>
        <attachList>
            <list>device</list>
            <list>display</list>
            <list>lifecycle</list>
            <list>push-notifications</list>
            <list>runtime-args</list>
            ...
    ...
</plugins>
Android configuration

For Android, we need to add the google-services.json file to the src/main/resources folder. This file can be downloaded from the project’s Firebase console.

iOS configuration

First make sure to configure your app so it can use Push notifications in the Apple Developer Center. You can follow this step-by-step guide.

As signing identity, use the certificate selected during the provisioning profile generation.

When deploying to your iOS device, the provisioning profile will be downloaded. If this is not the case, you still can install it manually: Open Xcode, connect your device, go to Window → Device, and at the bottom left there is a small engine icon, click to see the installed provisioning profiles, and then click on the + button to add this one.

Note that this provisioning profile contains the entitlements that match those installed within the app, required to enable push notifications.

The apsEnvironment property needs to match the match the used environment (development or production).

Build your app

When you’ve configured your project, all you have to do is building your app as usual with mvn -Pandroid client:build client:package for Android or mvn -Pios client:build client:package for iOS.

10. Samples

Gluon has curated a list of samples to help you get started with our platform. These samples include every thing starting from client to cloudlink. We walk through the process of creation of each of these samples to make it easier for you to work with them.

Please check our Samples page for more information.

11. Scene Builder

The latest version of Scene Builder can be downloaded from the Gluon website.

Open Source

Scene Builder is open-sourced, and licensed under the BSD-3 license. Its source code is hosted under Gluon organization in Github.

11.1. Installation

Download the correct installer for your platform from the Scene Builder download page. The installation details are different for each platform but should be straight-forward.

As of Scene Builder 8.3.0, the Windows installer will let you choose the installation folder, but please note that you will need to run as administrator (right-click, run as administrator) if you want to install in a system-wide location.

11.2. Library Manager

Up until Scene Builder version 8.1.1, custom controls could be added with the menu button available at the Library panel. Selecting the option Import JAR/FXML File…​ from the drop down menu allowed the user browsing locally for jar or FXML files in his file system. Those files were added to the Custom Library Folder, a local folder that the user could access and manually add more files or remove them.

0. Open SB - Library Button - Former mode

From Scene Builder version 8.2.0, a new function was added, allowing the user not only to import local jar or FXML files as before, but also to import jars from a number of repositories, remote and local, public and even private ones.

To import libraries either from disk or from repositories, the menu item JAR/FXML Manager gives access to a new dialog, and replaces the old menu item.

1. Open SB - Library Button

11.2.1. The Library Manager dialog

The top part of the dialog contains a list of the existing libraries (jars and FXML files), if any.

2. Select Library Manager

For each library, the user can edit or delete it. Editing an FXML will open it in Scene Builder. Editing a jar file will open a dialog where the user can preview and select or unselect different components (if any) of that jar that will be added to the Custom panel.

On the lower part of the dialog, there are different actions that the user can perform.

Search Repositories

The user can type a name of a library: full name or part of its group id, or full name or part of it of the artifact id, following the usual naming convention.

For instance, if the user wants to download and install the latest release of com.gluonhq:charm, he can search just for charm:

3.0 New Search Repository option

All the results listed from all possible artifacts with that keyword, are retrieved from a given set of repositories:

  • Maven Central

  • Jcenter

  • Sonatype (releases)

  • Gluon Nexus (releases)

  • Local M2 (releases)

The results don’t include the version number, and this is only retrieved if the artifact is finally installed.

Note: Only latest releases will be resolved with this option. For any other version, including snapshots, the user has to use the option Manually Add Libraries from repository (see below).

By selecting the desired artifact, and clicking Add JAR, the artifact will be resolved from one of the repositories, downloading the latest release version, and installing it into the local M2 repository.

3.1 Resolve artifact

Then the jar will be scanned and a dialog will show any component that can be added to the Custom panel, typically classes that are assignable from Node. The user can select or unselect those components that should be added or excluded from the Custom panel. Also, some components can be previewed. The user has to click on Import Components to finish the process.

3.2 Select components

Once the dialog is closed, the new library will show up on the list. Again, it can be edited, to modify the list of included components, or deleted, to remove the jar and its components from the Custom panel.

3.3 Installed artifact

Note: When an artifact is removed from the list, it will just be removed from Scene Builder, but the artifact won’t be removed from the local M2 repository.

Manually add Library from repository

If the user knows the exact names of groupID and artifactID, he can select from all the available versions for that artifact, including snapshots, from an initial set of repositories:

  • Maven Central

  • Jcenter

  • Sonatype (releases and snapshots)

  • Gluon Nexus (releases)

  • Local M2 (releases and snapshots)

that can be extended as it will be shown later.

4.0 Manually search for groupID and artifactID

Once a version is selected, clicking Add JAR will resolve the artifact, downloading and installing it into the local M2 repository.

4.1. Select version and install

The jar will be scanned and available components can be imported, after the user clicks on Import Components.

4.2. Select components
Add Library/FXML from file system

This option allows the user browsing locally for jar or FXML files, as in previous versions of Scene Builder. The files will be installed in the Custom Library Folder.

Manage Repositories

Finally, the user can manage the repositories where artifacts are resolved from.

5.0. Repository Manager

Initially, the preset list of remote repositories is listed. These are not editable and can’t be removed.

By clicking on Add Repository, the user can add new repositories to the list, both public or private.

A new repository requires a name and a valid URL. If it is private, the credentials are required as well. The test button will perform a connection to the given URL to check if it is valid or not, and if private, if the credentials are valid as well.

5.1. Add a new private repository

Note: The credentials will be stored locally in the user preferences. They will be used only when installing libraries from the private repository.

Finally, the user repository will be added to the list, with the possibility of edition or deletion.

5.2. It will be listed

11.3. Custom Panel

Finally, all the components imported from either local or remote repositories or from the local file system, will be listed in the Custom panel, ready to be used.

6. Components ready to drag and drop

11.4. Developing Applications Using Gluon

11.4.1. Gluon Panel

Since Scene Builder 8.3.0, a Gluon panel is included by default to support the easy creation of Gluon based apps using Scene Builder and Gluon Mobile.

When using Gluon controls, don’t forget to use the Gluon Mobile preview theme as shown in the screenshot below, or controls might not work correctly.

6. Components ready to drag and drop

As of Scene Builder 8.4.0 you can also preview your controls using a Gluon Swatch and Theme. The theme can either be Gluon Mobile Light or Gluon Mobile Dark, the Swatch is the primary color used, it is what affects the appearance of the controls the most. In your code you should then use the Swatch and Theme API to effectively have the same appearance.

6. Components ready to drag and drop

11.4.2. Designing Screens

With Gluon a screen is typically created using a View. That View is usually composed of an AppBar at the top and some content at the center.

When creating Views don’t forget to set their name.

If you want to create your View in FXML with an AppBar also defined, you should disable the globally provided AppBar in your code by toggling its visibility:

MobileApplication.getInstance().getAppBar().setVisible(false);

View also has a bottom property, for instance you can set a BottomNavigation on that position.

7. View with AppBar and BottomNavigation

11.4.3. Testing Your Designs In Different Form Factors

Scene Builder 8.4.0 brings with it the possibility of testing your designs against different form factors. To this effect you have several options:

8. Defining different form factors for the root container

As you can see on the previous image there are sizes for Desktop, Tablet and Mobile Phones. The default size can be changed in the Preferences window.

If you want to preview your design live, you can by selecting the Preview → Show Preview in Window (or Ctrl + P) menu item. This will open up a window with a Scene containing the FXML you defined.

With the preview window open you can change it’s size by dragging the window borders or to a specific value from the menu:

8. Changing the size of the Preview Window

12. Conclusion

In case you have any questions, your first stop should be the Gluon support page, where you can access the latest samples, documentation and more. Gluon also recommends the community to support each other over at the Gluon StackOverflow page. Finally, Gluon offers commercial support as well, to kickstart your projects.

We encourage you to start developing new projects using Gluon offering. We can’t wait to see what you create!