1

I'm trying to display a splash screen until I load all necessary resources and open the main stage afterwards, but I keep running into InvocationTargetException.

In other words, my primary stage loads a FXML which has a Controller which looks like this:

public class SplashController {

    @FXML
    VBox splashScreenVBox = new VBox();

    @FXML
    protected void initialize() throws InterruptedException, IOException {

        Stage primaryStage = (Stage) splashScreenVBox.getScene().getWindow();
        primaryStage.close();
        new MainStage();            
    }
}

The MainStage class is just loading a FXML and showing the scene:

public MainStage() throws IOException {

    Parent root = FXMLLoader.load(getClass().getResource("/core/views/Main.fxml"));
    this.setScene(new Scene(root, 800, 600));
    this.show();
}

The error that I'm getting points towards a line on which I FXMLLoader.load() the FXML the first time (I have two FXML files, one for each stage).

Can someone shed some light on why this happens and preferably how to correctly work with the FXMLLoader, in case that what I'm doing is an issue, please?

Edit: Stacktrace

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:389)
    at com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:328)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:767)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:917)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$155(LauncherImpl.java:182)
    at java.lang.Thread.run(Thread.java:745)
Caused by: javafx.fxml.LoadException: 
/C:/Users/REDACTED/out/production/REDACTED/core/views/Splash.fxml

    at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2571)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2441)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3214)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3175)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3148)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3124)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3104)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:3097)
    at core.Scenes.start(Scenes.java:19)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    ... 1 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.reflect.misc.Trampoline.invoke(MethodUtil.java:71)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.reflect.misc.MethodUtil.invoke(MethodUtil.java:275)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2566)
    ... 17 more
Caused by: java.lang.NullPointerException
    at core.controllers.SplashController.initialize(SplashController.java:18)
    ... 28 more
Exception running application core.Scenes
Slaw
  • 37,820
  • 8
  • 53
  • 80
Dropout
  • 13,653
  • 10
  • 56
  • 109
  • Post the entire stack trace of the `InvocationTargetException`. – Slaw Aug 20 '18 at 10:37
  • Also, don't initialize `FXML` annotated fields: `VBox splashScreenVBox = new VBox()`. That instance you created is just going to be replaced by the one injected by the `FXMLLoader`. – Slaw Aug 20 '18 at 10:39
  • @Slaw thanks, I've corrected the `@FXML` annotated field and edited in the stacktrace. – Dropout Aug 20 '18 at 10:48
  • I should add that the scenes/stages are running concurrently for a while, until the first one gets closed, but that's visible from the code. Isn't that an issue for FXMLLoader? – Dropout Aug 20 '18 at 10:59

1 Answers1

1

As you can see from the stack trace you are getting a NullPointerException in the initialize method of your SplashController on line 18. I'm going to assume that line 18 is this line:

Stage primaryStage = (Stage) splashScreenVBox.getScene().getWindow();

The issue is most likely this call: getScene().getWindow(). The method getScene() will return null here because splashScreenVBox is not part of a Scene yet. How could it be? The initialize method is called during the execution of FXMLLoader.load(). This means you have not had the chance to add the result of FXMLLoader.load() to a Scene yet.

To fix this one option is adding a method to your SplashController that does the loading for MainStage.

FXMLLoader loader = new FXMLLoader(getClass().getResource("your/resource"));
Parent root = loader.load();

Stage splashStage = new Stage();
splashStage.setScene(new Scene(root));
splashStage.show();

SplashController controller = loader.getController();
controller.loadMainApp(splashStage);

I passed the splashStage to the method so you can hide/close it when ready to show the main Stage. Depending on the design of your code there may be other ways to do this than passing an argument.


An example of how to create an abstract controller that will automatically call a method when the root is added to a Scene and that Scene has been added to a Window.

import java.util.function.Consumer;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Window;

public abstract class AbstractController<T extends Node> {

  // protected so subclasses can access the root
  // directly. You could also hide this behind a
  // getter.
  @FXML protected T root;

  // Subclasses that override this method must call the
  // super implementation
  @FXML
  protected void initialize() {
    Consumer<Window> onNewWindow = this::onAddedToWindow;
    Consumer<Scene> onNewScene = scene ->
        scene.windowProperty().addListener(new SelfRemovingChangeListener<>(onNewWindow));
    root.sceneProperty().addListener(new SelfRemovingChangeListener<>(onNewScene));
  }

  protected abstract void onAddedToWindow(Window window);

  private static class SelfRemovingChangeListener<T> implements ChangeListener<T> {

    private final Consumer<? super T> onNewValue;

    private SelfRemovingChangeListener(Consumer<? super T> onNewValue) {
      this.onNewValue = onNewValue;
    }

    @Override
    public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
      onNewValue.accept(newValue);
      observable.removeListener(this);
    }

  }

}

This requires that you have a fx:id="root" in your FXML file and that the root object is a Node. The onAddedToWindow is only called the first time the root has been added to a Window. You still have to add the root to a Window elsewhere (wherever you call FXMLLoader.load).

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Oh it makes sense to me finally. It was just as you said, I tried to get the scene from an object which wasn't there yet, `during initialize()`. Thanks very much for clearing this up. I've edited the code and it works precisely as intended now. Is there any method I could call instead of `initialize()` that works in a "after load" manner that gets invoked automatically, please? – Dropout Aug 20 '18 at 11:15
  • @Dropout Technically the `initialize` method _is_ the "after load" method. You want a method that's called automatically after the root is added a `Scene` _and_ that `Scene` is added to a `Stage`, right? The issue is that happens _outside_ of the `FXMLLoader`'s control. It has no idea _when_ this will happen. The closest you can get is by creating an abstract controller that will add this behavior. I'll edit my answer with an example. – Slaw Aug 20 '18 at 11:37
  • Thank you, it's really helpful! – Dropout Aug 20 '18 at 11:39
  • 1
    @Dropout example added – Slaw Aug 20 '18 at 11:47
  • Thanks! Much appreciated! – Dropout Aug 20 '18 at 12:03