Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject services into JavaFX controllers using Dagger 2

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.

like image 475
Sebastian S Avatar asked Jul 06 '15 18:07

Sebastian S


2 Answers

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

like image 109
Sebastian S Avatar answered Nov 15 '22 19:11

Sebastian S


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());
like image 41
BeshEater Avatar answered Nov 15 '22 21:11

BeshEater