Introduction
I am developing JavaFX applications for about 5 years now and used different architecture patterns for it.
First i started with MVC, then MVVM and lastly we used MVP passive view in my current project.
The way how to apply the MVC pattern is described well in the JavaFX documentation, MVVM is described well in the documentation of the mvvmFX framework, but for the MVP Passive View i spent a lot of time reading different articles about it, but i didn´t find the way that feels 100% right for me.
Futhermore i didn´t find a satisfying article about how to coordinate the different views built with the MVP pattern.
So i evaluated different ways of creating a complex JavaFX UI with views following the MVP Passive View pattern and how to coordinate them.
My focus was:
- to avoid bidirectional dependencies between view and presenter
- find the best way to manage the domain-specific properties for the views
- parent components attach child components
- workflow controllers access the components.
- how and where to comunicate with the backend
So i came to following conclusion that feels good for me.
Structure of a ui component
Simple ui component with few view property bindings
Component Module
The whole component is bound in a context that manages the dependencies. I used guice so there is a private guice module that manages the internal dependencies.
More global modules that use this module have to ensure that all dependencies needed by the presenter are provided.
Presenter
The main purpose of the presenter is to provide domain-specific properties the view can bind on and to provide methods the view can straight delegate to.
It is responsible what information is shown, not how.
The presenter must not hold business logic, only logic about adapting between view and business logic.
The presenter does not know the view.
public class Presenter { private final StringProperty someString = new SimpleStringProperty(); void doSomething() { // not implemented yet } StringProperty someStringProperty() { return someString; } }
View
The view is responsible for how information is shown.
The view is am humble object. It does only three things:
- provide a package-private factory method
- bind the properties of the controls to the presenter´s properties and delegates the control´s actions to the presenter within the initialize method
- provide its root node by a public methods
public class View { @FXML private StackPane root; @FXML private Label label; @FXML private Button button; private final Presenter presenter; private View(Presenter presenter) { this.presenter = presenter; } static View createInstance(Presenter presenter) { View view = new View(presenter); FXMLLoader.load("....", view); return view; } @FXML void initialize(){ label.textProperty().bind(presenter.someStringProperty()); button.setOnAction(event -\> presenter.doSomething()); } public Node getRootNode() { return root; } }
More complex ui component with several view property bindings
To keep the presenter clean, we outsource the view properties to a kind of view model.
The view model is only a helper class for the view and the presenter, so its access level only needs to be package-private.
Nested ui components
Nest a ui component as a child ui component (1:1 nesting)
If you want to nest a child view you need to install its module in the parent´s module.
So you can simply inject the exposed view of the child component into the factory method of the parent´s view.
If necessary it´s presenter can directly injected in the parent´s presenter.
Nest a component as a kind of control (1:n nesting)
Basicly i think a custom control should not be treated as a view and so not be implemented with the MVP pattern.
There would be a solution to handle that case this way:
- the presenter of such a “control component” must be accessed through the view itself, not exposed by the module
- the module´s provider of the view must be injected into the parent view
- the parent view must care about instanciating child views/controls and to propagate it to its own presenter, that connects view logic
But as you see the MVP pattern is inappropriate for this typical use case of injecting custom control, so i won´t dive deeper into this case.
Coordination of the ui components
A user always has a reason for using an interface of a system. So applications base on use cases.
The realization of those uses cases are projected in some abstract objects that i call workflow controller.
There are different concepts and names for it, but in common they build the context of a use case or workflow and there purpose is to keep control of the workflow on a very abstract level.
In the ui layer their responsibility is to coordinate and orchestrate ui components, to build a context in which they interact with each other and how they interact with the backend.
Depending on the complexity of the ui and the use cases, in many cases they will not control the ui components directly but abstracted by some helping components of different responsibilities.
To keep the worklow controller decoupled from the presenter, the access to the presenters should be abstracted by a functional interface that is implemented by the presenter and used by the workflow controler.
The callback from the presenter to the workflow controller can be implemented either by injecting a callback function to the presenter, attaching listers to its properties or letting the presenter fire events that are catched by the workflow controller.
Another idea would be to handle the communication between the worklow controller and presenter through a task specific model that holds properties and callback functions.
But in my opinion an interface is more object orientated and it is much harder to keep a model clean and independent from the workflow and the presenter stuff than an interface.
So the presenter from above could look like:
public class Presenter implements WorkflowTask{ private final StringProperty someString = new SimpleStringProperty(); private Consumer\<String\> onDoSomething; @Override public void showSomeString(String string) { someString.set(string); } @Override public void setOnDoSomething(Consumer\<String\> onDoSomething) { this.onDoSomething = onDoSomething; } void doSomething() { onDoSomething.accept(someString.get()); } StringProperty someStringProperty() { return someString; } }
Interaction with the backend
The workflow controllers treat the interaction with the the backend.
For reasons of testability i prefer to use a service class that internally delegates to a fx service and returns a CompletableFuture to its clients.
If you are interessted in Angular and AWS read my article about how to push an angular spa to the cloud
The post Architecture of a JavaFX application appeared first on maimArt.
Top comments (0)