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.
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
:
root
, which corresponds to the root element of the FXML structurecontroller
, 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):
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.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:
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).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 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:
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());
}
}