Using Java8 to build and deploy native HTML5 applications

Ever since I started using TileMill I’ve been interested in ways to develop native applications that are implemented with web technologies.  TideKit appears to be a promising option but I am also wondering if it is a scam.  I purchased a reservation from them a very long time ago and have not received anything from them.  That’s ok though because there’s an old kid on the block who has some new tricks.  Enter Java8’s WebEngine and WebView.  WebView provides you with a fully functional webkit based browser.  It ships with the Java8 runtime so there aren’t any crazy dependencies.  Java8 also has features for deploying applications as native packages/installers.  There is an excellent tutorial on the process of building and deploying a Java8 application here : http://code.makery.ch/java/javafx-8-tutorial-intro/

To expand on that tutorial I set out to build a simple HTML5 based application, and was pleasantly surprised at the results.

Project structure (using the e(fx)clipse plugin)

java8_html5_app_project_structure

Main.java starts the app and loads the layout.

package application;
 
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
 
public class Main extends Application {
 
	private Stage primaryStage;
    private BorderPane rootLayout;
 
	@Override
	public void start(Stage primaryStage) {
		try {
			BorderPane root = new BorderPane();
			Scene scene = new Scene(root,400,400);
			scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
			primaryStage.setScene(scene);
			primaryStage.show();
			this.primaryStage = primaryStage;
			initRootLayout();
 
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
 
	public void initRootLayout() {
        try {
            // Load root layout from fxml file.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(Main.class.getResource("MapView.fxml"));
            rootLayout = (BorderPane) loader.load();
 
            // Show the scene containing the root layout.
            Scene scene = new Scene(rootLayout);
            primaryStage.setScene(scene);
 
            // Initialize the MapController
            MapController controller = loader.getController();
            controller.init();
 
            primaryStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
	public static void main(String[] args) {
		launch(args);
	}
}

MapController.java ties all of the user interface components together.  The init function shows how to load custom content as well as setup an object for JavaScript to use for upcalls to Java.  The handleGo method shows how to execute javascript inside the browser engine.

package application;
 
import java.io.IOException;
import java.io.InputStream;
import netscape.javascript.JSObject;
import org.apache.commons.io.IOUtils;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
 
public class MapController {
 
    @FXML
    private Label statusLabel;
 
    @FXML
    private StringProperty status;
 
    @FXML
    WebView webView;
    WebEngine engine;
 
    @FXML
    BooleanProperty loaded = new SimpleBooleanProperty(false);
 
    @FXML
    Button goButton;
 
    public void init(){
 
    	// Make the "Go" button disabled until the page content is loaded
    	goButton.disableProperty().bind(loaded.not());
    	// A good example of "computed bindings" : http://stackoverflow.com/questions/23040531/how-to-disable-button-when-textfield-is-empty
 
    	// Bind the status label to a property
    	status = new SimpleStringProperty("");
    	statusLabel.textProperty().bind(status);
 
    	// Get a reference to the WebView's WebEngine
    	engine = webView.getEngine();
 
		// Add a listener so we can get notified when the page content is loaded
		engine.getLoadWorker().stateProperty().addListener(
        new ChangeListener<State>() {
            public void changed(ObservableValue ov, State oldState, State newState) {
                if (newState == State.SUCCEEDED) {
                    loaded.set(true);
                }
            }
        });
 
		// Give the browser a "java" object that can be used to "upcall" into the Java application.
		JSObject window = (JSObject)engine.executeScript("window");
        window.setMember("java", new JavaApplication());
 
        // Load the page content
    	String content = "Failed to load content.";
		try {
			InputStream in = this.getClass().getResourceAsStream("resources/map.html");
		    content = IOUtils.toString(in);
 
		} catch (IOException e) {
			e.printStackTrace();
		}
    	engine.loadContent(content);
 
    	// You can also do this:
    	//engine.load("http://www.youtube.com");
    }
 
    @FXML
    private void handleGo() {
		try {
			//When the go button is pressed, invoke go.js
			InputStream in = this.getClass().getResourceAsStream("resources/go.js");
		    String script = IOUtils.toString(in);
			engine.executeScript(script);
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
 
    /**
     * An example class for JavaScript to Java upcalls
     */
    public class JavaApplication {
    	//Using (Double lat, Double lng) did not work:
        public void updateCenter(String lat, String lng) {
            status.set("Center : "+lat+", "+lng);
        }
    }
}

MapView.fxml defines the application’s layout.  It has a WebView for displaying the app, a Button to test making JavaScript calls from Java, and a Label to demonstrate making upcalls from JavaScript to Java.

java8_html5_app_layout

<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.web.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
 
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MapController">
   <center>
      <WebView fx:id="webView" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
   </center>
   <bottom>
      <HBox>
         <children>
            <Button fx:id="goButton" mnemonicParsing="false" onAction="#handleGo" text="Go!" BorderPane.alignment="CENTER" />
            <Label fx:id="statusLabel" text="statusLabel">
               <HBox.margin>
                  <Insets left="12.0" top="5.0" />
               </HBox.margin></Label>
         </children>
      </HBox>
   </bottom>
</BorderPane>

Next, for the HTML and JavaScript that make up the web app.  Note how I included these resources in the classpath.  This makes it much easier to load them and ensure they get included in the native package.  map.html is a very simple (Google) map.  It contains some code to update the Java side whenever the map center changes.

<!DOCTYPE html>
<html>
  <head>
    <style type="text/css">
      html, body, #map-canvas { height: 100%; margin: 0; padding: 0;}
    </style>
    <script type="text/javascript"
      src="https://maps.googleapis.com/maps/api/js">
    </script>
    <script type="text/javascript">
      var map;
      function initialize() {
        var mapOptions = {
          center: { lat: -34.397, lng: 150.644},
          zoom: 8
        };
        map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
 
        google.maps.event.addListener(map, 'center_changed', function() {
        	//Do an upcall to java with the map center
        	java.updateCenter(map.getCenter().lat(), map.getCenter().lng());
        });
 
      }
      google.maps.event.addDomListener(window, 'load', initialize);
    </script>
  </head>
  <body>
	<div id="map-canvas"></div>
  </body>
</html>

Finally, go.js is a snippet of JavaScript that moves the map center and adds a marker.

//Center the map on a location
var myLatlng = new google.maps.LatLng(38.536056, -106.000932);
map.setCenter(myLatlng);
var marker = new google.maps.Marker({
    position: myLatlng,
    map: map,
    title: 'Hello World!'
});

The end result :

java8_app