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.