Java Desktop in Browser with Vaadin: Building Modern Web UIs

Vaadin is a powerful Java framework that enables developers to build modern web applications entirely in Java, without needing to write HTML, CSS, or JavaScript. It's particularly valuable for desktop-style applications that need to run in a browser.


What is Vaadin?

Vaadin provides a component-based architecture where you build web UIs using pure Java. The framework handles:

  • Client-Server Communication: Automatic AJAX updates
  • Component Rendering: Converts Java components to HTML/JavaScript
  • State Management: Maintains UI state on the server
  • Theming: Consistent styling across components

Key Features

  • 100% Java: No frontend coding required
  • Type-Safe: Compile-time error checking
  • Rich Component Library: 50+ pre-built UI components
  • Progressive Web App (PWA) Support: Offline capabilities
  • Security: Built-in protection against common vulnerabilities
  • Integration: Easy integration with Spring, CDI, and other Java frameworks

Getting Started

Dependencies (Maven)

<properties>
<vaadin.version>24.3.0</vaadin.version>
</properties>
<dependencies>
<!-- Vaadin Core -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
<version>${vaadin.version}</version>
</dependency>
<!-- Spring Boot Integration (optional) -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
<version>${vaadin.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-frontend</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Basic Vaadin Application

Example 1: Simple Vaadin Application

package com.example.vaadindemo;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;
@Route("") // Maps to root URL
@PWA(name = "My Vaadin App", shortName = "MyApp")
public class MainView extends VerticalLayout {
public MainView() {
// Create components
TextField nameField = new TextField("Your name");
Button sayHelloButton = new Button("Say Hello", 
event -> sayHello(nameField.getValue()));
// Add components to layout
add(nameField, sayHelloButton);
// Center the layout
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
setSizeFull();
}
private void sayHello(String name) {
if (name == null || name.trim().isEmpty()) {
Notification.show("Please enter your name");
} else {
Notification.show("Hello, " + name + "!");
}
}
}

Example 2: Complete Desktop-Style Application

package com.example.vaadindemo;
import com.vaadin.flow.component.*;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.tabs.Tab;
import com.vaadin.flow.component.tabs.Tabs;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.Route;
import java.util.ArrayList;
import java.util.List;
@Route("desktop")
public class DesktopApplication extends VerticalLayout {
private final List<Customer> customers = new ArrayList<>();
private final Grid<Customer> customerGrid = new Grid<>(Customer.class);
private final Binder<Customer> binder = new Binder<>(Customer.class);
// Form fields
private final TextField firstName = new TextField("First Name");
private final TextField lastName = new TextField("Last Name");
private final EmailField email = new EmailField("Email");
private final TextField phone = new TextField("Phone");
public DesktopApplication() {
initializeUI();
setupGrid();
setupForm();
loadSampleData();
}
private void initializeUI() {
// Application header
H1 header = new H1("Customer Management System");
header.getStyle().set("color", "var(--lumo-primary-text-color)");
// Create tabs for different sections
Tab customerTab = new Tab("Customers", new Icon(VaadinIcon.USERS));
Tab reportsTab = new Tab("Reports", new Icon(VaadinIcon.CHART));
Tab settingsTab = new Tab("Settings", new Icon(VaadinIcon.COG));
Tabs tabs = new Tabs(customerTab, reportsTab, settingsTab);
tabs.setWidthFull();
// Main content area
VerticalLayout contentArea = new VerticalLayout();
contentArea.setSizeFull();
contentArea.setPadding(false);
// Add everything to main layout
add(header, tabs, contentArea);
setSizeFull();
setPadding(false);
// Tab selection handler
tabs.addSelectedChangeListener(event -> {
contentArea.removeAll();
if (event.getSelectedTab() == customerTab) {
contentArea.add(createCustomerManagementLayout());
} else if (event.getSelectedTab() == reportsTab) {
contentArea.add(createReportsLayout());
} else if (event.getSelectedTab() == settingsTab) {
contentArea.add(createSettingsLayout());
}
});
// Show customer tab by default
contentArea.add(createCustomerManagementLayout());
}
private Component createCustomerManagementLayout() {
HorizontalLayout layout = new HorizontalLayout();
layout.setSizeFull();
layout.setSpacing(true);
// Form panel
VerticalLayout formLayout = new VerticalLayout();
formLayout.setWidth("400px");
formLayout.setPadding(true);
formLayout.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
H1 formTitle = new H1("Customer Form");
Button saveButton = new Button("Save Customer", 
new Icon(VaadinIcon.CHECK), event -> saveCustomer());
Button clearButton = new Button("Clear", 
new Icon(VaadinIcon.TRASH), event -> clearForm());
formLayout.add(formTitle, firstName, lastName, email, phone, 
new HorizontalLayout(saveButton, clearButton));
// Grid panel
VerticalLayout gridLayout = new VerticalLayout();
gridLayout.setSizeFull();
Button refreshButton = new Button("Refresh", 
new Icon(VaadinIcon.REFRESH), event -> refreshGrid());
gridLayout.add(new HorizontalLayout(refreshButton), customerGrid);
layout.add(formLayout, gridLayout);
return layout;
}
private Component createReportsLayout() {
VerticalLayout layout = new VerticalLayout();
layout.setPadding(true);
layout.add(new H1("Reports Dashboard"),
new com.vaadin.flow.component.html.Paragraph("Sales reports will be displayed here"),
new Button("Generate Report", 
new Icon(VaadinIcon.FILE_TEXT)));
return layout;
}
private Component createSettingsLayout() {
VerticalLayout layout = new VerticalLayout();
layout.setPadding(true);
layout.add(new H1("Application Settings"),
new com.vaadin.flow.component.html.Paragraph("Configure your application preferences"),
new Button("Save Settings", 
new Icon(VaadinIcon.COG)));
return layout;
}
private void setupGrid() {
customerGrid.setColumns("firstName", "lastName", "email", "phone");
customerGrid.getColumnByKey("firstName").setHeader("First Name");
customerGrid.getColumnByKey("lastName").setHeader("Last Name");
customerGrid.getColumnByKey("email").setHeader("Email");
customerGrid.getColumnByKey("phone").setHeader("Phone");
customerGrid.asSingleSelect().addValueChangeListener(event -> {
if (event.getValue() != null) {
populateForm(event.getValue());
} else {
clearForm();
}
});
}
private void setupForm() {
binder.forField(firstName)
.asRequired("First name is required")
.bind(Customer::getFirstName, Customer::setFirstName);
binder.forField(lastName)
.asRequired("Last name is required")
.bind(Customer::getLastName, Customer::setLastName);
binder.forField(email)
.asRequired("Email is required")
.withValidator(this::isValidEmail, "Invalid email format")
.bind(Customer::getEmail, Customer::setEmail);
binder.forField(phone)
.bind(Customer::getPhone, Customer::setPhone);
}
private boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
private void saveCustomer() {
Customer customer = new Customer();
try {
binder.writeBean(customer);
customers.add(customer);
refreshGrid();
clearForm();
Notification.show("Customer saved successfully!");
} catch (ValidationException e) {
Notification.show("Please fix validation errors");
}
}
private void clearForm() {
binder.readBean(null);
customerGrid.asSingleSelect().clear();
}
private void populateForm(Customer customer) {
binder.readBean(customer);
}
private void refreshGrid() {
customerGrid.setItems(customers);
}
private void loadSampleData() {
customers.add(new Customer("John", "Doe", "[email protected]", "555-0101"));
customers.add(new Customer("Jane", "Smith", "[email protected]", "555-0102"));
customers.add(new Customer("Bob", "Johnson", "[email protected]", "555-0103"));
refreshGrid();
}
// Customer data model
public static class Customer {
private String firstName;
private String lastName;
private String email;
private String phone;
public Customer() {}
public Customer(String firstName, String lastName, String email, String phone) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
}
// Getters and setters
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}
}

Advanced Vaadin Features

Example 3: Data Binding with Backend Service

package com.example.vaadindemo;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Route("data-binding")
public class DataBindingView extends VerticalLayout {
private final ProductService productService;
private final Grid<Product> grid = new Grid<>(Product.class);
private final TextField filterField = new TextField();
private ListDataProvider<Product> dataProvider;
public DataBindingView(ProductService productService) {
this.productService = productService;
initializeUI();
setupGrid();
setupFilter();
}
private void initializeUI() {
setSizeFull();
setPadding(true);
filterField.setPlaceholder("Filter by product name...");
filterField.setClearButtonVisible(true);
filterField.setWidth("300px");
add(filterField, grid);
grid.setSizeFull();
}
private void setupGrid() {
// Configure columns
grid.removeAllColumns();
grid.addColumn(Product::getName).setHeader("Product Name").setSortable(true);
grid.addColumn(Product::getCategory).setHeader("Category").setSortable(true);
grid.addColumn(Product::getPrice).setHeader("Price")
.setSortable(true)
.setComparator((p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice()));
grid.addColumn(Product::getStock).setHeader("In Stock")
.setSortable(true);
// Load data
List<Product> products = productService.getAllProducts();
dataProvider = new ListDataProvider<>(products);
grid.setDataProvider(dataProvider);
}
private void setupFilter() {
filterField.setValueChangeMode(ValueChangeMode.LAZY);
filterField.addValueChangeListener(e -> applyFilter(e.getValue()));
}
private void applyFilter(String filterText) {
dataProvider.setFilter(product -> 
product.getName().toLowerCase().contains(filterText.toLowerCase()) ||
product.getCategory().toLowerCase().contains(filterText.toLowerCase())
);
}
}
@Service
class ProductService {
private final List<Product> products = new ArrayList<>();
public ProductService() {
// Sample data
products.add(new Product("Laptop", "Electronics", 999.99, 15));
products.add(new Product("Smartphone", "Electronics", 699.99, 25));
products.add(new Product("Desk Chair", "Furniture", 199.99, 10));
products.add(new Product("Coffee Mug", "Kitchen", 12.99, 100));
products.add(new Product("Notebook", "Office Supplies", 4.99, 50));
}
public List<Product> getAllProducts() {
return new ArrayList<>(products);
}
public List<Product> searchProducts(String query) {
return products.stream()
.filter(p -> p.getName().toLowerCase().contains(query.toLowerCase()) ||
p.getCategory().toLowerCase().contains(query.toLowerCase()))
.collect(Collectors.toList());
}
}
class Product {
private String name;
private String category;
private double price;
private int stock;
public Product(String name, String category, double price, int stock) {
this.name = name;
this.category = category;
this.price = price;
this.stock = stock;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
}

Example 4: Dialog and Notification System

package com.example.vaadindemo;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
@Route("dialogs")
public class DialogView extends VerticalLayout {
public DialogView() {
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
setSizeFull();
setSpacing(true);
Button confirmationDialogButton = new Button("Show Confirmation Dialog", 
event -> showConfirmationDialog());
Button formDialogButton = new Button("Show Form Dialog", 
event -> showFormDialog());
Button customDialogButton = new Button("Show Custom Dialog", 
event -> showCustomDialog());
add(new H2("Dialog Examples"), 
confirmationDialogButton, 
formDialogButton, 
customDialogButton);
}
private void showConfirmationDialog() {
Dialog dialog = new Dialog();
VerticalLayout layout = new VerticalLayout();
layout.setPadding(true);
layout.setAlignItems(Alignment.CENTER);
com.vaadin.flow.component.html.Paragraph message = 
new com.vaadin.flow.component.html.Paragraph(
"Are you sure you want to delete this item?");
Button confirmButton = new Button("Delete", event -> {
Notification.show("Item deleted!");
dialog.close();
});
confirmButton.getStyle().set("color", "red");
Button cancelButton = new Button("Cancel", event -> dialog.close());
layout.add(message, new HorizontalLayout(confirmButton, cancelButton));
dialog.add(layout);
dialog.open();
}
private void showFormDialog() {
Dialog dialog = new Dialog();
dialog.setWidth("400px");
FormLayout formLayout = new FormLayout();
TextField nameField = new TextField("Name");
TextField emailField = new TextField("Email");
TextArea messageArea = new TextArea("Message");
messageArea.setHeight("100px");
formLayout.add(nameField, emailField, messageArea);
formLayout.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1)
);
Button submitButton = new Button("Submit", event -> {
if (!nameField.getValue().isEmpty() && !emailField.getValue().isEmpty()) {
Notification.show("Form submitted successfully!");
dialog.close();
} else {
Notification.show("Please fill all required fields");
}
});
Button cancelButton = new Button("Cancel", event -> dialog.close());
HorizontalLayout buttonLayout = new HorizontalLayout(submitButton, cancelButton);
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
VerticalLayout mainLayout = new VerticalLayout(
new H2("Contact Form"), formLayout, buttonLayout
);
mainLayout.setPadding(true);
dialog.add(mainLayout);
dialog.open();
}
private void showCustomDialog() {
Dialog dialog = new Dialog();
dialog.setCloseOnEsc(false);
dialog.setCloseOnOutsideClick(false);
// Custom styling
dialog.getStyle()
.set("border-radius", "10px")
.set("box-shadow", "0 4px 20px rgba(0,0,0,0.2)");
VerticalLayout content = new VerticalLayout();
content.setPadding(true);
content.setAlignItems(Alignment.CENTER);
content.setSpacing(true);
com.vaadin.flow.component.icon.Icon icon = 
new com.vaadin.flow.component.icon.Icon("lumo", "checkmark");
icon.setColor("var(--lumo-success-color)");
icon.setSize("48px");
H2 title = new H2("Operation Successful");
com.vaadin.flow.component.html.Paragraph message = 
new com.vaadin.flow.component.html.Paragraph(
"Your changes have been saved successfully.");
Button okButton = new Button("OK", event -> dialog.close());
okButton.getStyle().set("margin-top", "20px");
content.add(icon, title, message, okButton);
dialog.add(content);
dialog.open();
}
}

Spring Boot Integration

Example 5: Spring Boot Vaadin Application

package com.example.vaadindemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@SpringBootApplication
public class VaadinDemoApplication {
public static void main(String[] args) {
SpringApplication.run(VaadinDemoApplication.class, args);
}
}
// Service layer
@Service
class TaskService {
private final List<Task> tasks = new ArrayList<>();
private long nextId = 1;
public List<Task> getAllTasks() {
return new ArrayList<>(tasks);
}
public Task saveTask(Task task) {
if (task.getId() == null) {
task.setId(nextId++);
tasks.add(task);
} else {
tasks.removeIf(t -> t.getId().equals(task.getId()));
tasks.add(task);
}
return task;
}
public void deleteTask(Long id) {
tasks.removeIf(t -> t.getId().equals(id));
}
public void toggleTask(Long id) {
tasks.stream()
.filter(t -> t.getId().equals(id))
.findFirst()
.ifPresent(task -> task.setCompleted(!task.isCompleted()));
}
}
// Entity class
class Task {
private Long id;
private String title;
private String description;
private boolean completed;
public Task() {}
public Task(String title, String description) {
this.title = title;
this.description = description;
this.completed = false;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
}

Best Practices

1. Use Service Classes

@Service
public class CustomerService {
@PersistenceContext
private EntityManager entityManager;
public List<Customer> findAll() {
return entityManager.createQuery("SELECT c FROM Customer c", Customer.class)
.getResultList();
}
}

2. Implement Responsive Design

public class ResponsiveView extends VerticalLayout {
public ResponsiveView() {
setPadding(true);
setSpacing(true);
// Responsive configuration
setResponsiveSteps(
new ResponsiveStep("0", 1),   // 1 column on mobile
new ResponsiveStep("500px", 2), // 2 columns on tablet
new ResponsiveStep("800px", 3)  // 3 columns on desktop
);
}
}

3. Use CSS for Custom Styling

public class StyledView extends VerticalLayout {
public StyledView() {
addClassName("styled-view");
Button primaryButton = new Button("Primary Action");
primaryButton.addClassName("primary-action");
add(primaryButton);
}
}
.styled-view {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.primary-action {
background-color: var(--lumo-primary-color);
color: white;
border-radius: 25px;
padding: 10px 20px;
}

Deployment Options

1. Traditional WAR Deployment

<!-- Maven WAR packaging -->
<packaging>war</packaging>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

2. Docker Deployment

FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/my-vaadin-app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

3. Cloud Deployment

  • AWS Elastic Beanstalk
  • Google App Engine
  • Azure App Service
  • Heroku

Conclusion

Vaadin provides a powerful solution for building desktop-like applications that run in the browser:

Key Advantages:

  • Pure Java Development: No frontend expertise required
  • Type Safety: Compile-time error checking
  • Rich Component Set: Pre-built, customizable components
  • Security: Built-in CSRF and XSS protection
  • Integration: Seamless Spring Boot integration
  • Progressive Web Apps: Offline capabilities

Ideal Use Cases:

  • Enterprise Applications: CRM, ERP, internal tools
  • Data-Intensive Applications: Dashboards, reporting tools
  • Business Applications: Forms, workflows, data entry
  • Admin Panels: Content management, system configuration

Performance Considerations:

  • Use @Push for real-time updates
  • Implement lazy loading for large datasets
  • Optimize database queries
  • Use CDN for static resources

Vaadin enables Java developers to create modern, responsive web applications while leveraging their existing Java skills and ecosystem.


Next Steps: Explore Vaadin Fusion for hybrid Java/TypeScript development or Vaadin Flow for pure Java solutions. Consider Vaadin's commercial components for advanced charts, spreadsheets, and reporting capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper