Viele Beispielprojekte überfrachten den Einstieg, weil sie zu viele Themen gleichzeitig aufzeigen. Routing, Datenzugriff, Security, Formulare, Theme-Anpassungen und weitere Integrationen werden dann in einer einzigen Demo dargestellt. Für Leser wird es dadurch schwieriger, den eigentlichen Aufbau des Projekts zu erkennen.
Dieses Projekt verfolgt einen anderen Ansatz. Im Mittelpunkt steht ein kompaktes Gerüst aus AppShell, MainLayout, mehreren Views, einem kleinen Service, einem eigenen Theme und vorbereiteter Internationalisierung. Ziel ist es, die grundlegenden Bausteine einer Vaadin-Flow-Anwendung so sichtbar zu machen, dass ihr Zusammenspiel nachvollziehbar bleibt.

Der Quellcode für dieses Projekt findest Du auf
GitHub unter: https://3g3.eu/vdn-tpl
Gerade in Vaadin Flow ist das sinnvoll, weil die Oberfläche serverseitig in Java erstellt wird. Klassen wie AppShell, MainLayout oder MainView prägen deshalb früh die Form der Anwendung. Der Artikel folgt einer Vaadin-zentrischen Perspektive und untersucht nicht primär eine Fachdomäne, sondern den Aufbau eines brauchbaren Flow-Projekts.
Table of Contents
- Technologiestack und Projektidee
- Projektstruktur im Überblick
- Einstiegspunkt der Anwendung: AppShell
- Das Grundgerüst der UI mit MainLayout
- Navigation und Routen sauber aufbauen
- Die erste View: MainView als minimales Interaktionsbeispiel
- UI-Logik und fachliche Logik trennen
- Internationalisierung von Anfang an mitdenken
- Zusätzliche Views als Blaupause für eigene Seiten
- Build, Start und Entwicklungsmodus
- Fazit
Technologiestack und Projektidee
Das Projekt setzt auf Vaadin Flow 25, Maven, Jetty, JDK 25, klassisches WAR-Packaging, ein eigenes Theme und vorbereitete Internationalisierung. Diese Kombination definiert den technischen Rahmen der Anwendung.
Vaadin Flow bündelt Komponenten, Layouts, Routing und Interaktionen in einer einzigen Java-Struktur. Maven organisiert Build, Abhängigkeiten und Plugins. Jetty stellt einen leichtgewichtigen Servlet-Container für den lokalen Betrieb und die deploymentnahe Ausführung bereit. JDK 25 positioniert das Projekt auf einer aktuellen Java-Basis, während das WAR-Packaging den Charakter als klassische Webanwendung unterstreicht.
Theme und Internationalisierung ergänzen den Stack um zwei Querschnittsaspekte: visuelle Gestaltung und sprachabhängige Inhalte. Dadurch ist der technische Unterbau nicht nur lauffähig, sondern auch bereits auf typische Erweiterungen vorbereitet.
Projektstruktur im Überblick
Die Anwendung folgt der klassischen Maven-Struktur mit src/main/java für den Quelltext und src/main/resources für begleitende Ressourcen. Dieser Aufbau schafft einen vertrauten Rahmen und trennt ausführbaren Code von Konfigurations- und Sprachressourcen.
Innerhalb des Java-Teils sind die Zuständigkeiten entlang der Anwendungsstruktur gegliedert. Einstiegspunkt und globale Konfigurationen bilden den äußeren Rahmen. Layout-Klassen definieren das übergreifende UI-Gerüst, Views stehen für konkrete Seiten und Service-Klassen implementieren unterstützende Logik.
Ergänzt wird dies durch Ressourcen für Internationalisierung und Theme. Übersetzungsdateien liegen im Ressourcenbereich, visuelle Anpassungen sind an den dafür vorgesehenen Stellen gebündelt. So lässt sich im Repository schnell erkennen, wo neue Seiten, zusätzliche Logik oder gestalterische Anpassungen eingeordnet werden.
Einstiegspunkt der Anwendung: AppShell
Die AppShell definiert die globale Ebene der Anwendung. Hier werden Einstellungen verankert, die nicht nur eine einzelne Seite, sondern den Browserauftritt insgesamt betreffen. Dazu zählen etwa die Theme-Aktivierung, Meta-Informationen, Viewport-Angaben sowie weitere anwendungsweite Konfigurationen.
Im Projekt ist diese Rolle in einer kompakten Klasse umgesetzt:
/**
* Typical use cases of AppShell
* ✅ Viewport & mobile optimization
* ✅ Setting metadata (SEO, security)
* ✅ Favicons, touch icons
* ✅ Global JavaScript snippets (analytics, monitoring)
* ✅ Global CSS (e.g., corporate branding)
* ✅ Selecting a theme for the entire app
*/
@Meta(name = "author", content = "Sven Ruppert")
@Viewport("width=device-width, initial-scale=1.0")
@PWA(name = "Project Base for Vaadin", shortName = "Project Base")
@Theme("my-theme")
@Push
public class AppShell
implements AppShellConfigurator {
@Override
public void configurePage(AppShellSettings settings) {
// settings.addFavIcon("icon",
// "icons/my-favicon.png",
// "32x32");
//
// // Externes CSS
// settings.addLink("stylesheet",
// "https://cdn.example.com/styles/global.css");
//
// // Externes Script
// settings.addInlineWithContents(
// "console.log('Hello from AppShell!');",
// Inline.Wrapping.AUTOMATIC);
}
}
Bereits die Annotationen zeigen sehr gut, welche Art von Verantwortung in diese Klasse gehört. @Meta ergänzt globale Metadaten, @Viewport definiert das Verhalten auf mobilen Geräten, @PWA beschreibt grundlegende Progressive-Web-App-Eigenschaften, @Theme(“my-theme”) aktiviert das anwendungsweite Theme und @Push schaltet die serverseitige Push-Kommunikation frei. Damit bündelt die AppShell genau jene Einstellungen, die für den gesamten Browserauftritt relevant sind.
Zusätzlich implementiert die Klasse AppShellConfigurator und stellt mit configurePage(AppShellSettings settings) einen dedizierten Erweiterungspunkt bereit. Auch wenn die Methode im aktuellen Stand keine aktiven Einstellungen enthält, zeigen die auskommentierten Beispiele bereits typische Einsatzfelder: Favicons, globale Stylesheets oder Inline-Skripte lassen sich genau hier zentral registrieren. Damit ist im Projekt bereits strukturell vorbereitet, an welcher Stelle solche globalen Anpassungen später untergebracht werden.
Ihre Aufgabe besteht nicht darin, fachliche Inhalte zu rendern oder Komponentenlogik zu bündeln. Sie legt vielmehr den Rahmen fest, in dem sich die eigentliche Oberfläche später bewegt. Dadurch entsteht eine klar erkennbare Stelle für globale Entscheidungen, die nicht in Views oder Layout-Klassen verstreut werden.
Das Grundgerüst der UI mit MainLayout
Das MainLayout bildet die dauerhafte Struktur der Oberfläche. Es bündelt Navigation, Kopfbereich und die Einbettung des Inhaltsbereichs und bestimmt, wie sich einzelne Seiten innerhalb derselben Anwendung präsentieren. Im Projekt ist diese Aufgabe in einer eigenen Layout-Klasse umgesetzt, die direkt von AppLayout erbt:
public class MainLayout
extends AppLayout {
public MainLayout() {
createHeader();
}
private void createHeader() {
H1 appTitle = new H1("Vaadin Flow Demo");
SideNav views = getPrimaryNavigation();
Scroller scroller = new Scroller(views);
scroller.setClassName(LumoUtility.Padding.SMALL);
DrawerToggle toggle = new DrawerToggle();
H2 viewTitle = new H2("Headline");
HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle);
wrapper.setAlignItems(FlexComponent.Alignment.CENTER);
wrapper.setSpacing(false);
VerticalLayout viewHeader = new VerticalLayout(wrapper);
viewHeader.setPadding(false);
viewHeader.setSpacing(false);
addToDrawer(appTitle, scroller);
addToNavbar(viewHeader);
setPrimarySection(Section.DRAWER);
}
private SideNav getPrimaryNavigation() {
SideNav sideNav = new SideNav();
sideNav.addItem(new SideNavItem("Dashboard",
"/" + MainView.PATH,
DASHBOARD.create()),
new SideNavItem("Youtube",
"/" + YoutubeView.PATH,
CART.create()),
new SideNavItem("About",
"/" + AboutView.PATH,
USER_HEART.create())
);
return sideNav;
}
}
Wiederkehrende Elemente werden hier zentral untergebracht, statt in jeder View neu aufgebaut zu werden. Auf dieser Grundlage können sich einzelne Seiten auf ihre eigenen Inhalte konzentrieren, während das MainLayout den übergreifenden Rahmen bildet. Mit Vaadin AppLayout steht dafür eine passende Basis zur Verfügung.
Zwischen AppShell und Views nimmt das MainLayout damit die Rolle der verbindenden UI-Schicht ein: Globale Einstellungen bleiben außerhalb, konkrete Inhalte innerhalb der Views, und dazwischen liegt die dauerhafte Oberflächenstruktur.
Navigation und Routen sauber aufbauen
Die Routing-Struktur legt fest, über welche Pfade die einzelnen Views erreichbar sind. In Vaadin Flow geschieht das direkt an der jeweiligen Klasse über @Route. Dadurch bleibt das Routing eng mit der sichtbaren Seitenstruktur verbunden.
Im Projekt ist dieses Prinzip unmittelbar in den View-Klassen zu sehen. Die Startseite verwendet einen leeren Pfad und bindet sich zugleich an das MainLayout:
/**
* The main view contains a text field for getting the username and a button
* that shows a greeting message in a notification.
*/
@Route(value = MainView.PATH, layout = MainLayout.class)
public class MainView
extends VerticalLayout
implements LocaleChangeObserver {
public static final String YOUR_NAME = "your.name";
public static final String SAY_HELLO = "say.hello";
public static final String PATH = "";
private final GreetService greetService = new GreetService();
private final Button button = new Button();
private final TextField textField = new TextField();
public MainView() {
button.addClickListener(e -> {
add(new Paragraph(greetService.greet(textField.getValue())));
});
add(textField, button);
}
@Override
public void localeChange(LocaleChangeEvent localeChangeEvent) {
button.setText(getTranslation(SAY_HELLO));
textField.setLabel(getTranslation(YOUR_NAME));
}
}
Auch die weiteren Seiten folgen demselben Muster. AboutView und YoutubeView definieren ihre Pfade jeweils über eigene Konstanten und werden ebenfalls in das MainLayout eingebettet:
@Route(value = AboutView.PATH, layout = MainLayout.class)
public class AboutView
extends VerticalLayout {
public static final String PATH = "about";
public AboutView() {
H1 title = new H1("About");
H2 subtitle = new H2("Vaadin Flow Demo Application");
Paragraph description = new Paragraph("This is a demo application built with Vaadin Flow " +
"framework to showcase various UI components and features.");
Paragraph version = new Paragraph("Version: 1.0.0");
Paragraph author = new Paragraph("Created by: Sven Ruppert");
Paragraph bio = new Paragraph("""
Sven Ruppert has been involved in software development for more than 20 years. \
As developer advocate he is constantly looking for innovations in software development. \
He is speaking internationally at conferences and has authored numerous technical articles and books.""");
Paragraph homepage = new Paragraph(
new Paragraph("Visit my website: "),
new Anchor("https://www.svenruppert.com", "www.svenruppert.com", BLANK));
Image vaadinLogo = new Image("images/vaadin-logo.png", "Vaadin Logo");
vaadinLogo.setWidth("200px");
setSpacing(true);
setPadding(true);
setAlignItems(Alignment.CENTER);
add(title, subtitle, description, version, author, bio, homepage, vaadinLogo);
}
}
Diese Ausschnitte zeigen mehrere wichtige Punkte. Erstens wird das Routing direkt dort definiert, wo auch die jeweilige Seite implementiert ist. Zweitens bleibt durch layout = MainLayout.class klar erkennbar, in welchem Oberflächenrahmen die View später erscheint. Drittens werden die Pfade nicht an vielen Stellen als magische Strings verteilt, sondern über PATH-Konstanten an den Klassen selbst gebündelt.
Routen übernehmen dabei nicht nur eine technische Funktion. Sie ordnen die Anwendung in erreichbare Bereiche und machen sichtbar, wie Seiten benannt und voneinander abgegrenzt sind. In Verbindung mit dem MainLayout entsteht daraus die Navigationsstruktur, über die Benutzer zwischen den einzelnen Ansichten wechseln.
Wichtig ist die Unterscheidung zwischen Routing und Navigation: Routing definiert die Erreichbarkeit, Navigation hingegen deren Darstellung in der Oberfläche.
Die erste View: MainView als minimales Interaktionsbeispiel
Mit der MainView wird die Anwendung erstmals als konkrete Seite sichtbar. Sie zeigt ein einfaches Interaktionsmuster aus Eingabe, Aktion und sichtbarer Reaktion und macht damit die Grundidee der serverseitigen UI-Programmierung in Vaadin unmittelbar greifbar.
Im Projekt ist diese erste View bewusst kompakt gehalten:
/**
* The main view contains a text field for getting the username and a button
* that shows a greeting message in a notification.
*/
@Route(value = MainView.PATH, layout = MainLayout.class)
public class MainView
extends VerticalLayout
implements LocaleChangeObserver {
public static final String YOUR_NAME = "your.name";
public static final String SAY_HELLO = "say.hello";
public static final String PATH = "";
private final GreetService greetService = new GreetService();
private final Button button = new Button();
private final TextField textField = new TextField();
public MainView() {
button.addClickListener(e -> {
add(new Paragraph(greetService.greet(textField.getValue())));
});
add(textField, button);
}
@Override
public void localeChange(LocaleChangeEvent localeChangeEvent) {
button.setText(getTranslation(SAY_HELLO));
textField.setLabel(getTranslation(YOUR_NAME));
}
}
An dieser Klasse lässt sich das Grundmuster eines Vaadin-Views sehr gut ablesen. Die Seite ist über @Route direkt mit dem Routing verknüpft und wird zugleich in das MainLayout eingebettet. Mit TextField und Button definiert sie zwei zentrale UI-Komponenten, die im Konstruktor unmittelbar zusammengesetzt werden. Der Klick auf den Button löst eine serverseitige Aktion aus und ergänzt das Layout um einen neuen Absatz mit dem Ergebnis. Oberfläche, Ereignisbehandlung und Reaktion bleiben damit in einem geschlossenen Java-Modell zusammengeführt.
Bemerkenswert ist außerdem die bereits integrierte Mehrsprachigkeit. Über LocaleChangeObserver reagiert die View auf einen Sprachwechsel und aktualisiert Bezeichner wie Button-Text und Feldlabel direkt über die Übersetzungsschlüssel. Damit verbindet die Klasse Interaktion und Internationalisierung in einem sehr kleinen, aber vollständigen Beispiel.
Die eigentliche Textlogik liegt bereits nicht mehr direkt in der View, sondern in einer kleinen Service-Klasse:
public class GreetService implements Serializable {
public String greet(String name) {
if (name == null || name.isEmpty()) {
return "Hello anonymous user";
} else {
return "Hello " + name;
}
}
}
Gerade durch diese Reduktion wird deutlich, wie eine erste interaktive Seite in Vaadin aufgebaut ist. Komponenten, Event-Handling, Layout-Einbindung und eine kleine ausgelagerte Logik greifen bereits ineinander, ohne dass dafür zusätzliche Infrastruktur notwendig wäre. Zugleich bleibt die MainView Teil des größeren Anwendungsrahmens, der aus Routing und MainLayout besteht.
UI-Logik und fachliche Logik trennen
Mit der ersten interaktiven View stellt sich die Frage nach der Verteilung der Verantwortung. Eine View beschreibt die Oberfläche, Eingaben und Reaktionen auf Benutzeraktionen. Unterstützende Logik kann hingegen in eigene Klassen ausgelagert werden.
Im Projekt übernimmt diese Rolle der GreetService. Auch wenn seine Aufgabe klein bleibt, zeigt er die Grenze zwischen der Komponentensteuerung in der View und anwendungsnaher Logik außerhalb der Oberfläche. Diese Trennung erleichtert spätere Erweiterungen, weil zusätzliche Regeln oder Aufbereitungsschritte nicht direkt in die View integriert werden müssen.
Internationalisierung von Anfang an mitdenken
Texte entstehen in einer Vaadin-Anwendung unmittelbar dort, wo Komponenten erstellt werden. Gerade deshalb ist es sinnvoll, sprachabhängige Inhalte früh aus den Views herauszulösen und sie über Übersetzungsschlüssel sowie Ressourcen-Dateien zu verwalten.
Im Projekt wird das direkt in der MainView sichtbar. Die Klasse implementiert LocaleChangeObserver und bezieht Beschriftungen nicht als fest verdrahtete Strings, sondern über Übersetzungsschlüssel:
/**
* The main view contains a text field for getting the username and a button
* that shows a greeting message in a notification.
*/
@Route(value = MainView.PATH, layout = MainLayout.class)
public class MainView
extends VerticalLayout
implements LocaleChangeObserver {
public static final String YOUR_NAME = "your.name";
public static final String SAY_HELLO = "say.hello";
public static final String PATH = "";
private final GreetService greetService = new GreetService();
private final Button button = new Button();
private final TextField textField = new TextField();
public MainView() {
button.addClickListener(e -> {
add(new Paragraph(greetService.greet(textField.getValue())));
});
add(textField, button);
}
@Override
public void localeChange(LocaleChangeEvent localeChangeEvent) {
button.setText(getTranslation(SAY_HELLO));
textField.setLabel(getTranslation(YOUR_NAME));
}
}
Gerade die Methode localeChange(…) zeigt die praktische Umsetzung der Mehrsprachigkeit. Statt Texte direkt beim Erzeugen der Komponenten fest einzutragen, werden sie über getTranslation(…) anhand ihrer Schlüssel aufgelöst. Dadurch kann dieselbe View je nach aktiver Sprache unterschiedliche Beschriftungen anzeigen.
Die zugehörigen Ressourcen befinden sich im Projekt unter src/main/resources/vaadin-i18n. Die Standardsprache ist in translations.properties definiert:
your.name=Your name
say.hello=Say Hello
Für Deutsch existiert eine eigene Datei translations_de.properties:
your.name=Dein Name
say.hello=Sag Hallo
An diesen Dateien wird deutlich, wie die sprachabhängigen Inhalte aus dem Java-Code herausgelöst werden. Die View kennt nur noch die Schlüssel your.name und say.hello, während die eigentlichen Texte in den Ressourcen gepflegt werden. Damit bleibt die Oberfläche sprachlich anpassbar, ohne dass die Komponentenklassen selbst umformuliert werden müssen.
Im Projekt ist die Mehrsprachigkeit bereits vorbereitet. Texte werden nicht als feste Bestandteile einzelner Komponenten behandelt, sondern als Inhalte, die je nach aktiver Sprache bereitgestellt werden. In Verbindung mit sprachabhängiger Aktualisierung der Oberfläche wird sichtbar, dass Internationalisierung nicht nur aus Dateien besteht, sondern auch ein Verhalten der Anwendung beschreibt.
Zusätzliche Views als Blaupause für eigene Seiten
Neben der MainView zeigen AboutView und YoutubeView weitere Seitentypen innerhalb derselben Anwendung. Sie stehen für stärker informationsorientierte Inhalte und machen deutlich, dass dieselbe Struktur nicht nur für interaktive Eingabeseiten, sondern auch für einfachere Inhaltsseiten funktioniert.
Damit wird sichtbar, wie neue Views in Routing, Navigation und MainLayout eingebettet werden, ohne eigene Sonderstrukturen einzuführen. Unterschiedliche Seitentypen lassen sich so innerhalb eines gemeinsamen Rahmens umsetzen.
Build, Start und Entwicklungsmodus
Zum praktischen Arbeiten mit dem Projekt gehören Build, lokaler Start und Entwicklungsmodus. Maven bündelt dafür Abhängigkeiten, Build-Schritte und Plugin-Konfigurationen. Für Vaadin ist das besonders relevant, weil neben dem Java-Code auch Theme, Ressourcen und Vaadin-spezifische Verarbeitung in den Entwicklungsablauf eingebunden sind.
Jetty übernimmt den lokalen Betrieb mithilfe eines leichtgewichtigen Servlet-Containers. Dadurch bleibt der Weg vom Quelltext zur laufenden Anwendung kurz. Änderungen an Views, Layouts, Übersetzungen oder Theme-Regeln lassen sich im Entwicklungsmodus direkt überprüfen.
Das WAR-Packaging fügt sich in dieses Modell ein und hält die Anwendung im klassischen Webanwendungskontext.
Fazit
Das Projekt macht die zentralen Schichten einer Vaadin-Flow-Anwendung in kompakter Form sichtbar: globale Konfiguration über die AppShell, dauerhafte UI-Struktur im MainLayout, Routing für erreichbare Seiten, konkrete Views für Inhalte, einen ausgelagerten Service für unterstützende Logik, vorbereitete Internationalisierung, ein eigenes Theme sowie einen klaren Build- und Startpfad.
Gerade in dieser kompakten Form lässt sich gut nachvollziehen, wie diese Bausteine zusammenwirken. Das Repository zeigt damit nicht nur einzelne Vaadin-Techniken, sondern auch den Aufbau einer Anwendung, deren Struktur vom ersten Einstiegspunkt bis zum Entwicklungsmodus durchgängig erkennbar bleibt.