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.