Теперь у меня вопрос: в чем разница между ними? Бывают ли ситуации, когда один лучше другого?
Документация обоих методов на самом деле не отвечает на мой вопрос.
Фабрика контроллеров — это объект, который создает для вас экземпляры контроллера из класса контроллера (указанного в файле FXML так же, как и при использовании подхода по умолчанию). Вы можете реализовать фабрику любым способом (например, вызвав определенный конструктор и предоставив аргумент, или ссылаясь на структуру DI и т. д.). Вызов setController(…) просто напрямую указывает экземпляр контроллера, который нужно использовать. В этом случае вы не должны указывать класс контроллера в FXML.
Другими словами, установка фабрики контроллера настраивает механизм создания контроллера по умолчанию. Контроллер по-прежнему создается FXMLLoader с учетом класса, указанного в FXML, но вы указываете ему, как его создать. Вызов setController() полностью заменяет механизм по умолчанию.




Возможный дубликат здесь: JavaFX: нужна помощь в понимании setControllerFactory
Как поясняется в дубликате, setControllerFactory полезен, когда вы используете среду внедрения зависимостей и хотите использовать свою собственную фабрику для внедрения зависимостей. Если вы не используете какой-либо фреймворк для внедрения зависимостей, его проще использовать setController.
Обратите внимание, что еще одним преимуществом использования фабрики контроллеров является то, что она позволяет вам (фактически требует от вас) указать класс контроллера в файле FXML. Это позволяет инструментам и IDE проверять и даже создавать методы обработчиков событий, @FXML аннотации и т. д. Использование setController() не позволяет использовать эти аспекты инструментария.
@ fi0x Я не уверен, действительно ли это отвечает на мой вопрос. Разве вы не можете использовать DI одновременно с setControllerFactory и setController?
Возьмем, к примеру, Spring: если вам нужен компонент в вашем контроллере, setController всегда будет использовать конструктор вашего контроллера с 0 аргументами, а не устанавливать компонент. Но если вы используете собственную фабрику контроллеров, вы можете указать, какой конструктор использовать, и внедрить необходимые bean-компоненты. (По крайней мере, я так понял ответ в моем посте по ссылке)
«setController() всегда будет использовать конструктор вашего контроллера с 0 аргументами». Нет, это неправда. Вызывая setController(…), вы указываете реальный экземпляр контроллера, который вам нужно создать в коде (и вы можете создать его любым способом).
@SquidXTV Да, можно setController(di.get(Foo.class)). Но у этого есть два недостатка: (1) вы больше не можете определить контроллер в файле FXML и (2) он несколько менее гибок. Использование setControllerFactory(di::get) (или чего-то подобного) будет работать независимо от того, какой контроллер определен в загружаемом в данный момент файле FXML (при условии, что платформа DI настроена правильно). По моему опыту, действительно имеет смысл использовать setController только тогда, когда вы используете fx:root, и корень и контроллер должны быть одним и тем же экземпляром.
Когда вы загружаете файл FXML с помощью одного из методов load(...), определенных в FXMLLoader, при настройке по умолчанию с помощью FXMLLoader создаются два важных объекта:
root, соответствующий корневому элементу структуры FXML.controller, который является объектом (конкретным экземпляром), связанным с root различными способами с помощью FXMLLoader.Поскольку вы не можете указать фактический объект со строковым значением атрибута XML (т. е. значением, предоставленным для fx:controller = "..."), по умолчанию это работает с указанием имени класса контроллера в атрибуте fx:controller. По умолчанию FXMLLoader создает экземпляр контроллера, рефлексивно вызывая конструктор указанного класса с нулевым аргументом.
FXMLLoader вставляет @FXML-аннотированные элементы из файла FXML в соответствующие поля созданного им экземпляра контроллера. После того, как это будет сделано, он вызывает метод initialize(), если он есть, что позволяет интуитивно интуитивно инициализировать контроллер и свойства связанного представления.
Стоит отметить некоторые последствия этого (не все из них имеют отношение к этому вопросу):
FXMLLoader.load(...) или его варианты несколько раз, будет создано несколько экземпляров класса контроллера. Это (почти?) всегда то, что вам нужно, но стоит отметить, что состояние не будет сохраняться от одного экземпляра контроллера к другому.Обычно требуется предоставить данные в той или иной форме контроллеру. Например, в архитектурах, подобных MVC/MVP/MVVM, контроллеру (или его эквиваленту) часто требуется доступ к объекту модели. Этого можно добиться с помощью настроек по умолчанию, получив ссылку на контроллер после загрузки FXML и передав ее:
Model model = ...;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
Parent root = loader.load();
MyController controller = loader.getController();
controller.setModel(model);
Хотя этот подход работоспособен, он имеет ряд недостатков:
initialize(). Это означает, что любая инициализация, которая должна быть выполнена в зависимости от модели, должна выполняться в результате вызова setModel(), а не в более интуитивно понятных местах (таких как метод initialize()).final в классе контроллера (что обычно естественно и желательно), и не существует элегантного способа заставить устанавливать модель только один раз.Первые два из этих недостатков можно устранить, вызвав setController(...) на FXMLLoader перед загрузкой FXML. Однако это само по себе влечет за собой новые недостатки:
Model model = ... ;
MyController controller = new MyController(model);
FXMLLoader loader = new FXMLLoader(getClass().getResources("path/to/view.fxml"));
loader.setController(controller);
Parent root = loader.load();
Обратите внимание, что в этом случае мы можем передать экземпляр модели конструктору контроллера. Это означает, что модель может быть окончательной и доступна сразу после создания экземпляра контроллера. Таким образом, контроллер может выглядеть так:
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());
}
}
Когда мы вызываем setController(...), мы полностью заменяем механизм создания контроллера по умолчанию, вместо этого предоставляя экземпляр контроллера по нашему выбору. По этой причине мы теперь не можем указать атрибут fx:controller в файле FXML; в лучшем случае это будет избыточно и, возможно, противоречиво, и если мы это сделаем, будет выброшено исключение.
Два последних недостатка, описанные выше (многословие и явное соединение в коде между представлением и классом контроллера), все еще существуют, хотя мы устранили два других, которые, вероятно, более важны. Однако невозможность указать класс контроллера в файле FXML означает, что у нас появляется новый недостаток:
Напротив, фабрика контроллеров позволяет нам использовать механизм по умолчанию для создания экземпляра контроллера (т. е. указание класса в атрибуте fx:controller и разрешение FXMLLoader предоставить экземпляр), но позволяет нам настраивать способ создания экземпляра.
Фабрика контроллеров — это Callback<Class<?>, Object>, которая по сути является функцией, которая сопоставляет Class<?> (тип) с объектом, который будет использоваться в качестве контроллера. Это должен быть экземпляр предоставленного класса, но фактической проверки для этого не существует.
Предыдущий пример можно переписать
Model model = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
loader.setControllerFactory(type -> new MyController(model));
Parent root = loader.load();
В этом случае мы можем (и должны) иметь fx:controller = "com.example.MyController" в файле FXML. Наш контроллер может выглядеть точно так же, как и предыдущая версия, с экземпляром модели final, доступным в методе initialize(). А наши инструменты разработчика «знают», какой класс используется для контроллера, поэтому могут проверять имена методов, полей и т. д.
Обратите внимание, что мы по-прежнему жестко запрограммировали имя класса контроллера в нашем коде загрузки. Мы можем создать фабрику контроллеров общего назначения, используя отражение. Эта реализация просматривает предоставленный класс контроллера, пытается найти конструктор, принимающий экземпляр модели, и использует этот конструктор, если он есть. Если он не находит такой конструктор, он пытается использовать конструктор без аргументов по умолчанию. Если ни один из них не существует, будет выдано исключение. Очевидно, что логика здесь произвольна и может быть любой, какую вы выберете.
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();
Это немного более подробно, но мы могли бы написать его один раз и повторно использовать во всем приложении. Файлы FXML являются «естественными» и выглядят точно так же, как при настройке по умолчанию, а контроллер может выглядеть так, как опубликовано здесь (с конструктором, принимающим экземпляр модели и т. д.). Возможно, существует общий недостаток использования отражения с точки зрения производительности и отсутствия проверки типов во время компиляции, но FXMLLoader уже использует так много отражений, что дополнительные затраты здесь, вероятно, будут минимальными.
Стоит отметить один момент: для вложенных файлов FXML используется <fx:include> в FXML. При загрузке включенного файла FXML будет использоваться та же фабрика контроллера, что и для явно загруженного файла FXML, содержащего его. Поэтому, если вы используете фабрику контроллеров, она должна быть достаточно гибкой, чтобы обеспечить правильный контроллер как для «основного» FXML, так и для любых включенных FXML.
Если я правильно помню, фабрика контроллеров была представлена в версии 2.1 в ответ на запрос от кого-то, желающего использовать среду внедрения зависимостей для управления контроллерами JavaFX. (Возможно, это был Адам Бьен, в то время, когда он писал ныне не поддерживаемый фреймворк afterburner, но у меня не очень хорошая память.)
Фабрики контроллеров прекрасно работают в этом сценарии. На этом сайте есть несколько примеров Spring-JavaFX, но общая идея такова:
ApplicationContext context = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/view.fxml"));
loader.setControllerFactory(context::getBean);
Parent root = loader.load();
По сути, мы просто говорим FXMLLoader, чтобы получить контроллеры из контекста приложения Spring.
Контроллер на основе Spring выглядит так
@Component(scope = "prototype")
public class MyController {
@Autowired
private Model model;
@FXML
private Label fooLabel;
@FXML
private void initialize() {
fooLabel.setText(model.getFoo());
}
}
В этом ответе рассматривается использование фабрики контроллеров — среди других подходов — в конкретном контексте. Я могу помочь сфокусировать ваш вопрос на предполагаемом варианте использования.