【问题标题】:How to inject services into JavaFX controllers using Dagger 2如何使用 Dagger 2 将服务注入 JavaFX 控制器
【发布时间】:2020-01-22 07:40:06
【问题描述】:

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 依赖项注入此控制器类的构造函数,我会遇到问题,因为如果我使用 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(...); 也许这是一个很好的起点。但我对这些库没有足够的经验,不知道如何解决这个问题。

【问题讨论】:

  • FXML 主要是一种 XML 模式,用于描述 JavaFX 的对象图,但它可以用于描述任何对象。您可以使用 FXML 来描述您的依赖项并由 FXML 加载程序注入吗?说实话,将服务混合到您的视图中听起来可能违反了关注点分离。这将是一个好地方,例如,使用可以注入服务引用的 Enterprise JavaBean,同时保留 MVC 设计提供的解耦。
  • 您始终可以在代码中创建控制器,并在您的FXMLLoader 上调用setController,而不是使用fx:controller 属性指定它。或者,您可以使用controllerFactory,它将fx:controller 属性中指定的类映射到实际实例。
  • @James_D 是的,与此同时,我得出了同样的结论。调用 setController 是 唯一的方法,因为依赖项在编译时解决。基于反射的控制器工厂根本不可能。但是,不得在 .fxml 文件中指定控制器以避免冲突。如果没有人先发帖,我明天会自己回答这个问题(甚至可能在 github 上添加一个示例项目)。
  • 我不知道 Dagger,所以我在这里可能错了,但 controllerFactory 真的不必是基于反射的。 Here's a Spring example,如果你知道春天。

标签: java dependency-injection javafx dagger-2


【解决方案1】:

自定义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获得

【讨论】:

  • 这是一个非常轻量级的 DI 框架的有趣示例:afterburner.fx
【解决方案2】:

感谢来自@Sebastian_S 的Map multibinding mechanism 提示,我已经设法使用Map&lt;Class&lt;?&gt;, Provider&lt;Object&gt;&gt; 进行自动控制器绑定,将每个控制器映射到其类。

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

然后在 Component 中,我们可以使用它的名字来获取这个 Map 的一个实例。这个映射的值类型应该是Provider&lt;Object&gt;,因为我们希望每次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 和 Dagger 几年之后,这是我认为目前最好的做法。谢谢分享。我什至会进一步简化它,不使用@Provides,而是使用@Binds,就像我现在使用in this project一样。
【解决方案3】:

或者,您可以执行以下操作:

...

  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 检查),但必须明确列出所有类绝对是一个缺点。

【讨论】:

  • 有趣的方法。我想当我有时间的时候我会摆弄这个!
  • 我们可以使用查找 Map 和 Dagger 的 Map multibinding mechanism 来代替 switch 语句
【解决方案4】:

对于许多人来说,这可能很久以前就解决了。我不喜欢这里描述的解决方案,因为它依赖于类名或对干净设计的反射。我写了一个有点不同的,看起来更易于维护的。

它的要点是使用 Dagger 创建注入到Stage 中的场景。这是我的应用程序类

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 实例的创建。它没有在其他任何地方使用,我妥协了。

【讨论】:

  • 事实证明,在 Dagger 2 配置中保持对控制器的引用很重要。所以我最终返回了一个同时捕获控制器和场景的对象。这样我可以在其他地方引用控制器。例如,在网络上侦听的事件调度程序现在可以将事件发布到 UI...
猜你喜欢
  • 2016-08-31
  • 2013-05-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多