How to inject services into JavaFX controllers with Dagger 2
JavaFX itself has some DI facility to allow binding between XML interfaces and controllers:
<Pane fx:controller="foo.bar.MyController">
<children>
<Label fx:id="myLabel" furtherAttribute="..." />
</children>
</Pane>
Java side looks like this:
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, I cannot just create an instance of MyController. Instead, I have to ask JavaFX to do something for me:
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.
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(...);
This might be a good place to start. But I don't have enough experience with these libraries to know how to approach this problem.
source to share
The custom ControllerFactory
will need to create controllers of certain types that are only known at runtime. It might look like this:
T t = clazz.newInstance();
injector.inject(t);
return t;
This works great for most other DI libraries like Guice as they just need to look for type dependencies t
in their dependency graph.
Dagger 2 resolves dependencies at compile time. Its greatest capabilities are at the same time its biggest problem: if the type is known only at runtime, the compiler cannot distinguish between the calls inject(t)
. It can be inject(Foo foo)
or inject(Bar bar)
.
(Also this won't work with trailing fields as it newInstance()
calls the default constructor.)
There are no generic types. Let's take a look at the second approach: first, inject a controller instance from Dagger and then pass it to the FXMLLoader.
I used Dagger's CoffeeShop example and modified it to create JavaFX controllers:
@Singleton
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
Provider<CoffeeMakerController> coffeeMakerController();
}
If I receive a CoffeeMakerController, all of its fields are already entered, so I can easily use it in 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 should not contain the fx: controller attribute, as the loader will try to create a controller, which of course conflicts with our provided Dagger.
A complete example is available on GitHub
source to share
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 pointed out, a factory reflection based controller is not possible. However, calling setController is not the only way, I really like this setControllerFactory approach better because it doesn't break tooling (like IntelliJ XML validation), but for an explicit list of all classes is definitely a downside.
source to share
This solution is probably a long time ago for a lot of people. I didn't like the solution described here as it relies on class names or a reflection of clean design. I wrote a slightly different one that looks more comfortable on my eyes.
Its essence is to use the Dagger to create a scene that is entered into Stage
. 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 logic in Dagger 2 module to load fxml and configure controller i.e. SsdpClient input
@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 instantiation Parent
. It is not used anywhere and I have compromised.
source to share