JavaFX software design

My guess is that this design was motivated by the (vast) number of Swing applications that were incorrectly written, with the “primary” JFrames being instantiated and shown on the wrong thread (i.e. not on the AWT event dispatch thread). My guess is that so many Swing applications were incorrectly written that they had to defensively code the framework against the incorrect usage, and that they wanted to avoid this scenario with JavaFX.

Forcing (well, almost forcing, there are hack-arounds) an FX Application to start this way makes it much harder to write an application incorrectly in a similar way. The launch method (and the equivalent Oracle JVM startup process if you have an Application subclass without a main method and a call to launch) does quite a bit of boilerplate work: it starts the FX toolkit, instantiates the Application subclass and calls its init() method, then on the FX Application Thread it instantiates the primary Stage and passes it to the Application subclass’s start(...) method. This then ensures everything is running on the correct thread.

You should basically consider the start(...) method in a JavaFX application as the replacement for the main(...) method in a “traditional” Java application, with the understanding it is invoked on the FX Application Thread.

My recommendation is that the Application subclass should be as minimal as possible; it should just delegate to something else to actually create the UI, and then should just place it in the primary stage and show it. Include a main method that does nothing other than call launch(...) as a fallback for non-JavaFX-aware JVMs. You should only have one instance of one Application subclass present in any JVM. This way your Application subclass has no class members to set, and so the issues you describe simply don’t arise.

If you use FXML, this is actually fairly natural: the start(...) method essentially just delegates to the FXML-controller pair to do the real work. If you don’t use FXML, create a separate class to do the actual layout, etc, and delegate to it. See this related question which gets at the same kind of idea.

Note also that your statement

the inherited launch() method, although it’s public, must be called
from within this derived class

is not entirely accurate, as there is an overloaded form of the launch(...) method in which you can specify the application subclass. So, if you really need, you can just create a stub for starting the FX toolkit:

public class FXStarter extends Application {

    @Override
    public void start(Stage primaryStage) {
        // no-op
    }
}

Now you can do:

public class MyRegularApplication {

    public static void main(String[] args) {
        // start FX toolkit:
        new Thread(() -> Application.launch(FXStarter.class)).start();
        // other stuff here...
    }
}

Note that launch does not return until the FX toolkit shuts down, so it is imperative to put this call in another thread. This potentially creates race conditions, where you may try to do something needing the FX toolkit before launch(...) has actually initialized it, so you should probably guard against that:

public class FXStarter extends Application {

    private static final CountDownLatch latch = new CountDownLatch(1);

    public static void awaitFXToolkit() throws InterruptedException {
       latch.await();
    }

    @Override
    public void init() {
        latch.countDown();
    }

    @Override
    public void start(Stage primaryStage) {
        // no-op
    }
}

and then

public class MyRegularApplication {

    public static void main(String[] args) throws InterruptedException {
        // start FX toolkit:
        new Thread(() -> Application.launch(FXStarter.class)).start();
        FXStarter.awaitFXToolkit();
        // other stuff here...
    }
}

SSCCE (I just used inner classes for everything so this is convenient to run, but in real life these would be standalone classes):

import java.util.Random;
import java.util.concurrent.CountDownLatch;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class BackgroundProcessDrivenApp {

    public static void main(String[] args) throws InterruptedException {
        Platform.setImplicitExit(false);
        new Thread(() -> Application.launch(FXStarter.class)).start();
        FXStarter.awaitFXToolkit();
        new MockProcessor().doStuff() ;
    }

    public static class FXStarter extends Application {

        private static final CountDownLatch latch = new CountDownLatch(1);

        @Override
        public void init() {
            latch.countDown();
        }

        public static void awaitFXToolkit() throws InterruptedException {
            latch.await();
        }

        @Override
        public void start(Stage primaryStage) { }
    }

    public static class MockProcessor {

        private final int numEvents = 10 ;

        public void doStuff() {
            Random rng = new Random();
            try {
                for (int event = 1 ; event <= numEvents; event++) {
                    // just sleep to mimic waiting for background service...
                    Thread.sleep(rng.nextInt(5000) + 5000);
                    String message = "Event " + event + " occurred" ;
                    Platform.runLater(() -> new Messager(message).showMessageInNewWindow());
                }
            } catch (InterruptedException exc) {
                Thread.currentThread().interrupt();
            } finally {
                Platform.setImplicitExit(true);
            }
        }
    }

    public static class Messager {
        private final String message ;

        public Messager(String message) {
            this.message = message ;
        }

        public void showMessageInNewWindow() {
            Stage stage = new Stage();
            Label label = new Label(message);
            Button button = new Button("OK");
            button.setOnAction(e -> stage.hide());
            VBox root = new VBox(10, label, button);
            root.setAlignment(Pos.CENTER);
            Scene scene = new Scene(root, 350, 120);
            stage.setScene(scene);
            stage.setAlwaysOnTop(true);
            stage.show();
        }
    }
}

Leave a Comment