如何使用 Dagger 2 将服务注入 JavaFX 控制器
How to inject services into JavaFX controllers using Dagger 2
JavaFX 本身有一些 DI 方法允许在 XML 描述的 UI 和控制器之间进行绑定:
<Pane fx:controller="foo.bar.MyController">
<children>
<Label fx:id="myLabel" furtherAttribute="..." />
</children>
</Pane>
Java 端看起来像这样:
public class MyController implements Initializable {
@FXML private Label myLabel;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// FXML-fields have been injected at this point of time:
myLabel.setText("Hello world!");
}
}
要实现这一点,我不能只创建 MyController 的实例。相反,我不得不请 JavaFX 为我做事:
FXMLLoader loader = new FXMLLoader(MyApp.class.getResource("/fxml/myFxmlFile.fxml"), rb);
loader.load();
MyController ctrl = (MyController) loader.getController();
到目前为止,还不错
但是,如果我想使用 Dagger 2 将一些非 FXML 依赖项注入到此控制器 class 的构造函数中,我会遇到问题,因为我无法控制实例化过程,如果我使用 JavaFX.
public class MyController implements Initializable {
@FXML private Label myLabel;
/*
How do I make this work?
private final SomeService myService;
@Inject
public MyController(SomeService myService) {
this.myService = myService;
}
*/
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// FXML-fields have been injected at this point of time:
myLabel.setText("Hello world!");
}
}
有一个 API 看起来很有前途:loader.setControllerFactory(...);
也许这是一个很好的起点。但是我对这些库没有足够的经验,不知道如何解决这个问题。
自定义 ControllerFactory
将需要构建仅在运行时已知的某些类型的控制器。这可能如下所示:
T t = clazz.newInstance();
injector.inject(t);
return t;
这对于像 Guice 这样的大多数其他 DI 库来说完全没问题,因为它们只需要在依赖关系图中查找 t
类型的依赖关系。
Dagger 2 在编译时解析依赖关系。它最大的特点同时也是最大的问题:如果一个类型只在运行时已知,编译器就无法区分 inject(t)
的调用。可以是 inject(Foo foo)
或 inject(Bar bar)
.
(这也不适用于 final 字段,因为 newInstance()
调用默认构造函数)。
好的,没有通用类型。让我们看看第二种方法:首先从 Dagger 获取控制器实例,然后将其传递给 FXMLLoader。
我使用了 Dagger 中的 CoffeeShop 示例并对其进行了修改以构建 JavaFX 控制器:
@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
Provider<CoffeeMakerController> coffeeMakerController();
}
如果我得到一个 CoffeeMakerController,它的所有字段都已经注入,所以我可以轻松地在 setController(...)
:
中使用它
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
CoffeeMakerController ctrl = coffeeShop.coffeeMakerController().get();
/* ... */
FXMLLoader loader = new FXMLLoader(fxmlUrl, rb);
loader.setController(ctrl);
Parent root = loader.load();
Stage stage = new Stage();
stage.setScene(new Scene(root));
stage.show();
我的 FXML 文件不能包含 fx:controller 属性,因为加载程序会尝试构建一个控制器,这当然会与我们的 Dagger 提供的控制器发生冲突。
完整示例可在 GitHub
上找到
或者你可以这样做:
...
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> type) {
switch (type.getSimpleName()) {
case "LoginController":
return loginController;
case "MainController":
return mainController;
default:
return null;
}
}
});
...
如@Sebastian_S 所述,基于反射的控制器工厂是不可能的。然而,调用 setController 并不是唯一的方法,我实际上更喜欢这种 setControllerFactory 方法,因为它不会破坏工具(例如 IntelliJ 的 XML 检查),但必须明确列出所有 类 绝对是一个缺点.
这对很多人来说可能很久以前就解决了。我不喜欢此处描述的解决方案,因为它依赖于 class 名称或对简洁设计的反思。我写了一个有点不同的,在我看来更易于维护。
它的要点是使用 Dagger 创建注入到 Stage
中的场景。这是我的申请 class
CameraRemote context;
public static void main(String[] args) {
launch(args);
}
public SimpleUI() {
context = DaggerCameraRemote.builder().build();
}
@Override
public void start(Stage stage) throws IOException {
stage.setTitle("Remote Control");
stage.setScene(context.mainFrame());
stage.show();
}
我的 Dagger 2 模块中有加载 fxml 和自定义控制器的逻辑,即注入 SsdpClient
@Provides
public static Scene provideMainScene(SsdpClient ssdpClient) {
try {
FXMLLoader loader = new FXMLLoader(CameraModule.class.getResource("/MainFrame.fxml"));
Parent root;
root = loader.load();
MainController controller = (MainController) loader.getController();
controller.setClient(ssdpClient);
return new Scene(root, 800, 450);
} catch (IOException e) {
throw new RuntimeException("Cannot load MainFrame.fxml", e);
}
}
我可以进一步拆分 Parent
实例的创建。它没有在其他任何地方使用,我妥协了。
感谢来自@Sebastian_S的Map multibinding mechanism提示,我已经设法使用Map<Class<?>, Provider<Object>>
进行自动控制器绑定,将每个控制器映射到它的class。
在模块中将所有控制器收集到名为 "Controllers" 的地图中,并带有相应的 Class 键
@Module
public class MyModule {
// ********************** CONTROLLERS **********************
@Provides
@IntoMap
@Named("Controllers")
@ClassKey(FirstController.class)
static Object provideFirstController(DepA depA, DepB depB) {
return new FirstController(depA, depB);
}
@Provides
@IntoMap
@Named("Controllers")
@ClassKey(SecondController.class)
static Object provideSecondController(DepA depA, DepC depC) {
return new SecondController(depA, depC);
}
}
然后在Component中,我们可以通过它的名字得到这个Map的一个实例。此映射的值类型应为 Provider<Object>
,因为我们希望在每次 FXMLLoader
需要时获取控制器的新实例。
@Singleton
@Component(modules = MyModule.class)
public interface MyDiContainer {
// ********************** CONTROLLERS **********************
@Named("Controllers")
Map<Class<?>, Provider<Object>> getControllers();
}
最后,在您的 FXML 加载代码中,您应该设置新的 ControllerFactory
MyDiContainer myDiContainer = DaggerMyDiContainer.create()
Map<Class<?>, Provider<Object>> controllers = myDiContainer.getControllers();
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(type -> controllers.get(type).get());
JavaFX 本身有一些 DI 方法允许在 XML 描述的 UI 和控制器之间进行绑定:
<Pane fx:controller="foo.bar.MyController">
<children>
<Label fx:id="myLabel" furtherAttribute="..." />
</children>
</Pane>
Java 端看起来像这样:
public class MyController implements Initializable {
@FXML private Label myLabel;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// FXML-fields have been injected at this point of time:
myLabel.setText("Hello world!");
}
}
要实现这一点,我不能只创建 MyController 的实例。相反,我不得不请 JavaFX 为我做事:
FXMLLoader loader = new FXMLLoader(MyApp.class.getResource("/fxml/myFxmlFile.fxml"), rb);
loader.load();
MyController ctrl = (MyController) loader.getController();
到目前为止,还不错
但是,如果我想使用 Dagger 2 将一些非 FXML 依赖项注入到此控制器 class 的构造函数中,我会遇到问题,因为我无法控制实例化过程,如果我使用 JavaFX.
public class MyController implements Initializable {
@FXML private Label myLabel;
/*
How do I make this work?
private final SomeService myService;
@Inject
public MyController(SomeService myService) {
this.myService = myService;
}
*/
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// FXML-fields have been injected at this point of time:
myLabel.setText("Hello world!");
}
}
有一个 API 看起来很有前途:loader.setControllerFactory(...);
也许这是一个很好的起点。但是我对这些库没有足够的经验,不知道如何解决这个问题。
自定义 ControllerFactory
将需要构建仅在运行时已知的某些类型的控制器。这可能如下所示:
T t = clazz.newInstance();
injector.inject(t);
return t;
这对于像 Guice 这样的大多数其他 DI 库来说完全没问题,因为它们只需要在依赖关系图中查找 t
类型的依赖关系。
Dagger 2 在编译时解析依赖关系。它最大的特点同时也是最大的问题:如果一个类型只在运行时已知,编译器就无法区分 inject(t)
的调用。可以是 inject(Foo foo)
或 inject(Bar bar)
.
(这也不适用于 final 字段,因为 newInstance()
调用默认构造函数)。
好的,没有通用类型。让我们看看第二种方法:首先从 Dagger 获取控制器实例,然后将其传递给 FXMLLoader。
我使用了 Dagger 中的 CoffeeShop 示例并对其进行了修改以构建 JavaFX 控制器:
@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
Provider<CoffeeMakerController> coffeeMakerController();
}
如果我得到一个 CoffeeMakerController,它的所有字段都已经注入,所以我可以轻松地在 setController(...)
:
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
CoffeeMakerController ctrl = coffeeShop.coffeeMakerController().get();
/* ... */
FXMLLoader loader = new FXMLLoader(fxmlUrl, rb);
loader.setController(ctrl);
Parent root = loader.load();
Stage stage = new Stage();
stage.setScene(new Scene(root));
stage.show();
我的 FXML 文件不能包含 fx:controller 属性,因为加载程序会尝试构建一个控制器,这当然会与我们的 Dagger 提供的控制器发生冲突。
完整示例可在 GitHub
上找到或者你可以这样做:
...
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> type) {
switch (type.getSimpleName()) {
case "LoginController":
return loginController;
case "MainController":
return mainController;
default:
return null;
}
}
});
...
如@Sebastian_S 所述,基于反射的控制器工厂是不可能的。然而,调用 setController 并不是唯一的方法,我实际上更喜欢这种 setControllerFactory 方法,因为它不会破坏工具(例如 IntelliJ 的 XML 检查),但必须明确列出所有 类 绝对是一个缺点.
这对很多人来说可能很久以前就解决了。我不喜欢此处描述的解决方案,因为它依赖于 class 名称或对简洁设计的反思。我写了一个有点不同的,在我看来更易于维护。
它的要点是使用 Dagger 创建注入到 Stage
中的场景。这是我的申请 class
CameraRemote context;
public static void main(String[] args) {
launch(args);
}
public SimpleUI() {
context = DaggerCameraRemote.builder().build();
}
@Override
public void start(Stage stage) throws IOException {
stage.setTitle("Remote Control");
stage.setScene(context.mainFrame());
stage.show();
}
我的 Dagger 2 模块中有加载 fxml 和自定义控制器的逻辑,即注入 SsdpClient
@Provides
public static Scene provideMainScene(SsdpClient ssdpClient) {
try {
FXMLLoader loader = new FXMLLoader(CameraModule.class.getResource("/MainFrame.fxml"));
Parent root;
root = loader.load();
MainController controller = (MainController) loader.getController();
controller.setClient(ssdpClient);
return new Scene(root, 800, 450);
} catch (IOException e) {
throw new RuntimeException("Cannot load MainFrame.fxml", e);
}
}
我可以进一步拆分 Parent
实例的创建。它没有在其他任何地方使用,我妥协了。
感谢来自@Sebastian_S的Map multibinding mechanism提示,我已经设法使用Map<Class<?>, Provider<Object>>
进行自动控制器绑定,将每个控制器映射到它的class。
在模块中将所有控制器收集到名为 "Controllers" 的地图中,并带有相应的 Class 键
@Module
public class MyModule {
// ********************** CONTROLLERS **********************
@Provides
@IntoMap
@Named("Controllers")
@ClassKey(FirstController.class)
static Object provideFirstController(DepA depA, DepB depB) {
return new FirstController(depA, depB);
}
@Provides
@IntoMap
@Named("Controllers")
@ClassKey(SecondController.class)
static Object provideSecondController(DepA depA, DepC depC) {
return new SecondController(depA, depC);
}
}
然后在Component中,我们可以通过它的名字得到这个Map的一个实例。此映射的值类型应为 Provider<Object>
,因为我们希望在每次 FXMLLoader
需要时获取控制器的新实例。
@Singleton
@Component(modules = MyModule.class)
public interface MyDiContainer {
// ********************** CONTROLLERS **********************
@Named("Controllers")
Map<Class<?>, Provider<Object>> getControllers();
}
最后,在您的 FXML 加载代码中,您应该设置新的 ControllerFactory
MyDiContainer myDiContainer = DaggerMyDiContainer.create()
Map<Class<?>, Provider<Object>> controllers = myDiContainer.getControllers();
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(type -> controllers.get(type).get());