Im vorherigen Teil haben wir uns die Implementierung auf der Serverseite angesehen. In diesem Teil geht es nun um die Abbildung auf der Benutzerseite.
Table of Contents
Den Quelltext zu dem Ausgangszustand ist auf GitHub unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-00 zu finden. Der nachfolgende Screenshot zeigt diesen Entwicklunsgstand.

Der Fokus dieses Adventskalender-Tages liegt auf der Einführung einer gezielten Filter-, Such- und Paging-Funktionalität auf der UI Seite. Ziel ist es, die bestehende „Overview“-Ansicht so zu erweitern, dass Benutzer gezielt nach bestimmten Kurzcodes oder URL-Fragmenten suchen, Zeiträume einschränken und die Ergebnisse seitenweise abrufen können. Alle technischen Grundlagen dazu sind in dem vorherigen Teil gelegt worden.
Den Quelltext zu den heutigen Artikel befindet sich auf Github unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-01 zu finden.
Der nachfolgende Screenshot zeigt den darin befindlichen Entwicklungsstand den wir heute erreichen werden.

Integration in die Vaadin-UI
Nach der Umsetzung der Filter- und Paginglogik auf Server- und Clientseite steht nun die Integration in die Benutzeroberfläche im Mittelpunkt. Die Vaadin-UI fungiert als sichtbare Schicht des Systems und ist entscheidend für das Nutzererlebnis. Ziel dieses Kapitels ist es zu zeigen, wie die neuen Funktionen – Filtern, Suchen und seitenweises Blättern – in einer modernen, reaktiven Oberfläche zusammengeführt werden.
Im Gegensatz zu den vorangegangenen Teilen, die primär technische Strukturen beschrieben, liegt der Fokus hier auf der Interaktion zwischen Benutzer und System. Die OverviewView dient dabei als zentrale Ansicht, über die Benutzer direkt mit der API kommunizieren, ohne technische Details kennen zu müssen.
Die folgenden Abschnitte zeigen, wie aus den neuen API-Endpunkten und Clientmethoden eine dynamische und zugleich intuitive Oberfläche entsteht – von der Datenbindung über die Filterlogik bis hin zum visuellen Feinschliff und ergonomischen Design.
OverviewView – Filter, Suche und Paging in der Vaadin-UI
Mit den neuen Server- und Client-Funktionen steht nun die Grundlage für eine interaktive Benutzeroberfläche bereit. In diesem Kapitel wird die überarbeitete OverviewView beschrieben – jene zentrale Ansicht der Vaadin-Anwendung, in der alle bestehenden Kurzlinks aufgelistet, gefiltert und verwaltet werden können.
Die ursprüngliche Version der View zeigte lediglich eine vollständige Liste aller Mappings. In der neuen Variante wurde sie zu einer dynamischen, filterbaren und seitenbasierten Oberfläche ausgebaut, die direkt mit den Endpunkten /list und /list/count kommuniziert.
Aufbau der Benutzeroberfläche
Die OverviewView kombiniert mehrere Eingabeelemente, um Such- und Filterkriterien zu erfassen:
- Textfelder für Shortcode- und URL-Teilstrings (
codePart,urlPart) - Checkboxen für Groß-/Kleinschreibung (
codeCase,urlCase) - Datum- und Zeitauswahl für Zeiträume (
fromDate,toDate) - Dropdowns für Sortierung (
sortBy,dir) - Seitengröße über ein numerisches Eingabefeld (
pageSize)
Zusätzlich ermöglichen Navigations-Buttons (Prev / Next) die Seitensteuerung. Alle Eingaben werden in ein UrlMappingListRequest-Objekt überführt, das beim Aktualisieren der Ansicht an den URLShortenerClient übergeben wird.
Zentrale Logik: DataProvider
Die Datenversorgung des Grids erfolgt über einen CallbackDataProvider, der dynamisch auf Benutzerinteraktionen reagiert. Dieser ruft im Hintergrund zwei Methoden des Clients auf:
list()– lädt die eigentlichen Datensätze der aktuellen Seite.listCount()– bestimmt die Gesamtanzahl, um die Seitennavigation zu steuern.
Ein vereinfachter Ausschnitt der Initialisierung:
dataProvider = new CallbackDataProvider<>(
q -> {
var req = buildFilter(currentPage, pageSize.getValue());
return urlShortenerClient.list(req).stream();
},
q -> {
var baseReq = buildFilter(null, null);
totalCount = urlShortenerClient.listCount(baseReq);
refreshPageInfo();
return totalCount;
}
);
Durch diese Architektur ist die View in der Lage, auf jede Änderung im Filter sofort zu reagieren, ohne dass die gesamte Seite neu geladen werden muss. Der Benutzer kann bequem in den Ergebnissen blättern, Filter anpassen oder Zeiträume verändern – die Daten werden jeweils live nachgeladen.
UX-Verbesserungen
Neben der funktionalen Erweiterung wurde auch die Benutzerfreundlichkeit verbessert:
- Responsive Layouts: Alle Filterelemente ordnen sich flexibel an, auch bei kleineren Fenstergrößen.
- Automatische Seitennavigation: Die Buttons
PrevundNextwerden je nach Kontext automatisch deaktiviert. - Datum-Zeit-Kombination: Durch getrennte DatePicker- und TimePicker-Felder bleibt die Eingabe präzise und intuitiv.
Diese Neuerungen führen zu einem erheblichen Mehrwert im täglichen Gebrauch der Anwendung: Anstelle statischer Tabellen steht nun ein interaktives, reaktives Interface, das sowohl performanter als auch ergonomischer ist.
Im nächsten Abschnitt wird die Implementierung der Paging- und Filterlogik im Detail betrachtet, einschließlich der Methode buildFilter() und der Handhabung der Parameter in der UI.
Implementierungsdetails der Filterlogik – buildFilter() und Datenbindung
Die Methode buildFilter() bildet das Herzstück der neuen Filtermechanik in der Vaadin-Oberfläche. Sie sammelt sämtliche Eingaben des Nutzers – etwa Textfelder, Checkboxen, Datumsangaben und Sortieroptionen – und überträgt sie in ein UrlMappingListRequest-Objekt. Dieses Request-Objekt wird anschließend an den Client übergeben, der daraus die passenden Query-Parameter für den Server generiert.
Aufbau der Methode buildFilter()
Im Zentrum steht die Builder-Struktur des UrlMappingListRequest. Die Methode liest zunächst alle UI-Komponenten aus, prüft auf leere Felder und erstellt daraus ein konsistentes Filterobjekt:
private UrlMappingListRequest buildFilter(Integer page, Integer size) {
UrlMappingListRequest.Builder b = UrlMappingListRequest.builder();
if (codePart.getValue() != null && !codePart.getValue().isBlank()) {
b.codePart(codePart.getValue());
}
b.codeCaseSensitive(Boolean.TRUE.equals(codeCase.getValue()));
if (urlPart.getValue() != null && !urlPart.getValue().isBlank()) {
b.urlPart(urlPart.getValue());
}
b.urlCaseSensitive(Boolean.TRUE.equals(urlCase.getValue()));
if (fromDate.getValue() != null && fromTime.getValue() != null) {
var zdt = ZonedDateTime.of(fromDate.getValue(), fromTime.getValue(), ZoneId.systemDefault());
b.from(zdt.toInstant());
} else if (fromDate.getValue() != null) {
var zdt = fromDate.getValue().atStartOfDay(ZoneId.systemDefault());
b.from(zdt.toInstant());
}
if (toDate.getValue() != null && toTime.getValue() != null) {
var zdt = ZonedDateTime.of(toDate.getValue(), toTime.getValue(), ZoneId.systemDefault());
b.to(zdt.toInstant());
} else if (toDate.getValue() != null) {
var zdt = toDate.getValue().atTime(23, 59).atZone(ZoneId.systemDefault());
b.to(zdt.toInstant());
}
if (sortBy.getValue() != null && !sortBy.getValue().isBlank()) b.sort(sortBy.getValue());
if (dir.getValue() != null && !dir.getValue().isBlank()) b.dir(dir.getValue());
if (page != null && size != null) {
b.page(page).size(size);
}
return b.build();
}
Die Methode arbeitet strikt defensiv: Leere Felder werden ignoriert, optionale Werte nur gesetzt, wenn sie tatsächlich vorhanden sind. Das Ergebnis ist stets ein gültiges, wohldefiniertes Request-Objekt.
Datenfluss zwischen UI und Server
Die erzeugten Filterobjekte werden über den CallbackDataProvider an den URLShortenerClient übergeben. Dieser generiert daraus den Query-String für den HTTP-Request. Dadurch entsteht eine klare, lineare Kette:
UI → UrlMappingListRequest → Client → /list-Endpunkt → Filter → Ergebnisse
Jede Änderung im UI – sei es eine neue Suche, eine geänderte Seitengröße oder ein Datumsfilter – löst automatisch einen Refresh aus. Die Grid-Komponente aktualisiert sich dann mit den neu gefilterten Daten.
Vorteile der Kapselung
Diese Struktur bietet mehrere Vorteile:
- Wiederverwendbarkeit:
buildFilter()kapselt sämtliche UI-Logik in einer Methode. - Testbarkeit: Das Ergebnis lässt sich leicht in Unit-Tests prüfen, ohne Vaadin-spezifische Klassen zu benötigen.
- Erweiterbarkeit: Neue Filterfelder können problemlos ergänzt werden, solange sie im Builder hinterlegt sind.
Durch diese saubere Entkopplung bleibt die Vaadin-Oberfläche übersichtlich, während gleichzeitig eine tiefe Integration mit der API erreicht wird. Die Methode buildFilter() ist somit der zentrale Knotenpunkt zwischen Benutzerinteraktion und serverseitiger Datenlogik.
Paging und Nutzerinteraktion – Steuerung, Anzeige, Feedback
Die überarbeitete OverviewView bietet eine intuitive Steuerung für seitenweises Navigieren durch große Ergebnismengen. Ziel ist, dass Anwender:innen schnell erkennen, wo in der Ergebnisliste sie sich befinden, wie viele Elemente insgesamt existieren und welche Interaktionen aktuell möglich sind.
Paging-Elemente in der UI
- Buttons:
Prev(zur vorherigen Seite) undNext(zur nächsten Seite). - Seitengröße:
IntegerField pageSizemit Min/Max-Grenzen und Step-Buttons. - Statusanzeige:
pageInfozeigt z. B. „Page 3 / 12 • 289 total“.
Diese Elemente sind unmittelbar mit dem DataProvider verknüpft und werden nach jeder Abfrage aktualisiert.
Seitenwechsel und Begrenzung
Beim Klick auf Prev/Next wird die aktuelle Seite angepasst, anschließend lädt die View die Daten neu:
prevBtn.addClickListener(e -> {
if (currentPage > 1) {
currentPage--;
refresh();
}
});
nextBtn.addClickListener(e -> {
int size = Optional.ofNullable(pageSize.getValue()).orElse(25);
int maxPage = Math.max(1, (int) Math.ceil((double) totalCount / size));
if (currentPage < maxPage) {
currentPage++;
refresh();
}
});
Die Seitengröße wirkt sich direkt auf die Berechnung von maxPage aus. Änderungen am Feld pageSize setzen die aktuelle Seite auf 1 zurück und stoßen ein Refresh an:
pageSize.addValueChangeListener(e -> {
currentPage = 1;
grid.setPageSize(Optional.ofNullable(e.getValue()).orElse(25));
refresh();
});
Aktualisierung der Statusanzeige
Die Methode refreshPageInfo() synchronisiert Buttons und Anzeige mit dem aktuellen Datenstand:
private void refreshPageInfo() {
int size = Optional.ofNullable(pageSize.getValue()).orElse(25);
int maxPage = Math.max(1, (int) Math.ceil((double) totalCount / size));
currentPage = Math.min(Math.max(1, currentPage), maxPage);
pageInfo.setText("Page " + currentPage + " / " + maxPage + " • " + totalCount + " total");
prevBtn.setEnabled(currentPage > 1);
nextBtn.setEnabled(currentPage < maxPage);
}
Damit wird verhindert, dass die Navigation in ungültige Zustände gerät (z. B. Prev auf Seite 1 oder Next hinter der letzten Seite).
Zusammenspiel mit list() und listCount()
- Zählung (
listCount): Ermittelt die Gesamtmenge (totalCount) ohne Daten zu übertragen, die nicht angezeigt werden müssen. - Daten (
list): Lädt nur die aktuell sichtbare Teilmenge (basierend auf Seite/Größe und aktuellen Filtern).
Die Kombination minimiert Netzwerklast und sorgt für konstant schnelle Reaktionszeiten.
UX-Details und Fehlertoleranz
- Disable-Logik: Buttons werden abhängig von
currentPage/maxPageautomatisch (de)aktiviert. - Fehlerbehandlung: Netz- oder Serverfehler werden per
Notification.show("Loading failed")sofort sichtbar gemacht. - Reset-Verhalten: Der
Reset-Button setzt Filter auf Standardwerte,currentPageauf 1 und triggertrefresh().
Durch diese Mechanismen entsteht eine reaktive, robuste Paging-Erfahrung, die auch bei sehr großen Datenmengen übersichtlich bleibt und dem Benutzer klare Rückmeldung gibt.
UX & Styling – visuelle und ergonomische Verfeinerungen
Nachdem die Funktionalität der Filter-, Such- und Paging-Mechanismen steht, konzentriert sich dieses Kapitel auf die Feinarbeit im User Interface. Ziel ist es, das Nutzungserlebnis der Vaadin-Oberfläche zu verbessern, ohne die technische Struktur zu verändern. Der Fokus liegt dabei auf Klarheit, Responsivität und konsistentem visuellem Feedback.
Gestalterische Grundprinzipien
- Klarheit über Komplexität – Jede Funktion (z. B. Filter, Sortierung, Paging) soll visuell eindeutig erkennbar sein, ohne den Benutzer mit Optionen zu überfordern.
- Visuelle Gruppierung – Logisch zusammengehörige Elemente werden in horizontale oder vertikale Layouts gruppiert (z. B. Zeitbereich, Sortieroptionen, Suchfelder).
- Konsistentes Feedback – Jede Benutzeraktion (z. B. Änderung eines Filters, Seitenwechsel, Löschaktion) erhält ein direktes optisches oder textuelles Feedback.
Beispielhafte Anpassungen
1. Layout-Struktur
Die Eingabeelemente für Filter und Paging wurden in mehrere Layout-Zeilen gegliedert:
var filterRow = new HorizontalLayout(
codePart, codeCase,
urlPart, urlCase,
fromGroup, toGroup,
sortBy, dir
);
filterRow.setDefaultVerticalComponentAlignment(Alignment.END);
var pagingRow = new HorizontalLayout(prevBtn, nextBtn, pageInfo, pageSize);
pagingRow.setDefaultVerticalComponentAlignment(Alignment.CENTER);
add(filterRow, pagingRow, grid);
Diese Aufteilung trennt semantisch zwischen Filtern und Navigation und verbessert damit die visuelle Orientierung.
2. Farb- und Themenkonsistenz
Die Buttons nutzen nun Vaadin-eigene Theme-Varianten, um semantische Bedeutung zu vermitteln:
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
resetBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
deleteBtn.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
Damit wird dem Benutzer unmittelbar signalisiert, welche Aktionen primär, neutral oder destruktiv sind.
3. Responsive Verhalten
Durch die Nutzung von setWrap(true) und prozentualen Breiten wird das Layout auch auf kleineren Bildschirmen optimal dargestellt. Filterelemente umbrechen automatisch in neue Zeilen, ohne den Gesamteindruck zu stören.
filterRow.setWrap(true);
filterRow.setWidthFull();
4. Interaktive Statusmeldungen
Fehler- und Erfolgsmeldungen werden über Notification.show() kommuniziert. Ergänzend kann optional eine visuelle Markierung des betroffenen Elements erfolgen – etwa durch temporäre Farbänderungen oder Tooltips.
Notification.show("Short link deleted.");
Zusammenführung von Funktion und Ästhetik
Durch diese gestalterischen und ergonomischen Feinanpassungen wird die OverviewView zu einem reaktiven Werkzeug für den täglichen Gebrauch. Die Anwendung wirkt nicht nur technisch ausgereift, sondern vermittelt auch visuell den Anspruch an Präzision und Qualität.
Damit ist die Implementierung der UI für die neue Filter- und Suchlogik abgeschlossen – eine Grundlage, auf der spätere Erweiterungen wie Caching, Auto-Suggest oder erweiterte Sortierlogiken aufbauen können.
Fazit und Ausblick
Mit der Implementierung der Filter-, Such- und Pagingfunktionen wurde der erste Meilenstein des Adventskalender-Projekts erreicht. Aus einer statischen Übersicht ist ein dynamisches, interaktives System geworden, das es erlaubt, gezielt und effizient auf die Daten zuzugreifen. Dabei blieb der Grundgedanke des Projekts – eine vollständig in Core Java implementierte Lösung ohne externe Frameworks – konsequent erhalten.
Rückblick
Dieser Tag hat gezeigt, wie sich durch klare Trennung von Verantwortlichkeiten und sauberes API-Design eine solide Basis für komplexere Funktionalitäten schaffen lässt:
- Serverseitig: Einführung des
UrlMappingFilterund Ausbau desInMemoryUrlMappingStorefür strukturierte Abfragen. - Clientseitig: Erweiterung des
URLShortenerClientum typsichere Methoden zur Filterung und Zählung. - UI-seitig: Integration der neuen Funktionen in die Vaadin-basierte Oberfläche mit reaktivem Paging, intuitiver Navigation und klarer Benutzerführung.
Das Ergebnis ist ein konsistentes Gesamtsystem, das modular, erweiterbar und testbar bleibt.
Cheers Sven