Dependency injection looks hard, but really really isn’t. In fact, JavaFX has some dependency injection baked into it if you know how to use it.

As with anything else, there are different types of dependency injection, and if you want to manually control the process using reflection, it can get complicated. But that doesn’t mean you have to. In fact, we can achieve massive gains in functionality without using reflection at all.

What you’ll achieve in this tutorial

We’ll go through two ways to customise the FXMLLoader to give it ways to ‘inject’ information into our Controllers:

  • Constructor injection: Customise your Controller as its created by injecting data into the constructor method.
  • Field injection: Inject fields into the Controller based on resource files on your machine.

Finally, we’ll set everything up as part of your application life-cycle management class. In fact by the end of this tutorial, we’ll be able to set up a completely custom Controller and a View with a single line of code.

Parent root = InjectionManager.load("/sample.fxml");

Wait, if Dependency Injection’s hard, why are we doing it?

I just want to take a second to go through why this is a good idea (if you’re sold, jump ahead). I’m asking you to take a leap with me, and make your code a little mode complicated now. And I’m asking you to trust me that it’s going to improve your application by making it less complicated in the future.

So how’s it going to do that?!

We often have Views for different reasons – user logins, customer information, shopping cards, maybe a ‘recent downloads’, who knows.

Whatever it is, we want to give our users a great experience by letting them move between views. A good use case would me letting them launch some user details view from a shopping card.

We can use shoppingCart.java – the calling class – to populate the user details view it creates. So, we load the view, get the controller, and quickly write some code in shoppingCart.java to look up the user’s details in a database.

Without dependency injection, it's the responsibility of the calling class to populate the view
If you haven’t seen a sequence diagram before, work from top to bottom. First we call load() on the FXMLLoader, which creates the default controller, then we get the controller back, and so on.

Two days later, we’re coding slightlyDifferentClass.java, which shows a wish list, and we want that to load the user view too. Suddenly we’re forced to copy and paste all that database code into slightlyDifferentClass.java.

For every new view that needs that functionality, we copy and paste that code again.

Encapsulating responsibility

A better approach is to provide the FXMLLoader with the tools it needs to populate that Controller (and View) itself. Honestly, the calling class shouldn’t have had the responsibility to load user data in the first place. Then we can create the user view in any number of ways and not have to copy that code.

By creating a dependency injection manager, we can simplify the process of loading a view considerably.

Here, we’ve created a Callback function. That’s executable code that builds our user view – which adds some complexity to our application. But the important difference is that shoppingCart.java no longer has to know about the user database. And neither does slightlyDifferentClass.java or any other controller we create.

And, once all that code is tidied into an Injection Manager, we can set it up at the beginning of an application, and never have to worry about it again.

1. Strategies for Dependency Injection

In JavaFX, mastering dependency injection is about knowing how JavaFX pushes information around in the background. So, if you’re not familiar with how the loader works, take a second to check out my article on how the FXMLLoader works before you carry on.

Other than that, let’s get going.

In this section, we’ll run through the basic code you’ll need to get dependency injection up and running in your program. We’ll go through constructor and field injection in turn.

Before we start, we’ll quickly run through the parameters we can provide to the FXMLLoader and how they’re used during loading. The rest of the tutorial will be spent changing these parameters so we can get the FXMLLoader to do what we want!

Parameters for loading

When we don’t need anything custom, we can create a view using just the location of the FXMLLoader:

FXMLLoader loader = new FXMLLoader(getClass().getResource("/sample.fxml"));

However, we can provide the FXMLLoader object with three additional parameters:

  • BuilderFactory
  • ControllerFactory Callback
  • ResourceBundle

Here’s the prototype for the fully parameterised constructor:

FXMLLoader loader = new FXMLLoader(
        getClass().getResource("/sample.fxml"),
        ResourceBundle.getBundle("greetings"),
        new JavaFXBuilderFactory(),
        new Callback<Class<?>, Object>() {
            @Override
            public Object call(Class<?> param) {
                //return custom Controller class
            }
        }
);

Or, we can provide these parameters afterwards:

loader.setLocation(getClass().getResource("fxmlFileLocation.fxml"));
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setControllerFactory((Callback<Class<?>, Object>) controllerClass -> {
    //return a custom Controller
});
loader.setResources(ResourceBundle.getBundle("bundleName"));

Each element provides the opportunity to customise the View and the Controller. They must be set before calling loader.load().

Once we call loader.load(), we set into play a series of internal method calls that build the scene graph, create and initialize our controller, and fetch the root node.

FXMLLoader injects information into the controller and the view when provided with the correct methods and resources

a. Constructor Injection

Constructor injection is a process of creating a custom Controller object during the process of loading the View. For this, we’ll be using the setControllerFactory() method of the FXMLLoader.

The parameter you pass to the FXMLLoader might be called controllerFactory, but it’s absolutely nothing more than a set of instructions: executable code that tells the FXMLLoader the right way to make the Controller object.

The way we’ll create that executable code is with the Callback object: a functional interface that we’ll use to create our custom controller.

By creating a dependency injection manager, we can simplify the process of loading a view considerably.

Creating a Callback

We can either create a Callback using the new keyword, or as a lambda,which makes our code a little more concise.

Callback<Class<?>, Object> controllerFactory = param -> {
    UserData data = getUserData(UserManager.getCurrentUser());
    return new Controller(data);
};

Now, instead of setting the user data after creation, we give the FXMLController what it needs in advance of loading the View.

//previously...
UserData data = getUserData(UserManager.getCurrentUser());
loader.load();
Controller controller = loader.getController();
controller.setUserData(data);

//now...
loader.setControllerFactory(controllerFactory)
loader.load();

I get that seems like a really small change right now – but wait until we automate this in the final section. I promise you it’s going to make a difference.

b. Field Injection

The second common use case for Dependency injection in View scenarios is common phrases greetings or phrases you want to tailor to the situation. For example, an application you want to distribute to multiple countries, but you don’t want to have to build it over and over again.

Ideally, you want to be able to package the program with multiple languages and then have the FXMLLoader select the right language based on information it has – like locale – at the time.

This is where the ResourceBundle parameter becomes more important. It gives the FXMLLoader the opportunity to reach into that bundle, search for keywords that were flagged in the FXML file, and fill them.

Setting FXML flags

To set our FXML file so that it’s primed to tell the FXMLLoader a parameter needs injecting. We can do this with the % character.

As a simple example, we’ll create a Label in our scene. Then, we’ll prime it for some text to welcome our user.

<GridPane alignment="center" hgap="10" vgap="10" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.2-internal" fx:controller="com.edencoding.Controller">
    <Label text="%welcome" />
</GridPane>

Creating a ResourceBundle

Now we’ve created set our Label to expect a value, we need to create a ResourceBundle to fill it. That’s also really easy (I’m not kidding)

A ResourceBundle isn’t specific to JavaFX. In fact, it’s part of the core java.util package alongside Arrays and Date. And a ResourceBundle is just a group of systematically-named property files. The basic syntax for a resource bundle file is this:

resourceName_languageCode_regionCode.properties

These files are so common that many IDEs bundle them together so they appear to be in a directory of their own. In the file system itself, they’re simple separate property files.

In our simple case, we’ve included support for english, french and german although you do much much more.

Each file is set up with a single property in our example:

Text in greetings_en_GB.properties:
welcome=Hello!

Text in greetings_fr_FR.properties:
welcome=Bonjour!

Text in greetings_de_DE.properties:
welcome=Guten Tag!

Finally, we tie these together by loading this ResourceBundle ahead of loading our FXML. Then, we set it in the FXMLLoader instance we create:

FXMLLoader loader = new FXMLLoader(getClass().getResource("/sample.fxml"));
loader.setResources(ResourceBundle.getBundle("greetings", Locale.GERMANY));
loader.load();

By specifying the Locale, we tell Java which translation we’ll want ahead of time. Then, the FXML loader injects those into our application. Here’s three outputs from programmes using Locale.ENGLISH, Locale.FRANCE and Locale.GERMANY:

By using a ResourceBundle we can load different text into a JavaFX view easily

We could also use the ResourceBundle to help us set some multilingual application titles, but we’ll leave it for now.

Instead, let’s automate this process so we don’t have to do this every time we use the FXMLLoader…

2. Automating Dependency Injection

If you’re new to JavaFX, a natural question to ask is ‘what logic should go in Main.java?” It often seems like you use it to open the first Controller, and that’s basically it.

Well, this is the stuff. Application-level logic should be managed by the application itself, rather than by individual controllers, and that’s what we’ll implement right now.

We’re going to take three steps to automating dependency injection

  1. Encapsulate all the logic in a DependencyInjection class
  2. Have it store the callbacks and return them at the right time
  3. Get it to store the ResourceBundle and Locale

Finally, we’ll get it to load the FXML files for us and just return the root element and controller.

Creating DependencyInjection.java

To start, we’ll create a Dependency Injection class. This is where all the logic for dependency injecction so we properly encapsulate that code.

We can set it up from the Application, but the responsibility for actually injecting should be with the injector.

I’ve set it up inside its own package, but you’re welcome to keep it with the Main class if you want.

Storing callbacks for Controllers

We want our Dependency Injector to remember two things – which classes it knows the controller factories for, and which controller factories correspond to that class.

That just SHOUTS HashMap. Remembering that the controller factory is actually just a Callback, we’ll set it up right at the top of DependencyInjector.java:

/**
* A map of all Controllers that can be injected, and the methods responsible for loading them.
**/
private static final Map<Class<?>, Callback<Class<?>, Object>> injectionMethods = new HashMap<>();

Now we’ve got that, we need to start a conversation with our DependencyInjector. Fundamentally, we don’t want to ask it to load our constructor and then have to check whether that’s been successfully completed. We want it to do that logic itself. Something like:

  • Calling class: “Please load this controller for me”
  • DependencyInjector: “OK, gimme a second”
  • *DependencyInjector rummages around furiously*
  • DependencyInjector: I couldn’t find a method, so here’s just the default controller

So, we’ll need three methods:

  1. Check whether we have a saved controller factory for a class
  2. If we do, use it
  3. If we don’t, use the class’s default constructor

Some key points for this code:

  • Because the callback takes the class and returns the fully-functionalized controller, all we need to do is run the call() method and it will automatically do our job for us
  • controller.getConstructor().newInstance() is the best way to get a new instance of a class using its default controller
/**
* Determine whether a stored method is available 
* If one is, return the custom controller
* If one is not, return the default controller
* @param controllerClass the class of controller to be created
* @return the controller created
*/
private static Object constructController(Class<?> controllerClass) {
    if(injectionMethods.containsKey(controllerClass)) {
        return loadControllerWithSavedMethod(controllerClass);
    } else {
        return loadControllerWithDefaultConstructor(controllerClass);
    }
}

private static Object loadControllerWithSavedMethod(Class<?> controller){
    try {
        return injectionMethods.get(controller).call();
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
}

private static Object loadControllerWithDefaultConstructor(Class<?> controller){
    try {
        return controller.getConstructor().newInstance();
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        throw new IllegalStateException(e);
    }
}

Now we have a system where we can look up the controller factory, let’s add some methods to add and remove items from the map. That’s as simple as a few accessor methods.

public static void addInjectionMethod(Class<?> controller, Callback<Class<?>, Object> method){
    injectionMethods.put(controller, method);
}

public void removeInjectionMethod(Class<?> controller){
    injectionMethods.remove(controller);
}

We’ll automate the rest later, but for now, let’s move on to adding a ResourceBundle.

Storing the ResourceBundle and locale

There are a few ways to do this, but because we don’t need to change the Locale during the execution of our programme, we’ll simply load up a ResourceBundle at the start.

private static ResourceBundle bundle = null;

public static void setBundle(ResourceBundle bundle) {
    DependencyInjection.bundle = bundle;
}

You can add a getter if you want, but for now, let’s move on to automating it!

Adding the automation.

Absolutely finally, what we want to do is get the DependencyInjector to load up our FXML file for us knowing only the name of the FXML file (well, it’s location!)

To do this, we’ll create a method that produces the FXMLLoader we need, and a convenience method that will create the FXMLLoader, run it and return the root node. We’ll use this convenience method most of the time because we won’t need to inject information into the controller after loading.

If we do need to set data on the controller after loading, we’ll execute getLoader(), load the FXML ourselves using loader.load(), and then ask for the controller as usual.

public static Parent load(String location) throws IOException {
    FXMLLoader loader = getLoader(location);
    return loader.load();
}

public static FXMLLoader getLoader(String location) {
    return new FXMLLoader(
            DependencyInjection.class.getResource(location),
            bundle,
            new JavaFXBuilderFactory(),
            controllerClass -> constructController(controllerClass));
}

Just watch out! Neither we nor the FXMLLoader know what the class is going to be based only on the name of the FXML file. But, it will know the class by the time it comes to construct it. So, our controllerFactory is just a reference to the constructController() method, which is going to take the class that FXMLLoader gives it, and return the controller we want.

To save you the trouble of setting this up from snippets of code, here’s the full code for the DependencyInjector class:

Finally, we’ll set up the DependecyInjector from the Main class. Now, bear with me because we don’t actually have a user database, or any custom stuff, so you’ll have to modify this code to fit your situation, but still.

Because I’m a fan of clean code, we’ll create a method to set up the dependency injector and run it from the start method.

@Override
public void start(Stage primaryStage) throws Exception {
    setUpDependecyInjector();
        
    Parent root = DependencyInjection.load("/sample.fxml");
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(root, 300, 275));
    primaryStage.show();
}

private void setUpDependecyInjector() {
    //set bundle
    DependencyInjection.setBundle(ResourceBundle.getBundle("greetings", Locale.ENGLISH));

    //create factories - here we'll just create one!
    Callback<Class<?>, Object> controllerFactory = param -> {
        UserData data = getUserData(UserManager.getCurrentUser());
        return new Controller(data);
    };

    //save the factory in the injector
    DependencyInjection.addInjectionMethod(
            Controller.class, controllerFactory
    );
}

Notice now we can set up the view with a single command: Parent root = DependencyInjection.load("/sample.fxml");

If we were feeling very fancy, we could even in-line the variable with the setScene() method, to save us the extra variable.

Conclusions

Dependency injection is a method for correcting the responsibility-imbalance created by the MVC design pattern. Instead of every controller being responsible for setting up views it creates, we abstract that responsibility to a higher, application level.

Dependency injection in FXML can be achieved by providing an FXMLLoader object with a custom method or resource for use during loading.

We have set this up so that it can be configured at an application level, and referenced throughout the application.