Stell dir vor, du könntest eine moderne Single-Page-Application schreiben, ohne eine Zeile JavaScript, HTML oder CSS — nur mit Java. Genau das ermöglicht Vaadin Flow: Serverseitige UI-Komponenten, die im Browser als Web Components gerendert werden. Vaadin 25 bringt diesen Ansatz auf den neuesten Stand mit Spring-Boot-Integration, virtueller Scroll-Liste und flüssigem Daten-Binding.

Dieser Artikel führt dich durch die Erstellung einer vollständigen Todo-App mit Vaadin Flow 25 und Spring Boot.

Was ist Vaadin Flow?

Vaadin Flow ist ein serverseitiges UI-Framework: Du schreibst Java-Klassen, die UI-Komponenten wie Buttons, Textfelder und Grids repräsentieren. Die Kommunikation zwischen Server und Browser läuft automatisch über WebSockets. Du interagierst nie direkt mit dem DOM — Vaadin übernimmt das Rendering und Event-Handling vollständig.

Die aktuelle Version 25.2.1 (Juni 2026) läuft mit Spring Boot 4.0+ und Java 21+.

Projekt-Setup

Maven — Spring Boot + Vaadin BOM:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.4</version>
</parent>

<properties>
    <java.version>21</java.version>
    <vaadin.version>25.2.1</vaadin.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>com.vaadin</groupId>
        <artifactId>vaadin-spring-boot-starter</artifactId>
    </dependency>
</dependencies>
Code-Sprache: HTML, XML (xml)

Das Datenmodell und der Service

Zunächst das Domain-Objekt und eine In-Memory-Service-Schicht:

import java.util.concurrent.CopyOnWriteArrayList;

public class Todo {
    private Long id;
    private String task;
    private boolean done;

    public Todo() {}

    public Todo(Long id, String task) {
        this.id = id;
        this.task = task;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTask() { return task; }
    public void setTask(String task) { this.task = task; }
    public boolean isDone() { return done; }
    public void setDone(boolean done) { this.done = done; }
}
Code-Sprache: JavaScript (javascript)
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class TodoService {
    private final List<Todo> todos = new CopyOnWriteArrayList<>();
    private final AtomicLong counter = new AtomicLong(1);

    public List<Todo> findAll() {
        return Collections.unmodifiableList(todos);
    }

    public Todo add(String task) {
        Todo todo = new Todo(counter.getAndIncrement(), task);
        todos.add(todo);
        return todo;
    }

    public void remove(Todo todo) {
        todos.remove(todo);
    }

    public void toggle(Todo todo) {
        todo.setDone(!todo.isDone());
    }
}
Code-Sprache: PHP (php)

Die Haupt-View mit @Route

Vaadin Views werden mit @Route annotiert. Das folgende TodoView baut eine komplette Benutzeroberfläche aus Java-Komponenten zusammen:

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;

@Route("")
@PageTitle("Todo App")
public class TodoView extends VerticalLayout {

    private final TodoService service;
    private final Grid<Todo> grid = new Grid<>(Todo.class, false);
    private final TextField taskField = new TextField();

    public TodoView(TodoService service) {
        this.service = service;

        taskField.setPlaceholder("Neue Aufgabe eingeben...");
        taskField.setClearButtonVisible(true);

        Button addButton = new Button("Hinzufügen", VaadinIcon.PLUS.create());
        addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        addButton.addClickListener(e -> addTodo());
        addButton.addClickShortcut(Key.ENTER);

        grid.addColumn(Todo::getId).setHeader("ID").setWidth("80px");
        grid.addColumn(Todo::getTask).setHeader("Aufgabe").setAutoWidth(true);

        grid.addComponentColumn(todo -> {
            Button doneBtn = new Button(
                todo.isDone() ? "✅" : "⬜",
                e -> { service.toggle(todo); refreshGrid(); }
            );
            return doneBtn;
        }).setHeader("Status").setWidth("80px");

        grid.addComponentColumn(todo -> {
            Button deleteBtn = new Button("Löschen", VaadinIcon.TRASH.create());
            deleteBtn.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_SMALL);
            deleteBtn.addClickListener(e -> {
                service.remove(todo);
                refreshGrid();
            });
            return deleteBtn;
        }).setWidth("120px");

        HorizontalLayout toolbar = new HorizontalLayout(taskField, addButton);
        toolbar.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
        toolbar.setWidthFull();

        add(toolbar, grid);
        refreshGrid();
    }

    private void addTodo() {
        String task = taskField.getValue().trim();
        if (!task.isEmpty()) {
            service.add(task);
            taskField.clear();
            refreshGrid();
        }
    }

    private void refreshGrid() {
        grid.setItems(service.findAll());
    }
}
Code-Sprache: JavaScript (javascript)

Routing: Mehrseitige Apps mit @Route

Vaadin Flow bietet ein volles Routing-System mit Parameter-Support und verschachtelten Layouts:

<em>// Haupt-Layout mit Navigationsleiste</em>
public class MainLayout extends AppLayout implements RouterLayout {
    public MainLayout() {
        DrawerToggle toggle = new DrawerToggle();
        H1 title = new H1("Meine App");
        addToNavbar(toggle, title);

        VerticalLayout drawer = new VerticalLayout(
            new RouterLink("Todos", TodoView.class),
            new RouterLink("Statistik", StatsView.class)
        );
        addToDrawer(drawer);
    }
}

<em>// View mit Pfad-Parameter</em>
@Route(value = "todo/:todoId", layout = MainLayout.class)
public class TodoDetailView extends VerticalLayout
        implements BeforeEnterObserver {
    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        String todoId = event.getRouteParameters().get("todoId")
            .orElse("unbekannt");
        add(new H2("Todo #" + todoId));
    }
}

<em>// Programmgesteuerte Navigation</em>
getUI().ifPresent(ui -> ui.navigate("todo/" + someId));
Code-Sprache: JavaScript (javascript)

Formular-Validierung mit dem Binder

Vaadin’s Binder<T>-Klasse ermöglicht typsichere Zwei-Wege-Datenbindung zwischen UI-Komponenten und Beans:

import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.component.checkbox.Checkbox;

public class TodoEditor extends VerticalLayout {
    private final Binder<Todo> binder = new Binder<>(Todo.class);
    private final TextField task = new TextField("Aufgabe");
    private final Checkbox done = new Checkbox("Erledigt");
    private Todo currentTodo;

    public TodoEditor() {
        binder.forField(task)
            .asRequired("Aufgabe darf nicht leer sein")
            .withValidator(t -> t.length() >= 3,
                "Mindestens 3 Zeichen erforderlich")
            .bind("task");

        binder.forField(done).bind("done");

        Button save = new Button("Speichern", e -> {
            if (binder.writeBeanIfValid(currentTodo)) {
                Notification.show("Gespeichert!");
            }
        });

        add(task, done, save);
    }

    public void edit(Todo todo) {
        this.currentTodo = todo;
        binder.readBean(todo);
    }
}
Code-Sprache: JavaScript (javascript)

Fazit

Vaadin Flow ist kein Framework für jede Anwendung — aber für datenintensive Business-Apps mit komplexen CRUD-Formularen ist es unschlagbar produktiv. Der Verzicht auf eine separate Frontend-Codebase bedeutet weniger Context-Switching, weniger Schnittstellen und schnellere Entwicklung.

Mit Vaadin 25, Spring Boot 4.0 und der nahtlosen Binder-API schreibst du Full-Stack-Apps in reinem Java — und der Browser raunzt nie, weil kein JavaScript fehlt.