Docs

Documentation versions (currently viewingVaadin 25)
Documentation translations (currently viewingEnglish)

MVP and MVVM Compared Deep Dive

How the Model-View-Presenter pattern works in a Vaadin view, how it compares to MVVM, and when to use which.

Model-View-Presenter (MVP) is a pattern that many Vaadin applications use to separate the UI logic of a view from the UI components. The documentation of older Vaadin versions described it as one of the most common patterns for large applications, and many of the applications that are now being migrated to newer versions use it.

In MVP, a presenter holds the UI logic of a view, and the view is passive: it shows what the presenter tells it to, and reports the user’s actions to the presenter. The following diagram shows a view that edits the title of a ticket, the same view as at the start of Structure Views with MVVM:

A diagram of a view with a title field, a Save button, and a view interface method, a presenter with methods and a field, and a Model with a ticket service. Arrows show the user’s actions going to the presenter, and the presenter calling the view interface to enable the Save button.
  1. The user types in the title field. The view passes the new title to the titleChanged() method of the presenter.

  2. The presenter stores the new title in its draft field.

  3. It then calls updateEditor(), which works out whether the ticket can be saved.

  4. updateEditor() calls setSaveEnabled() on the view, through the view interface.

  5. The view enables or disables the Save button.

  6. When the user clicks Save, the view calls the save() method of the presenter.

  7. The presenter passes the ticket to the TicketService, which saves it.

In code, the presenter sees the view only through an interface:

Source code
Java
// The view, as the presenter sees it
interface TicketEditorView {
    void setSaveEnabled(boolean enabled);
}
Source code
Java
class TicketEditorPresenter {

    private final TicketService ticketService; // The Model
    private final TicketEditorView view;
    private Ticket draft; // The state

    TicketEditorPresenter(TicketService ticketService, TicketEditorView view,
            Ticket ticket) {
        this.ticketService = ticketService;
        this.view = view;
        this.draft = ticket;
        updateEditor();
    }

    void titleChanged(String title) {
        draft = draft.withTitle(title);
        updateEditor();
    }

    void save() {
        ticketService.save(draft);
    }

    // Works out the rule, and pushes the outcome to the view
    private void updateEditor() {
        view.setSaveEnabled(!draft.title().isBlank());
    }
}

The Flow view implements the interface, and passes the user’s actions to the presenter:

Source code
Java
class TicketEditorViewImpl extends VerticalLayout
        implements TicketEditorView {

    private final Button save = new Button("Save");

    TicketEditorViewImpl(TicketService ticketService, Ticket ticket) {
        var presenter = new TicketEditorPresenter(ticketService, this, ticket);

        TextField title = new TextField("Title");
        title.setValue(ticket.title());
        title.addValueChangeListener(
                event -> presenter.titleChanged(event.getValue()));

        save.addClickListener(event -> presenter.save());
        add(title, save);
    }

    @Override
    public void setSaveEnabled(boolean enabled) {
        save.setEnabled(enabled);
    }
}

In MVVM, the same rule is a derived signal that the Save button is bound to. In MVP, the presenter works out the rule and pushes the outcome to the view itself, in steps 3 to 5.

This article shows how MVP looks in a larger Flow view, and compares it with Model-View-ViewModel (MVVM), which builds on signals. It then discusses when to use which, and when neither is worth the extra code.

Important
Deep Dive - Recommended Approach
This opinionated deep dive weighs two patterns against each other, and recommends when to use which. For new views, the recommendation is MVVM. You don’t need this article to follow the MVVM guides. Read it if your application uses MVP, or if you’re choosing a pattern for your team.

How MVP Works

MVP splits a view into three roles:

  • The Model is the application itself: its application services, domain classes, and DTOs.

  • The Presenter holds the UI logic. It reacts to the user’s actions, calls the Model, and tells the view what to show.

  • The View is passive. It shows what the presenter tells it to, and reports the user’s actions to the presenter.

The view interface doesn’t mention Vaadin. The presenter is therefore plain Java: it has no dependency on Vaadin at all, and you can test it with a fake implementation of the view interface. A Vaadin class implements the interface, and that class is the only one that knows about UI components.

The Example

The example is the ticket triage view of the MVVM series, built with MVP and cut down to its core: a searchable list of tickets, an editor for the title of the selected ticket, and a Save button that’s enabled only when the title has changes and isn’t blank. Selecting another ticket while there are unsaved changes asks the user first. The Model is the same TicketService and Ticket record.

The View Interface

The view interface lists everything that the presenter can do to the view. Name its methods after what the view shows, not after UI components:

Source code
TicketsView.java

A question to the user is a method too. The view asks the question in its own way, and passes the answer back by calling the presenter.

The Presenter

The presenter keeps the state of the view in plain fields. It receives the view in its constructor, together with the application services:

Source code
TicketsPresenter.java

After every change, the presenter tells the view what to show. Whether Save is enabled depends on two things, whether the title has changes and whether it’s valid. Therefore, every method that changes the draft ends by calling updateEditor(), which works out both again and pushes them to the view. The keepEditing() method selects the opened ticket again, because the grid still shows the row that the user clicked.

The View Implementation

The route class implements the view interface. It creates the presenter in its constructor, and passes itself to it:

Source code
TicketsViewImpl.java

The listeners only pass the user’s actions on to the presenter. When the presenter sets the value of a field, the field fires a value change event in the same way as when the user changes it. Without the isFromClient() checks, the view would report the presenter’s own changes back to the presenter as if the user had made them.

The rest of the class implements the view interface:

Source code
TicketsViewImpl.java

Setting the items of a grid clears its selection, so showTickets() selects the same ticket again. The view handles this detail of the grid itself, so that the presenter doesn’t need to know about it.

The view also loads the tickets after navigation, and asks the user before leaving with unsaved changes, in the same way as the MVVM version. It asks the presenter with hasUnsavedChanges().

Testing the Presenter

Test the presenter with plain JUnit, and give it a fake view that remembers what the presenter told it:

Source code
TicketsPresenterTest.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class TicketsPresenterTest {

    // Remembers what the presenter last told the view to do
    static class FakeTicketsView implements TicketsView {
        List<Ticket> tickets;
        Ticket selected;
        Ticket shown;
        String titleError;
        boolean saveEnabled;
        boolean confirmingDiscard;

        @Override
        public void showTickets(List<Ticket> tickets) {
            this.tickets = tickets;
        }

        @Override
        public void selectTicket(Ticket ticket) {
            selected = ticket;
        }

        @Override
        public void showTicket(Ticket ticket) {
            shown = ticket;
        }

        @Override
        public void showTitleError(String error) {
            titleError = error;
        }

        @Override
        public void setSaveEnabled(boolean enabled) {
            saveEnabled = enabled;
        }

        @Override
        public void confirmDiscard() {
            confirmingDiscard = true;
        }

        @Override
        public void showSaved() {
        }
    }

    FakeTicketsView view;
    TicketsPresenter presenter;

    Ticket printer;
    Ticket vpn;

    @BeforeEach
    void setUp() {
        view = new FakeTicketsView();
        presenter = new TicketsPresenter(new TicketService(), view);
        presenter.refresh();
        printer = view.tickets.get(0);
        vpn = view.tickets.get(2);
    }

    @Test
    void blankTitleCannotBeSaved() {
        presenter.select(printer);
        presenter.titleChanged(" ");

        assertEquals("Title is required", view.titleError);
        assertFalse(view.saveEnabled);
    }

    @Test
    void switchingTicketsWithUnsavedChangesAsksFirst() {
        presenter.select(printer);
        presenter.titleChanged("Printer on fire again");

        presenter.select(vpn);

        assertTrue(view.confirmingDiscard);
        assertEquals(printer, view.shown);
    }

    @Test
    void keepingTheChangesSelectsTheTicketAgain() {
        presenter.select(printer);
        presenter.titleChanged("Printer on fire again");
        presenter.select(vpn);

        presenter.keepEditing();

        assertEquals(printer, view.selected);
        assertTrue(presenter.hasUnsavedChanges());
    }
}

The tests check what the presenter told the view to do. Instead of writing a fake view, you can use a mocking library, and verify the calls to the view interface. Either way, the tests depend on the view interface: when you change it, you also change the fake or the verified calls.

MVP in a Component Tree

MVP doesn’t change how you split a view into child UI components, as Compose with Components describes. The presenter talks only to the route view, through the view interface. The route view implements the interface by calling setters on its child components, and passes their events and callbacks on to the presenter:

Source code
Java
@Override
public void showTicket(Ticket ticket) {
    editor.setTicket(ticket); // A child component of the view
}
Source code
Java
grid.addTicketSelectedListener(
        event -> presenter.select(event.getTicket()));

This is the style of setters and events that Compose with Components describes, with the decisions moved from the view into the presenter.

Keep one presenter for each route view. Don’t give each child component a presenter of its own that calls the presenters of other components. If a child component is complex enough to need a presenter, keep the presenter inside the child, and let the child talk to its parent through its own setters and events, like any other UI component.

How the Patterns Differ

The following diagram shows the ticket editor from the start of this article, built with MVVM:

A diagram of the same ticket editor built with MVVM. The Save button is bound to a derived signal in the ViewModel, instead of being enabled by a call from a presenter.

A derived signal and a binding take the place of steps 3 to 5 of the MVP version. Nothing in the ViewModel calls the view.

Both patterns move the UI logic of a view out of the UI components, into a plain Java class that you test with JUnit. They differ in how the UI follows the state:

MVP MVVM

How the UI follows the state

The presenter calls the view interface after every change

The view binds its UI components to signals once

A rule over combined state

Worked out again, and pushed to the view, after every change that might affect it

A derived signal, stated once

Classes for each view

View interface, presenter, and view implementation

ViewModel and view

What the UI logic depends on

Nothing from Vaadin

Vaadin’s signals, but no UI components

What the tests check

What the presenter told the view to do

The state of the ViewModel

Pros and Cons of MVP

MVP has these strengths:

Independent of the UI framework

The presenter is plain Java. When you migrate an application from an older Vaadin version, or from another Java UI framework, you rewrite the view implementations, and keep the presenters and their tests. If the presenter doesn’t use any Vaadin types, an old and a new view implementation can even share it while you migrate the views one at a time, for example with Multiplatform Runtime.

Explicit contract

The view interface lists everything that the presenter can do to the UI. It’s easy to review, and one developer can work on the presenter while another builds the view.

Imperative and familiar

Each change to the UI is a method call that you can follow in a debugger. Many Java developers know the pattern, and many existing applications use it.

MVP has these weaknesses:

Updates by hand

The presenter has to update every part of the UI that depends on the state it changes. A forgotten call leaves the UI out of date. The compiler doesn’t notice, and the tests notice only if they check that part of the UI. As the rules combine more state, methods like updateEditor() grow, and more methods have to call them.

More code

Each view needs three classes, and showing one more value in the view touches all of them: a method in the interface, its implementation, and a call in the presenter.

State in two places

The state lives in the presenter, and again in the UI components: the values of the fields and the selection of the grid. The view implementation has to keep the two in step, as the example does when it filters the presenter’s own changes with isFromClient() and selects the ticket again after replacing the items.

Tests tied to the view interface

The tests check calls to the view. A change to the view interface changes the tests, even when the behavior stays the same.

Large views get tangled

When a large view is split into parts, each with a presenter of its own, the presenters tend to call each other, and the flow of a user’s action becomes hard to follow. MVP in a Component Tree shows how to avoid this.

Pros and Cons of MVVM

MVVM has these strengths:

Derived instead of pushed

A rule is a derived signal, stated once, and every UI component that is bound to it follows it. Nothing can forget to update the UI.

Less code

MVVM needs no view interface. A field binds to a signal and a command in one line.

State in one place

The signals of the ViewModel are the only copy of the state. A field doesn’t pass a value that it got from its signal back to the ViewModel, so the view needs no checks to tell the ViewModel’s changes from the user’s.

Tests that check state

The tests read the state of the ViewModel. Replacing a button with a menu item doesn’t change them, as long as the item calls the same command.

Shared state

Several UI components, and child components, bind to the same signals, so the parts of a view share state without calling each other.

MVVM has these weaknesses:

Depends on Vaadin

The ViewModel uses no UI components, but it depends on the signal classes of Vaadin, and needs a Vaadin version that has them. It doesn’t survive a change of UI framework as it is.

Gaps in the bindings

Vaadin doesn’t have bind methods for every property, such as the items of a grid and the error message of a field. For those, the application needs small helpers, as Bind the View shows.

A new way of thinking

Developers have to learn to derive state instead of setting it, to read signals with peek() in commands, and to keep effects out of the ViewModel. One-off interactions, such as a notification after a save, need a returned outcome, because they aren’t state.

When to Use Which

Situation Pattern

A new view with rules over combined state, shared state, flows, or background work

MVVM

A view that you’re rewriting anyway, in an application with signals

MVVM

An application that uses MVP, and that you’re migrating to a newer Vaadin version

MVP

UI logic that has to work with more than one UI framework, or one that you plan to replace

MVP

A view that shows none of the signs of a complex view

Neither

When you migrate an application that uses MVP, keep MVP during the migration. Port the view implementations, and keep the presenters and their tests as they are. The tests then show that the UI logic still works on the new version. Changing the pattern at the same time would change the code that the tests check, and you couldn’t tell whether a failure comes from the migration or from the new pattern.

After the migration, you don’t have to convert anything. Move a view to MVVM when you change it substantially anyway, and its updates by hand get in the way. A mix of patterns in one application is fine, as long as each view uses one of them.

Use MVP for a new view only when its UI logic really must not depend on Vaadin. Most applications never change their UI framework, and paying for that option in every view rarely pays off.

When Neither Is Worth It

Both patterns cost extra code. Most views don’t need either: a login view, a settings view, a read-only page, a list with one filter, or a form that loads and saves one entity. Build those as plain views, with Binder for the forms, and use signals in the view when a few UI components share some state. When to Use MVVM lists the signs that a view has outgrown that.

MVP costs the most for a simple view, because it needs three classes for each view, however simple the view is.

Testing alone isn’t a reason to use either pattern. A browserless test runs a plain view without a browser, and checks what the user sees after clicking and typing. See Browserless Tests. A separate presenter or ViewModel pays off when the view has rules worth testing on their own, not when the only rule is that a button calls a service.

Moving from MVP to MVVM

When you move a view from MVP to MVVM, the presenter becomes the ViewModel, and the view implementation becomes the view:

  1. Remove the view from the presenter’s constructor. Keep the application services.

  2. Turn each field that holds state into a private signal.

  3. Turn each method that the presenter calls on the view to show something into a read-only signal. A value that the presenter works out, such as whether Save is enabled, becomes a derived signal. Remove the code that pushes the values, such as updateEditor().

  4. Keep the methods that the view calls as commands.

  5. Turn each question that the presenter asks the view into state, such as confirmingDiscard, or into an outcome that a command returns.

  6. In the view, bind the UI components to the signals instead of implementing the view interface, and remove the interface. A listener with an isFromClient() check becomes a bindValue() call.

  7. In the tests, replace the fake view with assertions on the signals, read with peek().

The result is the ViewModel of Write a ViewModel. Move one view at a time, and run its tests after each step.