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.

+3


source to share


3 answers


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

+3


source


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.

+1


source


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.

0


source







All Articles