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.