Diseño del software JavaFX

En una aplicación JavaFX, javafx.application.Application se debe subclasificar, y el método heredado de launch (), aunque es público, se debe invocar desde esta clase derivada; de lo contrario, se lanza una excepción. El método launch () utiliza la reflexión para crear una instancia de la clase derivada, lo que dificulta establecer valores para los miembros de la clase sin perderlos al iniciar. Todo eso me parece totalmente inusual, y me preguntaba por qué comenzar una aplicación JavaFX es tan complicado, si ese tipo de diseño de software (¿patrón de diseño?) Tiene un nombre, o si es solo un mal diseño.

EDITAR:

Para ser más específico, quiero usar el patrón de observador, por lo que mi aplicación Java recibe una notificación cuando se carga un documento, como este:

public class MyDocumentLoader extends Application { private ChangeListener changeListener; public void setChangeListener(ChangeListener changeListener) { this.changeListener = changeListener; } ... public void loadDocument(String url) { webEngine.getLoadWorker().stateProperty().addListener(changeListener); webEngine.load(url); } ... } 

Necesito el miembro de callback en varios métodos, e idealmente puedo tener más de una instancia de la clase que carga documentos, por lo que puedo configurar diferentes Listas de cambio para diferentes URL.

JavaFX admite una gran cantidad de estrategias de implementación y empaque, ref. https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/toc.html , y tener un punto de entrada y salida de ciclo de vida estandarizado simplifica el soporte de todas estas estrategias.

Si está intentando inicializar su clase de aplicación principal, debido a que está instanciado por el iniciador JavaFX, su mejor opción es utilizar los métodos Application.init () y Application.stop (), como señala James_D.

Supongo que este diseño fue motivado por el (vasto) número de aplicaciones Swing que se escribieron incorrectamente, con el JFrame “primario” siendo instanciado y mostrado en el hilo equivocado (es decir, no en el hilo de envío del evento AWT). Mi suposición es que tantas aplicaciones Swing fueron escritas incorrectamente que tenían que codificar defensivamente el framework contra el uso incorrecto, y que querían evitar este escenario con JavaFX.

Forzar (bueno, casi forzar, hay hack-arounds) una aplicación FX para comenzar de esta manera hace que sea mucho más difícil escribir una aplicación incorrectamente de una manera similar. El método de launch (y el proceso de inicio Oracle JVM equivalente si tiene una subclase de Application sin un método main y una llamada para launch ) hace bastante trabajo repetitivo: inicia el juego de herramientas FX, crea la subclase Application y llama a su init() , luego en el subproceso de la aplicación FX, crea una instancia de la Stage primaria y la pasa al método de start(...) la subclase de Application start(...) . Esto asegura que todo se ejecuta en el hilo correcto.

Básicamente, debe considerar el método start(...) en una aplicación JavaFX como reemplazo del método main(...) en una aplicación Java “tradicional”, entendiéndose que se invoca en el subproceso de la aplicación FX.

Mi recomendación es que la subclase Application sea ​​lo más mínima posible; solo debería delegar en otra cosa para crear realmente la IU, y luego simplemente colocarla en la etapa primaria y mostrarla. Incluya un método main que no hace otra cosa que llamar al launch(...) como una alternativa para las JVM que no tienen JavaFX. Solo debe tener una instancia de una subclase de Application presente en cualquier JVM. De esta forma, su subclase de Application no tiene miembros de clase para establecer, por lo que los problemas que describe simplemente no surgen.

Si usa FXML, esto es realmente bastante natural: el método de start(...) esencialmente solo delega al par controlador de FXML para hacer el trabajo real. Si no usa FXML, cree una clase separada para hacer el diseño real, etc., y delegue en él. Vea esta pregunta relacionada que tiene el mismo tipo de idea.

Tenga en cuenta también que su statement

el método heredado de launch (), aunque es público, debe invocarse desde esta clase derivada

no es del todo exacto, ya que hay una forma sobrecargada del método de launch(...) en la que puede especificar la subclase de la aplicación. Entonces, si realmente lo necesita, puede crear un stub para iniciar el kit de herramientas de FX:

 public class FXStarter extends Application { @Override public void start(Stage primaryStage) { // no-op } } 

Ahora puedes hacer:

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

Tenga en cuenta que el launch no se reanuda hasta que el conjunto de herramientas FX se cierre, por lo que es imprescindible colocar esta llamada en otro hilo. Esto potencialmente crea condiciones de carrera, donde puedes intentar hacer algo que necesite el kit de herramientas FX antes de que el launch(...) haya inicializado, por lo que probablemente deberías protegerte de eso:

 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 } } 

y entonces

 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 (Acabo de utilizar clases internas para todo, así que es conveniente ejecutarlo, pero en la vida real estas serían clases independientes):

 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(); } } }