Search code examples
javajavafx

FXMLLoader setController vs setControllerFactory


In JavaFX, the FXMLLoader automatically creates the specified controller in an .fxml file using 0-arg constructor. It also provides methods to set/create the controller manually using FXMLLoader#setController or FXMLoader#setControllerFactory.

My question now is what is the difference between them? Are there situation where one is better than the other?

The documentation of both methods don't really answer my question.


Solution

  • When you load an FXML file with one of the load(...) methods defined in FXMLLoader, in the default setup two important objects are created by the FXMLLoader:

    1. The root, which corresponds to the root element of the FXML structure
    2. The controller, which is an object (a specific instance) connected to the root in various ways by the FXMLLoader.

    Because you can't specify an actual object with a string value of an XML attribute (i.e. the value provided to fx:controller="..."), the default way this works is to specify the name of the controller class in the fx:controller attribute. By default, the FXMLLoader creates the controller instance by reflectively invoking the zero-argument constructor of the specified class.

    The FXMLLoader injects @FXML-annotated elements from the FXML file into matching fields in the controller instance it has created. After this is done, it calls the initialize() method, if there is one, allowing fairly intuitive initialization of the controller and properties of the associated view.

    Some consequences of this are worth noting (not all of which are relevant to this question):

    • If you call FXMLLoader.load(...) or its variants multiple times, multiple instances of the controller class will be created. This is (almost?) always what you want, but it's worth noting that state won't persist from one controller instance to another.
    • If the controller class does not have a zero-argument constructor, an error will be thrown. This error is necessarily thrown at runtime, as the compile cannot check the validity of the reflexive constructor call.

    It is common to want to provide data in some form to the controller. For example, in MVC/MVP/MVVM-like architectures, the controller (or equivalent) often needs access to a model object. You can achieve this with the default setup by retrieving a reference to the controller after the FXML is loaded and passing it:

    Model model = ...;
    
    FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
    Parent root = loader.load();
    MyController controller = loader.getController();
    controller.setModel(model);
    

    While this approach is workable, it has several disadvantages:

    • The model is not provided until after the FXML is loaded, and consequently after the initialize() method is invoked. This means any initialization that has to be performed that depends on the model has to be done as a result of the setModel() call, and not in the more intuitive places (such as the initialize() method).
    • The model cannot be final in the controller class (which is usually natural and desirable), and there is no elegant way to enforce setting the model only once.
    • The controller class associated with the view is hardcoded in the code that loads the FXML, which arguably violates encapsulation of the view-controller pair.
    • The code is a little verbose.

    The first two of these disadvantages can be remedied by calling setController(...) on the FXMLLoader prior to loading the FXML. However, this introduces new disadvantages of its own:

    Model model = ... ;
    MyController controller = new MyController(model);
    FXMLLoader loader = new FXMLLoader(getClass().getResources("path/to/view.fxml"));
    loader.setController(controller);
    Parent root = loader.load();
    

    Note in this case, we are able to pass a model instance to the controller constructor. This means the model can be final, and is available as soon as the controller is instantiated. So the controller can look like:

    public class MyController {
    
        private final Model model;
    
        @FXML
        private Label fooLabel;
    
        public MyController(Model model) {
            this.model = model;
        }
    
        @FXML
        private void initialize() {
            fooLabel.setText(model.getFoo());
        }
    }
    

    What happens when we call setController(...) is that we completely replace the default mechanism for creating a controller, instead providing a controller instance of our choosing. For this reason, we cannot now specify a fx:controller attribute in the FXML file; it would at best be redundant and possibly contradictory, and an exception will be thrown if we do.

    The last two disadvantages above (verbosity and explicit connection in code between the view and controller class) still exist, though we have resolved the other two which are probably more important. However, not being able to specify the controller class in the FXML file means we introduce a new disadvantge:

    • Developer tools, such as SceneBuilder or your IDE, no longer have any way to determine the controller class being used for the FXML file. This means they can no longer highlight event handler method name or injected field name typos, or create stubs of those for us. This can be a pretty big disadvantage in terms of productivity.

    By contrast, the controller factory allows us to use the default mechanism of creating a controller instance (i.e. specifying the class in the fx:controller attribute and letting the FXMLLoader provide the instance) but allows us to configure how the instance is created.

    The controller factory is a Callback<Class<?>, Object>, which is essentially a function that maps a Class<?> (a type) to an object which will be used as a controller. It should be an instance of the provided class, but there is no actual check for that.

    The previous example can be rewritten

    Model model = ... ;
    FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
    loader.setControllerFactory(type -> new MyController(model));
    Parent root = loader.load();
    

    In this case we can (and must) have fx:controller="com.example.MyController" in the FXML file. Our controller can look exactly the same as the previous version, with the final model instance which is accessible in the initialize() method. And our developer tools "know" which class is being used for the controller, so can check method and field names, etc.

    Note we still hard-code the controller class name in our loading code. We can make a general controller factory using reflection. This implementation looks at the provided controller class, tries to find a constructor taking a model instance, and uses that constructor if there is one. If it doesn't find such a constructor, it tries to use the default no-argument constructor. If neither exist, an exception will be thrown. Obviously, the logic here is arbitrary and can be anything you choose.

    Model model = ...;
    FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
    loader.setControllerFactory(type -> {
        for (Constructor<?> constructor : type.getConstructors()) {
            if (constructor.getParameterCount() == 1 && constructor.getParameterTypes()[0].equals(Model.class)) {
                return constructor.newInstance(model);
            }
        }
        return type.getConstructor().newInstance();
    });
    Parent root = loader.load();
    

    This is a bit more verbose, but we could write it once and reuse it across the application. The FXML files are "natural" and look exactly the same as in the default setup, and the controller can look like the one posted here (with a constructor taking a model instance, etc). There is arguably a general disadvantage of using reflection in terms of performance and lack of compile-time type checking, but the FXMLLoader is already using so much reflection the additional cost here is likely to be minimal.

    One point worth noting is for nested FXML files, using <fx:include> in the FXML. The same controller factory will be used when the included FXML file is loaded as for the FXML file explicitly loaded that contains it. So if you use a controller factory, it must be flexible enough to provide the correct controller for both the "main" FXML and any included FXMLs.


    If I remember correctly, the controller factory was introduced in version 2.1 in response to a request from someone wanting to use a dependency injection framework to manage JavaFX controllers. (It might have been Adam Bien, at the time he was writing the now-unmaintained afterburner framework, but my memory is not that good.)

    Controller factories work nicely in this scenario. There are several Spring-JavaFX examples on this site, but the general idea is:

    ApplicationContext context = ... ;
    
    FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
    loader.setControllerFactory(context::getBean);
    Parent root = loader.load();
    

    Essentially we just tell the FXMLLoader to get the controllers from the spring application context.

    The Spring-based controller looks like

    @Component(scope="prototype")
    public class MyController {
        @Autowired
        private Model model;
    
        @FXML
        private Label fooLabel;
    
        @FXML
        private void initialize() {
            fooLabel.setText(model.getFoo());
        }
    }