Dropout
Dropout

Reputation: 13866

Trying to switch stages after a splash screen in JavaFX

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

Upvotes: 1

Views: 414

Answers (1)

Slaw
Slaw

Reputation: 45806

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).

Upvotes: 1

Related Questions