Skip to the content.

How to start?

In the following section you will find a step-by-step guide on how to start a new project using FulibFx. The guide will cover the entire process of re-creating the base for the board game Ludo. Some parts such as Dagger and RxJava will only be covered briefly, as they are not the main focus of this tutorial. The game logic itself also plays a minor role in this tutorial, as the code can be looked up in the ludo example.

This tutorial is based on gradle and IntelliJ IDEA, but you can use any other IDE or build tool.

Prerequisites/Dependencies

To start a new project, set up a new workspace directory in your IDE and add the required dependencies. Assuming you are already installed JavaFX, Dagger and RxJava, you can add the following dependencies to your build.gradle file:

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.fulib:fulibFx:VERSION'
    implementation 'org.fulib:fulibFx-processor:VERSION'
}

After adding the dependencies, refresh your project and you are ready to start.

Create a new project

Like with JavaFX, the first step of starting a new project is to create a new class which will be the starting point of your application. Instead of extending Application, we will extend FulibFxApp and override the start method.

Make sure to call super.start(primaryStage) to initialize the framework. Otherwise, the application will not work as expected.

During the start method, we will later configure things like routing and show the first view. For now, we can add some basic configuration such as configuring the resource path and setting up the auto refresher.

public class App extends FulibFxApp {

    @Override
    public void start(Stage primaryStage) {
        try {

            // Starting the framework, initializes all the necessary components
            super.start(primaryStage);

            // Setting the resource path to the resources folder of the project (required for reloading in dev)
            // If the resource path is not set, the framework will use the default resource path (src/main/resources)
            setResourcesPath(Path.of("ludo/src/main/resources/"));

            // Setting the path which the auto refresher should watch (required for auto-reloading in dev)
            autoRefresher().setup(Path.of("ludo/src/main/resources/de/uniks/ludo"));

            // ...
        } catch (Exception e) {
            // If an error occurs while starting the application, we want to log it
            LOGGER.log(Level.SEVERE, "An error occurred while starting the application: " + e.getMessage(), e);
        }
    }
}

When using Dagger, add the following snippet module to provide the FulibFxApp to internal components using your app class.

@Module
public class MainModule {

    // ...

    @Provides
    FulibFxApp app(LudoApp app) {
        return app;
    }
}

The app can now be started by creating a main method and calling the launch method of the Application class.

public static void main(String[] args) {
    Application.launch(App.class, args);
}

Right now, nothing will happen when you run the application, as we have not yet added any content. In the next section, we will create a simple controller with a view and add it to the application.

Controllers and Views

In FulibFx, the application is built using controllers and views. A controller describes the behavior of the application and links the model with the view, which defines the look of the frontend.

In order to set up a basic controller, create a new class and annotate it with @Controller. This will mark the class as a controller for later usage in the framework. In this example, we will create a controller for configuring the amount of players for our ludo game.


@Controller
public class SetupController {

    @Inject
    public SetupController() {
        // Required for creating instances using Dagger
        // See https://dagger.dev/tutorial/
    }

}

Right now, our controller doesn’t do much and if we try to display its view, the application would crash as we haven’t configured anything yet.

In order to set the controller’s view, the @Controller annotation can be configured with a path specifying an FXML file. The path is always relative to the package of the class.

// Path would be "src/main/resources/de/uniks/controller/Setup.fxml" for example
@Controller(view = "Setup.fxml")
public class SetupController {
    // ...
}

Often times the name of the controller class and the name of the FXML will match each other. In order to remove the need of having to retype the basically same name twice, a default value will be used if no path is provided. This will be the name of your class with Controller or Component removed. The class name MySetupController would be transformed to MySetup.fxml.

As the names would be the same in our example, we will remove it again for simplicity’s sake.

If the controller were to be displayed now, the FXML file specified using the annotation would be used to describe its view. If the fx:controller attribute is set in the FXML file, the controller will be automatically linked to the view so that @FXML annotated fields can be used to access the view’s elements.

In our example the view will contain a label, a slider and a button to start the game. The button is linked with the onPlayClicked method, which will be called when the button is clicked.


@Controller
public class SetupController {

    @FXML
    private Slider playerAmountSlider;

    @Inject // Using dagger to inject the app
    App app;

    @Inject
    public SetupController() {
    }

    @FXML
    public void onPlayClick() {
        int playerAmount = (int) this.playerAmountSlider.getValue();
        // TODO: Start the game with the given amount of players
    }
}

All other controllers can be created in the same way.

Routing

In order to display the view of the controller, we need to add it to the application’s routing. Routes can be configured by creating a new class containing Provider<?> fields for each controller.

public class Routing {

    @Inject
    @Route("")
    // The empty route is the default route. It is often used as the starting point of the application.
    public Provider<SetupController> setupController;

    @Inject
    @Route("ingame")
    // Routes can be used to show controllers. Using the route "/ingame" will show the IngameController.
    public Provider<IngameController> ingameController;

    @Inject
    @Route("ingame/gameover")
    public Provider<GameOverController> gameOverController;

    // ...
}

The @Route annotation is used to specify the route of the controller. The provider is used to create a new instance of the controller when the route is accessed.

In order to register the routing, the Routing class needs to be set using the registerRoutes(Object router) method in the FulibFxApp class. To display the view of the controller, the show method can be used. This method takes the route of the controller and an optional Map<String, Object> containing parameters for the controller.

public class App extends FulibFxApp {


    @Override
    public void start(Stage primaryStage) {
        try {
            super.start(primaryStage);
            // ...
            Routing routing = ...; // Create an instance, for example using Dagger
            registerRoutes(routing);
            show(""); // Show the setup view
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "An error occurred while starting the application: " + e.getMessage(), e);
        }
    }
}

Using the show method, the view of the SetupController will be displayed when the application is started. Now we can start the application and see the view of the SetupController.

Initialization, Rendering and Destruction

The lifecycle of a controller is divided into three phases: initialization, rendering and destruction. Between rendering and destruction, the controller is in an active state and can be used to interact with the view.

The initialization phase is used to set up the controller and configure some initial values. This phase is called after the controller is created and before the view is rendered.

The rendering phase is used to display the view of the controller. This phase is called after the initialization phase when the controller view has been created and is ready to be displayed.

The destruction phase is used to clean up the controller and remove any references to the view. This phase is called after the view has been removed and the controller is no longer needed.

In order to add custom behavior to the lifecycle of a controller, the @OnInit, @OnRender and @OnDestroy annotations can be used to mark methods which should be called during the respective phase. If these methods have to be executed in a specific order, an additional integer parameter can be used to specify the execution order.

The following method will be called after the controller’s view has been loaded and is ready to be displayed.


@OnRender
public void drawBoard() {
    for (Field field : this.game.getBoard().getFields()) {
        Circle circle = createFieldCircle(field);
        this.boardPane.getChildren().add(circle);
        this.fieldToCircle.put(field, circle);
    }
}

These methods can be used to initialize the controller and configure its view, but for more complex behavior where we have to pass information to the controller, the @Param annotation can be used.

Parameters

In order to start the game, we have to somehow pass the amount of players to the IngameController. This can be done by adding parameters to the route when showing the controller.

public class SetupController {
    // ...
    @FXML
    public void onPlayClick() {
        int playerAmount = (int) this.playerAmountSlider.getValue();
        app.show("ingame", Map.of("playerAmount", playerAmount));
    }
}

These parameters can be injected into the controller using the @Param annotation.

@Controller
public class IngameController {
    // ...

    @OnInit
    public void setup(@Param("playerAmount") int playerAmount) {
        if (this.game == null) this.game = this.gameService.createGame(playerAmount);
        this.currentPlayer.set(this.game.getCurrentPlayer());
    }
}

In this example, when using show("ingame", Map.of("playerAmount", playerAmount)), the playerAmount parameter will be injected into the setup method of the IngameController when the controller is initialized.

Besides from event methods, the annotation can also be used to mark fields and setter methods. For more information, see the documentation.

Resources

JavaFX supports the usage of language files to provide translations for the application. In order to use language files in FulibFx, the @Resource annotation can be used to mark fields which should be used as the resource bundle for the controller the field is in.

@Controller
public class IngameController {

    @Resource
    ResourceBundle bundle;

    // ...
}

When loading the controller’s view, the marked resource bundle will be used to provide translations for the keys specified in the FXML file.

It can also be used to translate titles into different languages.

Titles

In order to set the title of the application, the @Title annotation can be used on the controller class. This will set the title of the application to the value of the annotation.

@Title("Set up the game")
@Controller
public class SetupController {
    // ...
}

Since we already set up a resource bundle for the IngameController, we can use it to translate the title into different languages by using a key instead of a fixed value.

@Title("%ingame.title")
@Controller
public class IngameController {
    // ...
}

In order to specify what exactly our titles should look like when applied to the application, we can change the format of the title using the setTitlePattern method in the FulibFxApp class.

public class App extends FulibFxApp {

    @Override
    public void start(Stage primaryStage) {
        try {
            super.start(primaryStage);
            // ...
            setTitlePattern("Ludo - %s");
            setTitlePattern(title -> "Ludo - " + title); // Same as above
            // ...
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "An error occurred while starting the application: " + e.getMessage(), e);
        }
    }
}

Subscriber

Often times you have to link object’s properties together or listen to changes in a property. JavaFX and RxJava offer many convenient ways to do this, but all of them have to be cleaned up manually.

In FulibFx, a Subscriber can be used to listen to changes in a property and automatically clean up the subscription when the controller is destroyed. Instead of having to dispose every subscription manually, the Subscriber will take care of it for you when calling its dispose method in the @OnDestroy phase. The Subscriber contains many different methods to listen to changes in a property, bind properties together, subscribe to an observable and more.

@Controller
public class IngameController {

    @Inject // Injected by Dagger for convenience
    Subscriber subscriber;

    @OnRender
    public void setupTexts() {
        this.subscriber.listen(
                game.listeners(),
                Game.PROPERTY_CURRENT_PLAYER,
                evt -> this.currentPlayer.set((Player) evt.getNewValue())
        );
        this.subscriber.bind(this.playerLabel.textProperty(), this.currentPlayer.map(Player::getId).map(id -> this.bundle.getString("ingame.current.player").formatted(id)));
    }

    @OnDestroy
    public void destroy() {
        // ...
        this.subscriber.dispose();
    }
    
    // ...

}

Subcomponents

Components are a special type of controller which can be used inside other controllers to split the application into smaller parts.

In order to create a component, the @Component annotation can be used to mark the class as a component. Every component also is a controller and can be used in the same way as a controller.

@Component(view = "Dice.fxml")
public class DiceSubComponent extends VBox {
    
    @FXML
    public Label eyesLabel;
    
    @OnRender
    public void render() {
        // ...
    }
}

The main difference between creating a component and a controller is that a component is a subclass of a JavaFX parent. This means that a component can be used as a JavaFX node and can be added to the view of another controller.

Setting the view works slightly different from the view of a controller as well. As you can see in the example, the FXML file is specified even though the name of the component and the FXML file match. This is because if you leave the view attribute empty, the framework will use the class itself as the view. If the FXML file is specified, the framework will load the FXML file and set the class object as the root node of the FXML file. This means that the FXML file has to contain an fx:root element.


<fx:root  type="VBox" fx:controller="de.uniks.ludo.controller.sub.DiceSubComponent">
    <Label fx:id="eyesLabel" text="🎲"/>
    ...
</fx:root>

In order to use the component in another controller, create a field in the controller and annotate it with @SubComponent.

@Controller
public class IngameController {

    @SubComponent
    @Inject // Injected by Dagger
    public DiceSubComponent dice;

    // ...
}

Subcomponents will be initialized and rendered together with its parent controller. For more information, see the documentation.

The component can now be used as a JavaFX node and added to the view of the controller. As the component is a javafx node, it can also be added in the FXML file directly. See the documentation for more information.

public class IngameController {

    @FXML
    @Inject
    @SubComponent
    DiceSubComponent diceSubComponent;

    @OnRender
    public void setupDice() {
        this.diceSubComponent.setOnMouseClicked(event -> {
            // ...
        });
    }
    
    // ...
}
<VBox fx:controller="de.uniks.ludo.controller.IngameController">
    <Label fx:id="playerLabel" text="%ingame.current.player" />
    <AnchorPane fx:id="boardPane" />
    <DiceSubComponent fx:id="diceSubComponent" />
</VBox>

Conclusion

In this tutorial, we have learned how to start a new project using FulibFx and how to create controllers and views. We have also learned how to configure the routing and how to pass parameters to controllers. In addition, we have learned how to use resources, titles, subscribers and subcomponents. For more information about the different features of FulibFx, see the documentation.


Overview