Gluon Mobile enables Java developers to create truly native mobile applications for iOS and Android using JavaFX. This comprehensive guide covers building, testing, and deploying mobile apps with Gluon Mobile.
Gluon Mobile Architecture Overview
Key Components
- Gluon Mobile: Cross-platform mobile framework
- Gluon VM: Java Virtual Machine for mobile platforms
- JavaFX Ports: Mobile-optimized JavaFX implementation
- Charm Down: Cross-platform services and APIs
Project Setup
Maven Configuration
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mobile-app</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<gluon.mobile.version>4.0.18</gluon.mobile.version>
<gluonfx.maven.plugin.version>1.0.22</gluonfx.maven.plugin.version>
</properties>
<dependencies>
<!-- Gluon Mobile -->
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>charm-glisten-6</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<!-- Charm Down Services -->
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>display</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>storage</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>util</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>lifecycle</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>battery</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>push-notifications</artifactId>
<version>${gluon.mobile.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>${gluonfx.maven.plugin.version}</version>
<configuration>
<target>ios</target> <!-- or android -->
<mainClass>com.example.mobileapp.MobileApp</mainClass>
<attachList>
<list>display</list>
<list>lifecycle</list>
<list>storage</list>
<list>battery</list>
<list>push-notifications</list>
</attachList>
</configuration>
</plugin>
</plugins>
</build>
</project>
Gradle Configuration
// build.gradle
plugins {
id 'java'
id 'application'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.22'
}
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
maven {
url 'https://nexus.gluonhq.com/nexus/content/repositories/releases/'
}
}
dependencies {
implementation 'com.gluonhq:charm-glisten-6:4.0.18'
implementation 'com.gluonhq.attach:display:4.0.18'
implementation 'com.gluonhq.attach:storage:4.0.18'
implementation 'com.gluonhq.attach:util:4.0.18'
implementation 'com.gluonhq.attach:lifecycle:4.0.18'
implementation 'com.gluonhq.attach:battery:4.0.18'
}
mainClassName = 'com.example.mobileapp.MobileApp'
gluonfx {
target = 'ios' // or 'android'
attachList {
compile 'display', 'storage', 'lifecycle', 'battery'
}
}
Basic Mobile Application Structure
Main Application Class
package com.example.mobileapp;
import com.gluonhq.charm.glisten.application.MobileApplication;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.Swatch;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MobileApp extends MobileApplication {
@Override
public void init() {
// Register views
addViewFactory(HOME_VIEW, () -> new HomeView().getView());
addViewFactory(SETTINGS_VIEW, () -> new SettingsView().getView());
addViewFactory(PROFILE_VIEW, () -> new ProfileView().getView());
// Create drawer menu
Drawer drawer = new Drawer();
drawer.getMenuItems().addAll(
new MenuItem("Home", MaterialDesignIcon.HOME.graphic(), HOME_VIEW),
new MenuItem("Profile", MaterialDesignIcon.PERSON.graphic(), PROFILE_VIEW),
new MenuItem("Settings", MaterialDesignIcon.SETTINGS.graphic(), SETTINGS_VIEW)
);
}
@Override
public void postInit(Scene scene) {
// Apply mobile-optimized styles
Swatch.BLUE.assignTo(scene);
// Mobile-specific scene setup
scene.getStylesheets().add(MobileApp.class.getResource("mobile.css").toExternalForm());
// Set up navigation
((Window) scene.getWindow()).getNavPane().setVisible(true);
}
// View constants
public static final String HOME_VIEW = "Home";
public static final String PROFILE_VIEW = "Profile";
public static final String SETTINGS_VIEW = "Settings";
}
Home View with Mobile UI Components
package com.example.mobileapp.views;
import com.gluonhq.charm.glisten.control.*;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
public class HomeView {
private final View view;
private final AppBar appBar;
public HomeView() {
// Create main view
view = new View();
// Setup app bar
appBar = MobileApplication.getInstance().getAppBar();
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e ->
MobileApplication.getInstance().showLayer("Drawer")));
appBar.setTitleText("Home");
appBar.getActionItems().add(
MaterialDesignIcon.SEARCH.button(e -> showSearch())
);
// Create content
VBox content = createContent();
view.setCenter(content);
}
private VBox createContent() {
VBox content = new VBox(20);
content.setAlignment(Pos.TOP_CENTER);
content.setPadding(new Insets(20));
// Welcome card
Card welcomeCard = new Card();
welcomeCard.setMaxWidth(400);
VBox cardContent = new VBox(10);
cardContent.setPadding(new Insets(15));
Label welcomeLabel = new Label("Welcome to Mobile App!");
welcomeLabel.setStyle("-fx-font-size: 18px; -fx-font-weight: bold;");
Label subtitle = new Label("Built with JavaFX and Gluon Mobile");
subtitle.setTextFill(Color.GRAY);
cardContent.getChildren().addAll(welcomeLabel, subtitle);
welcomeCard.setContent(cardContent);
// Action buttons
FloatingActionButton fab = new FloatingActionButton(
MaterialDesignIcon.ADD.text,
e -> createNewItem()
);
fab.showOn(view);
// Stats section
Tile statsTile = new Tile();
statsTile.setMaxWidth(400);
statsTile.setText("Today's Stats");
statsTile.setGraphic(MaterialDesignIcon.TRENDING_UP.graphic());
// Recent activity list
ListTile<String> activityList = new ListTile<>();
activityList.setMaxWidth(400);
activityList.setTitle("Recent Activity");
activityList.setItems(
"Completed task A",
"Started project B",
"Updated profile",
"Received message"
);
content.getChildren().addAll(welcomeCard, statsTile, activityList);
return content;
}
private void showSearch() {
// Show search layer
MobileApplication.getInstance().showLayer("SearchLayer");
}
private void createNewItem() {
// Handle FAB click
Dialog<String> dialog = new Dialog<>("Create New Item", "Enter item name:");
dialog.setOnConfirm(e -> {
if (!dialog.getTextInput().isEmpty()) {
// Save new item
showMessage("Item created: " + dialog.getTextInput());
}
});
dialog.showAndWait();
}
private void showMessage(String message) {
MobileApplication.getInstance().showMessage(message);
}
public View getView() {
return view;
}
}
Mobile-Optimized UI Components
Responsive Layout with Navigation
package com.example.mobileapp.views;
import com.gluonhq.charm.glisten.control.*;
import com.gluonhq.charm.glisten.layout.layer.SidePopupView;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
public class ProfileView {
private final View view;
private final ObservableList<ProfileItem> profileItems;
public ProfileView() {
this.profileItems = FXCollections.observableArrayList(
new ProfileItem("Personal Info", MaterialDesignIcon.INFO),
new ProfileItem("Security", MaterialDesignIcon.SECURITY),
new ProfileItem("Notifications", MaterialDesignIcon.NOTIFICATIONS),
new ProfileItem("Privacy", MaterialDesignIcon.LOCK),
new ProfileItem("Help", MaterialDesignIcon.HELP)
);
view = new View();
setupAppBar();
setupContent();
}
private void setupAppBar() {
AppBar appBar = MobileApplication.getInstance().getAppBar();
appBar.setNavIcon(MaterialDesignIcon.ARROW_BACK.button(e ->
MobileApplication.getInstance().switchView(HomeView.HOME_VIEW)));
appBar.setTitleText("Profile");
appBar.getActionItems().add(
MaterialDesignIcon.EDIT.button(e -> editProfile())
);
}
private void setupContent() {
VBox content = new VBox();
content.setPadding(new Insets(20));
// User info card
Avatar userAvatar = new Avatar(80, "https://example.com/avatar.jpg");
userAvatar.setOnMouseClicked(e -> changeAvatar());
Label userName = new Label("John Doe");
userName.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
Label userEmail = new Label("[email protected]");
userEmail.setStyle("-fx-text-fill: gray;");
VBox userInfo = new VBox(5, userAvatar, userName, userEmail);
userInfo.setAlignment(Pos.CENTER);
// Settings list
ListView<ProfileItem> settingsList = new ListView<>(profileItems);
settingsList.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(ProfileItem item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.getName());
setGraphic(item.getIcon().graphic());
}
}
});
settingsList.setOnMouseClicked(e -> {
ProfileItem selected = settingsList.getSelectionModel().getSelectedItem();
if (selected != null) {
handleSettingSelection(selected);
}
});
content.getChildren().addAll(userInfo, settingsList);
view.setCenter(content);
}
private void editProfile() {
// Show edit profile layer
SidePopupView editView = new SidePopupView(new EditProfileLayer());
MobileApplication.getInstance().addLayerFactory("EditProfile", () -> editView);
MobileApplication.getInstance().showLayer("EditProfile");
}
private void changeAvatar() {
// Handle avatar change
// Implementation for camera/gallery access
}
private void handleSettingSelection(ProfileItem item) {
switch (item.getName()) {
case "Personal Info":
showPersonalInfo();
break;
case "Security":
showSecuritySettings();
break;
case "Notifications":
showNotificationSettings();
break;
// ... other cases
}
}
private void showPersonalInfo() {
MobileApplication.getInstance().switchView("PersonalInfoView");
}
// ... other methods
public View getView() {
return view;
}
// Profile item data class
private static class ProfileItem {
private final String name;
private final MaterialDesignIcon icon;
public ProfileItem(String name, MaterialDesignIcon icon) {
this.name = name;
this.icon = icon;
}
public String getName() { return name; }
public MaterialDesignIcon getIcon() { return icon; }
}
}
Charm Down Services Integration
Device Services Usage
package com.example.mobileapp.services;
import com.gluonhq.attach.battery.BatteryService;
import com.gluonhq.attach.battery.impl.DummyBatteryService;
import com.gluonhq.attach.display.DisplayService;
import com.gluonhq.attach.lifecycle.LifecycleService;
import com.gluonhq.attach.storage.StorageService;
import com.gluonhq.attach.util.Services;
import javafx.beans.property.*;
import java.io.File;
import java.util.Optional;
public class DeviceService {
private final DoubleProperty batteryLevel = new SimpleDoubleProperty(0);
private final BooleanProperty isCharging = new SimpleBooleanProperty(false);
private final StringProperty deviceOrientation = new SimpleStringProperty("PORTRAIT");
public DeviceService() {
setupBatteryMonitoring();
setupDisplayMonitoring();
setupLifecycleHandling();
}
private void setupBatteryMonitoring() {
Services.get(BatteryService.class).ifPresent(service -> {
service.batteryLevelProperty().addListener((obs, oldVal, newVal) -> {
batteryLevel.set(newVal.doubleValue());
});
service.chargingProperty().addListener((obs, oldVal, newVal) -> {
isCharging.set(newVal);
});
// Initial values
batteryLevel.set(service.getCurrentBatteryLevel());
isCharging.set(service.isCharging());
});
}
private void setupDisplayMonitoring() {
Services.get(DisplayService.class).ifPresent(service -> {
service.orientationProperty().addListener((obs, oldVal, newVal) -> {
deviceOrientation.set(newVal.name());
handleOrientationChange(newVal);
});
});
}
private void setupLifecycleHandling() {
Services.get(LifecycleService.class).ifPresent(service -> {
service.addListener(LifecycleEvent.PAUSE, () -> {
// App is going to background
saveAppState();
});
service.addListener(LifecycleEvent.RESUME, () -> {
// App is coming to foreground
restoreAppState();
});
});
}
private void handleOrientationChange(DisplayService.Orientation orientation) {
switch (orientation) {
case LANDSCAPE:
// Adjust UI for landscape
break;
case PORTRAIT:
// Adjust UI for portrait
break;
}
}
private void saveAppState() {
// Save to local storage
Services.get(StorageService.class).ifPresent(storage -> {
Optional<File> privateStorage = storage.getPrivateStorage();
privateStorage.ifPresent(file -> {
// Save application state
});
});
}
private void restoreAppState() {
// Restore from local storage
}
// Public API
public DoubleProperty batteryLevelProperty() { return batteryLevel; }
public BooleanProperty chargingProperty() { return isCharging; }
public StringProperty orientationProperty() { return deviceOrientation; }
public double getBatteryLevel() { return batteryLevel.get(); }
public boolean isCharging() { return isCharging.get(); }
public String getOrientation() { return deviceOrientation.get(); }
}
Storage Service Example
package com.example.mobileapp.services;
import com.gluonhq.attach.storage.StorageService;
import com.gluonhq.attach.util.Services;
import com.google.gson.Gson;
import java.io.*;
import java.util.Optional;
public class DataStorageService {
private static final Gson GSON = new Gson();
public <T> void saveData(String key, T data) {
Services.get(StorageService.class).ifPresent(storage -> {
Optional<File> privateStorage = storage.getPrivateStorage();
privateStorage.ifPresent(storageDir -> {
File dataFile = new File(storageDir, key + ".json");
try (Writer writer = new FileWriter(dataFile)) {
GSON.toJson(data, writer);
} catch (IOException e) {
System.err.println("Failed to save data: " + e.getMessage());
}
});
});
}
public <T> Optional<T> loadData(String key, Class<T> type) {
Optional<File> privateStorage = Services.get(StorageService.class)
.flatMap(StorageService::getPrivateStorage);
if (privateStorage.isPresent()) {
File dataFile = new File(privateStorage.get(), key + ".json");
if (dataFile.exists()) {
try (Reader reader = new FileReader(dataFile)) {
T data = GSON.fromJson(reader, type);
return Optional.ofNullable(data);
} catch (IOException e) {
System.err.println("Failed to load data: " + e.getMessage());
}
}
}
return Optional.empty();
}
public void deleteData(String key) {
Services.get(StorageService.class).ifPresent(storage -> {
Optional<File> privateStorage = storage.getPrivateStorage();
privateStorage.ifPresent(storageDir -> {
File dataFile = new File(storageDir, key + ".json");
if (dataFile.exists()) {
dataFile.delete();
}
});
});
}
}
Push Notifications
Push Notification Setup
package com.example.mobileapp.services;
import com.gluonhq.attach.push.PushNotification;
import com.gluonhq.attach.push.PushNotificationsService;
import com.gluonhq.attach.util.Services;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class NotificationService {
private final StringProperty lastNotification = new SimpleStringProperty();
public NotificationService() {
setupPushNotifications();
}
private void setupPushNotifications() {
Services.get(PushNotificationsService.class).ifPresent(service -> {
// Register for push notifications
service.register().ifPresent(token -> {
System.out.println("Push token: " + token);
// Send token to your server
sendTokenToServer(token);
});
// Handle received notifications
service.setNotificationHandler(this::handleNotification);
});
}
private void handleNotification(PushNotification notification) {
// Update UI on JavaFX Application Thread
javafx.application.Platform.runLater(() -> {
lastNotification.set(notification.getTitle() + ": " + notification.getBody());
// Show local notification
showLocalNotification(notification);
// Update app state based on notification
handleNotificationContent(notification);
});
}
private void showLocalNotification(PushNotification notification) {
// Use Gluon's notification API or create custom notification UI
MobileApplication.getInstance().showMessage(
notification.getTitle() + "\n" + notification.getBody()
);
}
private void handleNotificationContent(PushNotification notification) {
// Handle different notification types
String type = notification.getData().get("type");
if ("NEW_MESSAGE".equals(type)) {
// Refresh messages
refreshMessages();
} else if ("UPDATE_AVAILABLE".equals(type)) {
// Prompt for update
showUpdatePrompt();
}
}
private void sendTokenToServer(String token) {
// Implement server communication
// This would typically make an HTTP request to your backend
}
private void refreshMessages() {
// Refresh message list
}
private void showUpdatePrompt() {
// Show update available dialog
}
public StringProperty lastNotificationProperty() {
return lastNotification;
}
}
Mobile-Specific Styling
CSS for Mobile Optimization
/* mobile.css */
/* Root styles for mobile */
.root {
-fx-font-family: "Roboto", "San Francisco", "Helvetica Neue", sans-serif;
-fx-font-size: 14px;
}
/* App Bar styling */
.app-bar {
-fx-background-color: #2196F3;
-fx-text-fill: white;
}
.app-bar .title {
-fx-font-size: 18px;
-fx-font-weight: bold;
}
/* Card components */
.card {
-fx-background-color: white;
-fx-background-radius: 8px;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 8, 0, 0, 2);
-fx-padding: 0;
}
.card:hover {
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.15), 12, 0, 0, 3);
}
/* Buttons optimized for touch */
.button {
-fx-background-radius: 25px;
-fx-padding: 12px 24px;
-fx-font-size: 16px;
-fx-min-height: 44px; /* Minimum touch target size */
-fx-min-width: 44px;
}
.primary-button {
-fx-background-color: #2196F3;
-fx-text-fill: white;
}
.secondary-button {
-fx-background-color: transparent;
-fx-border-color: #2196F3;
-fx-border-width: 2px;
-fx-text-fill: #2196F3;
}
/* List cells for touch */
.list-cell {
-fx-padding: 15px;
-fx-min-height: 44px;
}
.list-cell:filled:selected {
-fx-background-color: #E3F2FD;
}
/* Text fields optimized for mobile */
.text-field, .text-area {
-fx-background-radius: 8px;
-fx-border-radius: 8px;
-fx-padding: 12px;
-fx-font-size: 16px; /* Prevents zoom on iOS */
}
/* Responsive layouts */
@media (max-width: 600px) {
.container {
-fx-padding: 10px;
}
.card {
-fx-margin: 5px;
}
}
/* Dark mode support */
.root:darker {
-fx-background-color: #121212;
-fx-text-fill: #FFFFFF;
}
.root:darker .card {
-fx-background-color: #1E1E1E;
-fx-text-fill: #FFFFFF;
}
Building and Deployment
GluonFX Plugin Configuration
<!-- Extended gluonfx-maven-plugin configuration --> <plugin> <groupId>com.gluonhq</groupId> <artifactId>gluonfx-maven-plugin</artifactId> <version>1.0.22</version> <configuration> <target>ios</target> <mainClass>com.example.mobileapp.MobileApp</mainClass> <verbose>true</verbose> <runtimeArgs> <arg>-Xmx2G</arg> </runtimeArgs> <appIdentifier>com.example.mobileapp</appIdentifier> <appName>Mobile App</appName> <bundleName>MobileApp</bundleName> <version>1.0.0</version> <icon> <ios>src/main/resources/ios/Images.xcassets/AppIcon.appiconset</ios> <android>src/main/resources/android/icon</android> </icon> <attachList> <list>display</list> <list>lifecycle</list> <list>storage</list> <list>battery</list> <list>push-notifications</list> <list>util</list> </attachList> <reflectionList> <list>com.example.mobileapp.**</list> <list>com.gluonhq.charm.glisten.**</list> </reflectionList> <graalArgs> <arg>--allow-incomplete-classpath</arg> <arg>--initialize-at-build-time=com.sun.javafx</arg> </graalArgs> </configuration> </plugin>
Build Commands
# Build for desktop (testing) mvn clean javafx:run # Build native image for iOS mvn clean gluonfx:build # Build for Android mvn clean gluonfx:build -Dgluonfx.target=android # Run on device mvn gluonfx:nativerun # Create IPA/APK package mvn gluonfx:package
Testing on Desktop and Devices
Desktop Launcher for Development
package com.example.mobileapp;
import com.gluonhq.charm.glisten.application.AppManager;
import javafx.application.Application;
import javafx.stage.Stage;
public class DesktopLauncher extends Application {
@Override
public void start(Stage primaryStage) {
// Initialize mobile services for desktop
setupDesktopServices();
// Launch the mobile app
MobileApp mobileApp = new MobileApp();
mobileApp.init();
Scene scene = new Scene(new StackPane());
mobileApp.postInit(scene);
primaryStage.setScene(scene);
primaryStage.setTitle("Mobile App - Desktop");
primaryStage.setWidth(414); // iPhone width
primaryStage.setHeight(736); // iPhone height
primaryStage.show();
}
private void setupDesktopServices() {
// Mock services for desktop development
// This allows testing without mobile device
}
public static void main(String[] args) {
launch(args);
}
}
Best Practices
Performance Optimization
package com.example.mobileapp.optimization;
import javafx.animation.AnimationTimer;
import javafx.scene.Node;
import javafx.scene.image.Image;
public class PerformanceOptimizer {
// Image caching
private static final Map<String, Image> imageCache = new HashMap<>();
public static Image getCachedImage(String url) {
return imageCache.computeIfAbsent(url, Image::new);
}
// Lazy loading for lists
public static <T> void setupLazyLoading(ListView<T> listView,
Supplier<List<T>> dataLoader) {
listView.getProperties().put("lazyLoader", dataLoader);
listView.setOnScroll(e -> {
if (e.getDeltaY() < 0) { // Scrolling down
loadMoreData(listView);
}
});
}
// Memory management
public static void cleanupUnusedResources() {
imageCache.entrySet().removeIf(entry -> {
// Remove images not used recently
return isImageUnused(entry.getKey());
});
}
// Battery optimization
public static void optimizeForBattery() {
// Reduce animations when battery is low
// Use simpler rendering when possible
}
}
Conclusion
Gluon Mobile provides a robust solution for Java developers to build native mobile applications with:
- True native performance through GraalVM Native Image
- Cross-platform development for iOS and Android
- Full JavaFX capabilities optimized for mobile
- Access to native device features via Charm Down
- Modern mobile UI patterns with Glisten components
Key Benefits:
- Write once, run anywhere with native performance
- Leverage existing Java skills and libraries
- Access to complete mobile device capabilities
- Strong enterprise support and community
- Continuous updates and improvements
Ideal Use Cases:
- Enterprise mobile applications
- Cross-platform business tools
- Applications requiring complex business logic
- Teams with strong Java expertise
- Projects needing shared code between desktop and mobile
Gluon Mobile represents the state-of-the-art in Java mobile development, providing a viable alternative to React Native or Flutter for Java-centric development teams.