Techsenger MVVM4FX is a tiny framework for developing JavaFX applications using the MVVM pattern. It provides all the necessary interfaces and base class implementations for creating components, which serve as the units of the MVVM pattern. Examples of components include tabs, dialog windows, toolbars, image viewers, help pages, and more.
As a real example of using this framework, see TabShell project.
- Overview
- Features
- MVVM
- Component
- Requirements
- Dependencies
- Code building
- Running Demo
- License
- Contributing
- Support Us
MVVM4FX reimagines the Model–View–ViewModel pattern for JavaFX as a component-based, extensible platform designed
around clarity, modularity, and the KISS principle. Each Component exists as a self-contained unit composed of a
View, ViewModel, Mediator optionally extended with History.
The framework enforces a strict separation between presentation, logic, and identity. The View defines the visual
structure and behavior; the ViewModel encapsulates logic and state; the Component is responsible for initialization
and deinitialization, managing child components, and their composition, operating at the component level. The
ComponentMediator is the interface through which the ViewModel interacts with the Component, and the History
preserves continuity across sessions.
At its core, MVVM4FX follows the KISS principle – every class, method, and abstraction exists only for a clear reason, avoiding unnecessary complexity or dependencies. This simplicity is deliberate: it keeps the architecture transparent, predictable, and easy to extend.
By combining conceptual clarity with structural discipline, MVVM4FX achieves both architectural purity and practical flexibility — a balance where components remain independent yet fully interoperable. It is not a minimalistic abstraction but a complete design system for building coherent, maintainable, and intelligent JavaFX applications.
Key features include:
- Support for the component lifecycle.
- Organization of core tasks within the view.
- Component inheritance.
- Ability to preserve component history.
- Designed without considering FXML support.
- Support for component-level logging.
- Detailed documentation and sample code.
MVVM (Model-View-ViewModel) is an architectural pattern that divides an application's logic into three main parts:
Model, View, and ViewModel.
Model — encapsulates the data and business logic of the application. Models represent an abstraction that stores and
processes the application’s data, including all business logic rules and data validation logic. Models do not interact
with the UI and do not know about View or ViewModel. Instead, they provide data and perform actions related to the
business logic. Model can include:
- Data (for example, entities from a database or objects obtained from external sources).
- Business logic (such as data processing rules, calculations, data manipulation).
- Validation logic (for example, checks that are performed before saving data).
View — represents the user interface that displays the data. The View's task is to contain UI elements and bind their
state to the ViewModel. View is responsible for displaying data and interacting with the user, but it should not
contain logic for managing the state of these elements. Because it is the responsibility of the ViewModel to control
this state without knowing about specific controls in the View. For example, if the ViewModel indicates that a button
should be active or inactive, the View will update the control, but the View will not manage the logic that determines
when the button should be enabled or disabled.
Besides, the View may and should contain logic related to the visual behavior and layout of elements (presentation
logic). This includes calculating positions and sizes, managing component arrangement (e.g., docking or resizing),
handling animations, drag-and-drop operations, or other view-related interactions that depend on specific UI components.
ViewModel — manages the state of UI elements without needing to know the implementation details of the user interface.
ViewModel can also serve as a layer between the View and Model, obtaining data from the Model and preparing it for
display in the View. It can transform the data from the model into a format suitable for UI presentation.
-
Separation of concerns. MVVM helps to clearly separate the presentation logic (
View), business logic and data (Model), and interaction logic (ViewModel). This simplifies code maintenance and makes it more readable. -
Testability. The
ViewModelcan be tested independently of the user interface (UI) because it is not tied to specific visual elements. This makes it easy to write unit tests for business logic. -
Two-way data binding. In MVVM, data is automatically synchronized between the
ViewandViewModel, which reduces the amount of code required for managing UI state and simplifies updates. -
Simplification of complex UIs. When an application has complex UIs with dynamic data, MVVM helps make the code more understandable and structured, easing management of UI element states.
-
UI updates without direct manipulation. The
ViewModelmanages updates to theViewvia data binding, avoiding direct manipulation of UI elements. This makes the code more flexible and scalable.
A component is a fundamental, self-contained building block of a user interface (UI) that provides a specific piece of functionality and enables user interaction. A component represents a higher-level abstraction than standard UI controls, fundamentally distinguished by its compositional nature, which encompasses and organizes multiple UI controls, its managed lifecycle, and its capacity to maintain state history. Crucially, while usually components also encapsulate business logic, this is not a mandatory trait for all, as structural components like layout containers demonstrate.
A component always consists of at least four classes: a Component, a ComponentView, a ComponentViewModel
and ComponentMediator. A natural question might arise: why is there no Model in the component, given that
the pattern is called MVVM?
Firstly, a component is a building block for constructing a user interface, which might not be related to the
application's business logic at all. Secondly, the Model exists independently of the UI and should have no knowledge
of the component's existence. Thirdly, MVVM is fundamentally about the separation of responsibilities rather than
the mandatory presence of all three layers in every element. In other words, a component does not violate MVVM
principles simply because it lacks a Model; it remains compliant as long as the View and ViewModel maintain a
clear separation of concerns and communicate exclusively through data binding and observable properties.
The ComponentView and ComponentViewModel classes correspond to the View and ViewModel in the MVVM pattern and
are relatively straightforward. The Component and ComponentMediator classes, on the other hand, address the
aspects that MVVM does not cover and are therefore more complex, which is why they are explained in detail below.
The Component is responsible for:
- Initializing and deinitializing the component.
- Creating, managing and destroying child components (those that will reside directly inside this component).
- Storing references to both the parent component and its child components
- Creating derived components (those that will be provided to another component after creation, e.g., dialogs, tabs, system notifications, etc.).
Thus, a Component always operates strictly at the component level. It is important to keep this in mind to prevent it from turning into a God object.
The ComponentMediator is the interface that the ViewModel uses to interact with the Component. This interface
is needed for two reasons: first, it allows the ViewModel to be tested independently; second, it allows the Component
to access both the View and the ViewModel, without exposing the View to the ViewModel.
The ComponentMediator is implemented as a non-static inner class within the Component, which allows it to work with
both the View and the ViewModel without violating MVVM principles.
Working with a Component and ComponentMediator is one of the most challenging parts of using the platform for the
following reasons:
- MVVM Gap. MVVM does not specify how child and derived components should be created, how their lifecycle should be managed, or how they should be composed.
- Architectural Conflict. According to MVVM, the
ViewModelmust not know about theView, yet theViewModelmay need to initiate the creation of new components (for example, opening a dialog) and their composition — which is impossible without interacting with theView. - Implementation Complexity. Due to the two-layer structure of a component (
ViewandViewModel), each of them requires its own version of aComponent, which doubles the complexity of the problem. In addition, naming becomes difficult, since names likeFooViewComponentandFooViewModelComponentare hardly convenient to work with. - Inheritance Challenges. Supporting component inheritance, where hierarchies of all classes of inherited components
must be created:
ChildViewextendsParentView,ChildViewModelextendsParentViewModel,ChildComponentextendsParentComponentetc.
Let’s look at some code demonstrating the use of these classes.
public interface FooMediator extends ChildMediator {
...
}
public class FooViewModel extends AbstractChildViewModel {
...
@Override
public FooMediator getMediator() {
return (FooMediator) super.getMediator();
}
}
public class FooView extends AbstractChildView<FooViewModel> {
public FooView(FooViewModel viewModel) {
...
}
...
@Override
public FooComponent getComponent() {
return (FooComponent) super.getComponent();
}
}
public class FooComponent extends AbstractChildComponent<FooView> {
protected class Mediator extends AbstractChildComponent.Mediator implements FooMediator {...}
public FooComponent(FooView view) {
...
}
...
@Override
protected FooMediator createMediator() {
return new FooComponent.Mediator(); // the mediator is created at the beginning of initialization
}
}This code demonstrates how to create a component instance.
var viewModel = new FooViewModel();
var view = new FooView(viewModel);
var component = new FooComponent(view);
component.initialize();
...
component.deinitialize();Advantages of this approach:
- Strict Separation. Using a
Componenttogether with aMediatorenforces a clear separation of layers according to MVVM and simplifies testing. - Clean Architecture. The
Componentcentralizes all logic related to managing child components, keeping theViewandViewModelfree from responsibilities that do not belong to them. - MVVM Compliance. The
Mediatorinterface defines how aViewModelcan initiate the addition or removal of a component without violating MVVM principles.
In addition to the four classes, a component may include a ComponentHistory. The ComponentHistory enables the
preservation of the component’s state across its lifecycle. Data exchange occurs exclusively between the
ComponentViewModel and the ComponentHistory. When the component’s state transitions to INITIALIZING, data is
restored from the ComponentHistory to the ComponentViewModel. Conversely, when the state transitions to
DEINITIALIZED, data from the ComponentViewModel is saved back to the ComponentHistory. The volume of state
information that is restored and persisted is defined by the HistoryPolicy enum.
Each component features Component#initialize() and Component#deinitialize() methods,
which initialize and deinitialize all the parts of the component, respectively, updating its state.
In the default implementation during initialization, the component first enters the pre-initialization phase, where
the ComponentMediator is created, attached to the ViewModel, and the component’s history is restored. After that,
the main initialization phase begins, during which the ViewModel and View perform their own internal initialization.
Once both parts are initialized, the component completes the process with a post-initialization phase that can be used
for any additional logic specific to the component.
Deinitialization follows the same structure in reverse. It begins with a pre-deinitialization phase, then proceeds to
the main deinitialization of the View and ViewModel (reverse order), and finishes with a post-deinitialization
phase. By default, the component saves its history at this final stage.
Both AbstractComponentView and AbstractComponentViewModel provide protected initialize() and deinitialize() methods
that are automatically invoked during the lifecycle, allowing each part to perform its own work without breaking
the architectural boundaries. The optional pre and post hooks in AbstractComponent give developers additional
flexibility to extend the lifecycle while preserving its structure. This design keeps the component's behavior
predictable, transparent, and easy to customize.
The default implementation of the AbstractComponentView#initialize() and AbstractComponentView#deinitialize() methods
is split into four protected methods that perform the core View operations. These protected methods may be overridden
and are responsible for the following:
- building/unbuilding
- binding/unbinding
- adding/removing listeners
- adding/removing handlers
It is important to note that these protected methods should not be considered the only place for performing such tasks
(e.g., adding or removing handlers) within the View; rather, they represent one part of the
initialization/deinitialization process. Thus, such tasks may also be performed in other methods.
A component has five distinct states (see ComponentState):
| State | Description |
|---|---|
| CREATING | The component is being constructed. The ComponentViewModel, ComponentView, and Component objects exist, but initialization has not yet begun. This is the earliest detectable phase of the lifecycle. |
| INITIALIZING | The component is undergoing initialization. Its ComponentViewModel, ComponentView, and other internal parts are being initialized. |
| INITIALIZED | The component has been fully initialized. The component, its view, and its view-model are active, bound, and synchronized, and the component is ready for use. |
| DEINITIALIZING | The component is undergoing deinitialization. Its ComponentView, ComponentViewModel, and other internal parts are being deinitialized. |
| DEINITIALIZED | The component has been completely deinitialized. All resources have been released and cleanup has been performed. This is the terminal state of the lifecycle. |
Components can act as both parents and children, forming a tree structure that can change dynamically. The library provides a mechanism for dynamically creating and removing components and includes optional logic for managing component relationships, leaving their use to the developer's discretion.
The component tree is built according to the Unidirectional Hierarchy Rule (UHR). This rule establishes a strict hierarchical order by explicitly prohibiting circular parent-child relationships, meaning a component cannot simultaneously be a direct parent and a direct child of another component. The UHR is designed to maintain a clear, acyclic structure, which prevents logical conflicts and ensures predictable behavior. Importantly, this rule does not restrict child components from directly accessing or communicating with their parents; it solely forbids cyclical dependencies that would compromise the architectural integrity of the hierarchy.
It is crucial to highlight the interaction between components. Consider a parent and a child component as an example.
The parent component's ComponentViewModel holds a reference to the child component's ComponentViewModel via its
children field, while the child component's ComponentViewModel holds a reference to the parent component's
ComponentViewModel via its parent field. Similarly, the parent component's ComponentView holds a reference to the
child component's ComponentView through its children field, and the child component's ComponentView holds a
reference to the parent component's ComponentView via its parent field.
This two-layer linkage establishes a coherent and symmetric relationship between parent and child components at both
the View and ViewModel layers. The parent and child components are fully aware of each other's existence and state,
enabling direct coordination and communication within the hierarchy while maintaining clear separation of concerns
between the presentation (View) and logic (ViewModel) layers. This design ensures consistency and synchronization
across the component tree without violating the Unidirectional Hierarchy Rule (UHR), as the relationships are strictly
hierarchical and non-cyclic.
- The element has independent testable state or business logic that can exist without a
View. - The element has a distinct lifecycle requiring separate initialization/deinitialization, or can be dynamically added/removed.
- The element is potentially reusable across different contexts (e.g., dialogs, toolbars, multiple editor types).
- Multiple closely related properties form a logical unit - grouping them into a separate component improves maintainability and reduces parent component complexity.
- The element manages structural composition - it contains child components or forms an independent subtree (e.g., containers, tabs, panels).
- State persistence is required - the element needs its own
Historyto save and restore state between sessions.
- The element’s
ViewModelwould contain no meaningful behavior or data - making the component redundant. - The element represents a minor visual part of the interface and does not require its own logic or state.
- The element is simple enough that separating it into its own component would add unnecessary complexity rather than improving clarity.
Java 11+ and JavaFX 19.
The project will be added to the Maven Central repository in a few days.
To build the library use standard Git and Maven commands:
git clone https://github.com/techsenger/mvvm4fx
cd mvvm4fx
mvn clean install
To run the demo execute the following commands in the root of the project:
cd mvvm4fx-demo
mvn javafx:run
Please note, that debugger settings are in mvvm4fx-demo/pom.xml file.
Techsenger MVVM4FX is licensed under the Apache License, Version 2.0.
We welcome all contributions. You can help by reporting bugs, suggesting improvements, or submitting pull requests with fixes and new features. If you have any questions, feel free to reach out — we’ll be happy to assist you.
You can support our open-source work through GitHub Sponsors. Your contribution helps us maintain projects, develop new features, and provide ongoing improvements. Multiple sponsorship tiers are available, each offering different levels of recognition and benefits.