简体   繁体   English

如何使用 Dagger 2 将服务注入 JavaFX 控制器

[英]How to inject services into JavaFX controllers using Dagger 2

JavaFX itself has some means of DI to allow binding between XML-described UIs and controllers: JavaFX 本身有一些 DI 方法来允许在 XML 描述的 UI 和控制器之间进行绑定:

<Pane fx:controller="foo.bar.MyController">
  <children>
    <Label fx:id="myLabel" furtherAttribute="..." />
  </children>
</Pane>

The Java-side looks like this: 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!");
    }

}

For this to work, I can not just create an instance of MyController.为此,我不能只创建 MyController 的实例。 Instead I have to ask JavaFX to do stuff for me:相反,我必须要求 JavaFX 为我做一些事情:

FXMLLoader loader = new FXMLLoader(MyApp.class.getResource("/fxml/myFxmlFile.fxml"), rb);
loader.load();
MyController ctrl = (MyController) loader.getController();

So far, so good到现在为止还挺好

However, if I want to use Dagger 2 to inject some non-FXML-dependencies into the constructor of this controller class, I have a problem, as I have no control over the instantiation process, if I use JavaFX.但是,如果我想使用 Dagger 2 将一些非 FXML 依赖项注入到这个控制器类的构造函数中,我有一个问题,因为我无法控制实例化过程,如果我使用 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!");
    }

}

There is one API that looks promising: loader.setControllerFactory(...);有一种 API 看起来很有前途: loader.setControllerFactory(...); Maybe this is a good point to start with.也许这是一个很好的起点。 But I do not have enough experience with these libraries to know how to approach this problem.但是我对这些库没有足够的经验来知道如何解决这个问题。

A custom ControllerFactory would need to construct Controllers of certain types only known at runtime.自定义ControllerFactory将需要构造仅在运行时已知的某些类型的控制器。 This could look like the following:这可能如下所示:

T t = clazz.newInstance();
injector.inject(t);
return t;

This is perfectly ok for most other DI libraries like Guice, as they just have to look up dependencies for the type of t in their dependency graph.这对于像 Guice 这样的大多数其他 DI 库来说是完全可以的,因为它们只需要在它们的依赖图中查找t类型的依赖关系。

Dagger 2 resolves dependencies during compile time. Dagger 2 在编译时解决依赖关系。 Its biggest features is at the same time its biggest problem: If a type is only known at runtime the compiler can not distinguish invocations of inject(t) .它最大的特点同时也是最大的问题:如果一个类型只在运行时已知,编译器就无法区分inject(t)调用。 It could be inject(Foo foo) or inject(Bar bar) .它可以是inject(Foo foo)inject(Bar bar)

(Also this wouldn't work with final fields, as newInstance() invokes the default-constructor). (这也不适用于 final 字段,因为newInstance()调用默认构造函数)。


Ok no generic types.好的,没有泛型类型。 Lets look at a second approach: Get the controller instance from Dagger first and pass it to the FXMLLoader afterwards.让我们看看第二种方法:首先从 Dagger 获取控制器实例,然后将其传递给 FXMLLoader。

I used the CoffeeShop example from Dagger and modified it to construct JavaFX controllers:我使用了 Dagger 中的 CoffeeShop 示例并对其进行了修改以构建 JavaFX 控制器:

@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
    Provider<CoffeeMakerController> coffeeMakerController();
}

If I get a CoffeeMakerController, all its fields are already injected, so I can easily use it in setController(...) :如果我得到一个 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();

My FXML file must not contain a fx:controller attribute, as the loader would try to construct a controller, which of course stands in conflict with our Dagger-provided one.我的 FXML 文件不能包含 fx:controller 属性,因为加载程序会尝试构造一个控制器,这当然与我们的 Dagger 提供的控制器相冲突。

The full example is available on GitHub完整示例可在GitHub找到

Thanks to Map multibinding mechanism hint from @Sebastian_S I've managed to make automatic controller binding using Map<Class<?>, Provider<Object>> that maps each controller to its class.感谢来自@Sebastian_S 的Map 多重绑定机制提示,我设法使用Map<Class<?>, Provider<Object>>将每个控制器映射到其类来进行自动控制器绑定。

In Module collect all controllers into Map named "Controllers" with corresponding Class keys在 Module 中,将所有控制器收集到名为“Controllers”的 Map 中,并带有相应的 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);
    }
}

Then in Component, we can get an instance of this Map using its name.然后在 Component 中,我们可以使用其名称获取此 Map 的实例。 The value type of this map should be Provider<Object> because we want to get a new instance of a controller each time FXMLLoader needs it.这个映射的值类型应该是Provider<Object>因为我们想要在每次FXMLLoader需要它时获得一个控制器的新实例。

@Singleton
@Component(modules = MyModule.class)
public interface MyDiContainer {
    // ********************** CONTROLLERS **********************
    @Named("Controllers")
    Map<Class<?>, Provider<Object>> getControllers();
}

And finally, in your FXML loading code, you should set new ControllerFactory最后,在您的 FXML 加载代码中,您应该设置新的 ControllerFactory

MyDiContainer myDiContainer = DaggerMyDiContainer.create()
Map<Class<?>, Provider<Object>> controllers = myDiContainer.getControllers();

FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(type -> controllers.get(type).get());

Alternatively you can do something like:或者,您可以执行以下操作:

...

  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;
        }
     }
  });

...

As @Sebastian_S noted, a reflection-based controller factory is not possible.正如@Sebastian_S 所指出的,基于反射的控制器工厂是不可能的。 However calling setController is not the only way, I actually like this setControllerFactory approach better because it doesn't break the tooling (eg IntelliJ's XML inspections) but having to explicitly list out all the classes is definitely a drawback.然而,调用 setController 并不是唯一的方法,我实际上更喜欢这种 setControllerFactory 方法,因为它不会破坏工具(例如 IntelliJ 的 XML 检查),但必须明确列出所有类绝对是一个缺点。

This is solved probably long ago for many people.对于很多人来说,这可能很久以前就已经解决了。 I did not like the solution described here in as it relies on class names or reflection over clean design.我不喜欢这里描述的解决方案,因为它依赖于类名或反射而不是干净的设计。 I wrote a bit different one that looks more maintainable to my eyes.我写了一个有点不同的,在我看来更易于维护。

The gist of it is to use Dagger to create the Scene that is injected into the Stage .它的要点是使用 Dagger 创建注入Stage的 Scene 。 Here is my Application 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();
}

I have in my Dagger 2 module the logic for loading fxml and customization of the controller ie injecting the SsdpClient我在我的 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);
    }
}

I can split further the creation of Parent instance.我可以进一步拆分Parent实例的创建。 It is not used anywhere else and I compromised.它没有在其他任何地方使用,我妥协了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM