JavaFX itself has some means of DI to allow binding between XML-described UIs and controllers:
<Pane fx:controller="foo.bar.MyController">
<children>
<Label fx:id="myLabel" furtherAttribute="..." />
</children>
</Pane>
The 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 to work, I can not just create an instance of MyController. Instead I have to ask JavaFX to do stuff 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(...);
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. 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());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With