How to inject services into JavaFX controllers using Dagger 2
A custom ControllerFactory
would need to construct Controllers of certain types only known at runtime. 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.
Dagger 2 resolves dependencies during compile time. 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)
. It could be inject(Foo foo)
or inject(Bar bar)
.
(Also this wouldn't work with final fields, as newInstance()
invokes the default-constructor).
Ok no generic types. Lets look at a second approach: Get the controller instance from Dagger first and pass it to the FXMLLoader afterwards.
I used the CoffeeShop example from Dagger and modified it to construct JavaFX controllers:
@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(...)
:
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.
The full example is available on 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.
In Module collect all controllers into Map named "Controllers" with corresponding Class keys
@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. 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.
@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
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. However calling setController is not the only way, I actually like this setControllerFactory approach better because it doesn't break the tooling (e.g. IntelliJ's XML inspections) but having to explicitly list out all the classes is definitely a drawback.