Vaadin #
Membangun antarmuka web untuk aplikasi Java enterprise biasanya berarti memisahkan dua dunia: tim backend menulis Java, tim frontend menulis JavaScript, dan keduanya harus berkoordinasi melalui REST API. Vaadin menawarkan pendekatan yang sama sekali berbeda — kamu membangun seluruh UI dalam Java murni, tanpa menyentuh HTML, CSS, atau JavaScript secara langsung. Setiap komponen UI adalah objek Java, event handler adalah method Java, dan data binding dilakukan melalui tipe yang aman di waktu kompilasi. Di balik layar, Vaadin menangani komunikasi antara server dan browser melalui WebSocket, merender perubahan UI secara efisien, dan menghasilkan HTML yang bisa diakses. Pendekatan ini menjadikan Vaadin pilihan yang sangat produktif untuk tim Java yang perlu membangun aplikasi internal, dashboard admin, atau sistem CRUD yang kompleks tanpa harus menguasai ekosistem frontend modern.
Arsitektur Vaadin #
Memahami bagaimana Vaadin bekerja di balik layar penting untuk menulis aplikasi yang performan dan menghindari jebakan umum.
Server-Side Rendering dan State #
Vaadin menjaga state UI di server. Setiap browser session memiliki satu instance UI yang berjalan di server. Ketika pengguna mengklik tombol, event dikirimkan ke server melalui WebSocket, server memproses event dan memperbarui state UI, lalu perubahan dikirim kembali ke browser sebagai differential update — hanya bagian yang berubah yang dikirim, bukan seluruh halaman.
sequenceDiagram
participant Browser
participant Server as Vaadin Server\n(Java)
Browser->>Server: HTTP request — akses /dashboard
Server-->>Browser: HTML awal + Vaadin JS bundle
Browser->>Server: WebSocket: klik tombol "Simpan"
Server->>Server: Jalankan event handler Java\nperbarui state UI
Server-->>Browser: WebSocket: differential update\n(hanya bagian yang berubah)
Browser->>Browser: Perbarui DOMKomponen dan Virtual DOM Server-Side #
Vaadin memiliki virtual DOM di sisi server. Ketika kamu memanggil button.setText("Disimpan"), Vaadin tidak langsung mengirim update ke browser — ia mencatat perubahan, mengumpulkan semua perubahan dalam satu request-response cycle, lalu mengirimkan diff yang efisien.
flowchart TD
subgraph Server[Server Java]
UI[UI Instance\nper session]
VDOM[Virtual DOM\nServer-side]
COMP[Komponen Java\nButton TextField Grid]
UI --> VDOM
COMP --> VDOM
end
subgraph Browser[Browser]
DOM[Real DOM]
VAADINJS[Vaadin JS\nClient-side]
DOM --> VAADINJS
end
VDOM -->|WebSocket\ndifferential update| VAADINJS
VAADINJS -->|event: klik, input| VDOMKarena state ada di server, setiap tab browser yang terbuka adalah session terpisah dengan instance UI yang berbeda. Ini berarti kamu tidak perlu mengelola state secara manual di sisi client — tapi juga berarti setiap pengguna aktif menggunakan memori di server.
Setup Project #
Cara termudah membuat project Vaadin baru adalah melalui start.vaadin.com atau Spring Initializr dengan dependency Vaadin.
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>24.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Vaadin + Spring Boot integration -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Data JPA untuk akses database -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>24.4.0</version>
<executions>
<execution>
<goals>
<goal>prepare-frontend</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Struktur direktori project Vaadin:
src/
├── main/
│ ├── java/
│ │ └── com/contoh/app/
│ │ ├── Application.java
│ │ ├── views/ ← View (halaman UI)
│ │ │ ├── MainView.java
│ │ │ └── produk/
│ │ │ └── ProdukView.java
│ │ ├── service/ ← logika bisnis
│ │ └── data/ ← entity dan repository
│ └── resources/
│ ├── application.properties
│ └── META-INF/
│ └── resources/
│ └── frontend/ ← CSS kustom (opsional)
View dan Routing #
Di Vaadin, setiap “halaman” adalah sebuah View — kelas Java yang dianotasi dengan @Route. Vaadin Router menangani navigasi antar view tanpa reload halaman penuh.
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.PageTitle;
// @Route("") → halaman utama (URL: /)
// @Route("produk") → URL: /produk
// @Route("admin/pengguna") → URL: /admin/pengguna
@Route("")
@PageTitle("Beranda | Toko Online")
public class BerandaView extends VerticalLayout {
public BerandaView() {
// Semua komponen ditambahkan di constructor
H1 judul = new H1("Selamat Datang di Toko Online");
Paragraph deskripsi = new Paragraph(
"Kelola produk, pesanan, dan pelanggan dari satu tempat."
);
// add() menambahkan komponen ke layout ini
add(judul, deskripsi);
// Styling via Java API
setSizeFull();
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
}
}
Layout Utama (Main Layout) #
View yang berbagi tampilan umum (navbar, sidebar) bisa menggunakan layout bersama:
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
// @Layout membuat kelas ini sebagai layout untuk semua view di package yang sama
@Layout
public class MainLayout extends AppLayout {
public MainLayout() {
// Navbar atas
DrawerToggle toggle = new DrawerToggle();
H2 appName = new H2("Toko Online");
appName.getStyle().set("font-size", "var(--lumo-font-size-l)")
.set("margin", "0");
addToNavbar(toggle, appName);
// Sidebar navigasi
SideNav nav = new SideNav();
nav.addItem(new SideNavItem("Beranda", BerandaView.class,
new com.vaadin.flow.component.icon.VaadinIcon.HOME.create()));
nav.addItem(new SideNavItem("Produk", ProdukView.class,
new com.vaadin.flow.component.icon.VaadinIcon.PACKAGE.create()));
nav.addItem(new SideNavItem("Pesanan", PesananView.class,
new com.vaadin.flow.component.icon.VaadinIcon.CART.create()));
addToDrawer(nav);
}
}
// View dengan layout — cukup tentukan layout di @Route
@Route(value = "produk", layout = MainLayout.class)
@PageTitle("Produk | Toko Online")
public class ProdukView extends VerticalLayout {
// konten view...
}
Navigasi Programatis #
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.RouteParameters;
// Navigasi ke route tertentu
UI.getCurrent().navigate(ProdukView.class);
// Navigasi dengan parameter URL
UI.getCurrent().navigate(ProdukDetailView.class, new RouteParameters("id", "42"));
// Navigasi dengan URL string (untuk kasus kompleks)
UI.getCurrent().navigate("produk/42/edit");
Komponen UI Dasar #
Vaadin menyediakan library komponen yang kaya. Berikut komponen yang paling sering digunakan.
Input dan Form Sederhana #
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
public class KomponenDasarDemo extends VerticalLayout {
public KomponenDasarDemo() {
// TextField — input teks
TextField namaTF = new TextField("Nama Produk");
namaTF.setPlaceholder("Masukkan nama produk...");
namaTF.setRequired(true);
namaTF.setMinLength(3);
namaTF.setMaxLength(100);
namaTF.setWidth("300px");
// PasswordField
PasswordField passwordTF = new PasswordField("Password");
// NumberField — input angka
NumberField hargaNF = new NumberField("Harga");
hargaNF.setPrefixComponent(new com.vaadin.flow.component.html.Span("Rp"));
hargaNF.setMin(0);
hargaNF.setStep(1000);
// ComboBox — dropdown dengan search
ComboBox<String> kategoriCB = new ComboBox<>("Kategori");
kategoriCB.setItems("Elektronik", "Pakaian", "Makanan", "Olahraga");
kategoriCB.setPlaceholder("Pilih kategori");
// DatePicker
DatePicker tanggalDP = new DatePicker("Tanggal Kadaluarsa");
tanggalDP.setMin(java.time.LocalDate.now());
// Checkbox
Checkbox aktifCB = new Checkbox("Produk aktif");
aktifCB.setValue(true);
// Button dengan variant styling
Button simpanBtn = new Button("Simpan");
simpanBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button batalBtn = new Button("Batal");
batalBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
// Event handler — klik tombol
simpanBtn.addClickListener(event -> {
String nama = namaTF.getValue();
if (nama.isBlank()) {
// Tampilkan error inline di field
namaTF.setErrorMessage("Nama tidak boleh kosong");
namaTF.setInvalid(true);
return;
}
// Tampilkan notifikasi
Notification notif = Notification.show("Produk '" + nama + "' berhasil disimpan!");
notif.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
notif.setPosition(Notification.Position.BOTTOM_END);
});
HorizontalLayout tombolLayout = new HorizontalLayout(simpanBtn, batalBtn);
add(namaTF, hargaNF, kategoriCB, tanggalDP, aktifCB, tombolLayout);
setSpacing(true);
setPadding(true);
}
}
Dialog dan Konfirmasi #
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
public class DialogDemo {
// Dialog kustom
public static Dialog buatDialogForm(String judul) {
Dialog dialog = new Dialog();
dialog.setHeaderTitle(judul);
dialog.setWidth("500px");
dialog.setCloseOnEsc(true);
dialog.setCloseOnOutsideClick(false);
// Konten dialog
VerticalLayout konten = new VerticalLayout();
TextField namaTF = new TextField("Nama");
konten.add(namaTF);
dialog.add(konten);
// Footer dengan tombol
Button simpan = new Button("Simpan", e -> {
// proses penyimpanan
dialog.close();
});
simpan.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button tutup = new Button("Tutup", e -> dialog.close());
dialog.getFooter().add(tutup, simpan);
return dialog;
}
// ConfirmDialog — dialog konfirmasi hapus
public static void konfirmasiHapus(String namaProduk, Runnable onKonfirmasi) {
ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Hapus Produk?");
dialog.setText("Apakah kamu yakin ingin menghapus '" + namaProduk + "'? " +
"Tindakan ini tidak bisa dibatalkan.");
dialog.setCancelable(true);
dialog.setCancelText("Batal");
dialog.setConfirmText("Hapus");
dialog.setConfirmButtonTheme("error primary");
dialog.addConfirmListener(event -> onKonfirmasi.run());
dialog.open();
}
}
Grid — Tabel Data #
Grid adalah komponen paling penting di Vaadin untuk menampilkan data tabular. Ia mendukung lazy loading, sorting, filtering, dan selection.
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.renderer.NumberRenderer;
import java.text.NumberFormat;
import java.util.Locale;
public class ProdukGrid extends VerticalLayout {
private final Grid<Produk> grid;
private final ProdukService produkService;
public ProdukGrid(ProdukService produkService) {
this.produkService = produkService;
grid = new Grid<>(Produk.class, false); // false = tidak auto-generate kolom
configureGrid();
loadData();
add(grid);
setSizeFull();
}
private void configureGrid() {
grid.setSizeFull();
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_BORDERED);
// Definisi kolom secara eksplisit
grid.addColumn(Produk::getNama)
.setHeader("Nama Produk")
.setSortable(true)
.setFlexGrow(2) // kolom ini dapat 2x lebih banyak ruang
.setResizable(true);
grid.addColumn(new NumberRenderer<>(
Produk::getHarga,
NumberFormat.getCurrencyInstance(new Locale("id", "ID"))
))
.setHeader("Harga")
.setSortable(true)
.setTextAlign(com.vaadin.flow.component.grid.ColumnTextAlign.END)
.setFlexGrow(1);
grid.addColumn(Produk::getStok)
.setHeader("Stok")
.setSortable(true)
.setFlexGrow(0)
.setWidth("100px");
// Kolom dengan komponen kustom — badge status stok
grid.addColumn(new ComponentRenderer<>(produk -> {
com.vaadin.flow.component.html.Span badge = new com.vaadin.flow.component.html.Span(
produk.getStok() > 0 ? "Tersedia" : "Habis"
);
badge.getElement().getThemeList().add(
"badge " + (produk.getStok() > 0 ? "success" : "error")
);
return badge;
}))
.setHeader("Status")
.setFlexGrow(0)
.setWidth("120px");
// Kolom aksi — tombol edit dan hapus
grid.addColumn(new ComponentRenderer<>(produk -> {
Button editBtn = new Button("Edit",
new com.vaadin.flow.component.icon.Icon("lumo", "edit"));
editBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
editBtn.addClickListener(e -> bukaFormEdit(produk));
Button hapusBtn = new Button("Hapus",
new com.vaadin.flow.component.icon.Icon("lumo", "cross"));
hapusBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY,
ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ERROR);
hapusBtn.addClickListener(e ->
DialogDemo.konfirmasiHapus(produk.getNama(), () -> hapusProduk(produk))
);
return new HorizontalLayout(editBtn, hapusBtn);
}))
.setHeader("Aksi")
.setFlexGrow(0)
.setWidth("180px");
// Selection mode — single selection
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
grid.addSelectionListener(event ->
event.getFirstSelectedItem().ifPresent(this::tampilkanDetail)
);
}
private void loadData() {
grid.setItems(produkService.semuaProduk());
}
// Untuk dataset besar — gunakan lazy loading
private void loadDataLazy() {
grid.setItems(query -> {
// Hanya load data yang terlihat — efisien untuk ribuan row
int offset = query.getOffset();
int limit = query.getLimit();
return produkService.findAll(offset, limit).stream();
});
}
private void bukaFormEdit(Produk produk) {
// buka dialog atau navigasi ke halaman edit
}
private void hapusProduk(Produk produk) {
produkService.hapus(produk.getId());
loadData(); // refresh grid
Notification.show("Produk berhasil dihapus").addThemeVariants(NotificationVariant.LUMO_SUCCESS);
}
private void tampilkanDetail(Produk produk) {
// tampilkan panel detail di samping grid
}
}
Data Binding dengan Binder #
Binder adalah mekanisme Vaadin untuk menghubungkan field UI dengan objek model Java secara dua arah, lengkap dengan validasi.
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.validator.StringLengthValidator;
import com.vaadin.flow.data.validator.DoubleRangeValidator;
import com.vaadin.flow.data.converter.StringToDoubleConverter;
public class ProdukForm extends VerticalLayout {
private final Binder<Produk> binder = new Binder<>(Produk.class);
private Produk produkSaatIni;
private final TextField namaTF = new TextField("Nama Produk");
private final NumberField hargaNF = new NumberField("Harga");
private final NumberField stokNF = new NumberField("Stok");
private final ComboBox<String> kategoriCB = new ComboBox<>("Kategori");
private final Checkbox aktifCB = new Checkbox("Aktif");
private final Button simpanBtn = new Button("Simpan");
private final Button batalBtn = new Button("Batal");
private final ProdukService produkService;
public ProdukForm(ProdukService produkService) {
this.produkService = produkService;
configureFields();
configureBinder();
configureButtons();
add(namaTF, hargaNF, stokNF, kategoriCB, aktifCB,
new HorizontalLayout(simpanBtn, batalBtn));
setWidth("400px");
setPadding(true);
setSpacing(true);
}
private void configureFields() {
kategoriCB.setItems("Elektronik", "Pakaian", "Makanan", "Olahraga");
hargaNF.setPrefixComponent(new com.vaadin.flow.component.html.Span("Rp"));
hargaNF.setMin(0);
stokNF.setMin(0);
stokNF.setStep(1);
}
private void configureBinder() {
// Binding field ke property model dengan validasi
binder.forField(namaTF)
.withValidator(new StringLengthValidator(
"Nama harus 3-100 karakter", 3, 100))
.bind(Produk::getNama, Produk::setNama);
binder.forField(hargaNF)
.withValidator(nilai -> nilai != null && nilai > 0,
"Harga harus lebih dari 0")
.bind(
produk -> produk.getHarga() != null
? produk.getHarga().doubleValue() : null,
(produk, nilai) -> produk.setHarga(
nilai != null ? java.math.BigDecimal.valueOf(nilai) : null)
);
binder.forField(stokNF)
.withValidator(nilai -> nilai != null && nilai >= 0,
"Stok tidak boleh negatif")
.bind(
produk -> (double) produk.getStok(),
(produk, nilai) -> produk.setStok(nilai != null ? nilai.intValue() : 0)
);
binder.forField(kategoriCB)
.asRequired("Kategori harus dipilih")
.bind(Produk::getKategori, Produk::setKategori);
binder.forField(aktifCB)
.bind(Produk::isAktif, Produk::setAktif);
}
private void configureButtons() {
simpanBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
batalBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
simpanBtn.addClickListener(e -> simpan());
batalBtn.addClickListener(e -> batal());
}
// Isi form dengan data produk yang akan diedit
public void setProduk(Produk produk) {
this.produkSaatIni = produk;
binder.readBean(produk); // populate field dari objek
setVisible(true);
}
// Kosongkan form untuk tambah produk baru
public void setNovoProduk() {
this.produkSaatIni = new Produk();
binder.readBean(produkSaatIni);
setVisible(true);
}
private void simpan() {
try {
// Validasi semua field dan tulis nilai ke objek model
binder.writeBean(produkSaatIni);
produkService.simpan(produkSaatIni);
Notification.show("Produk berhasil disimpan")
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
setVisible(false);
// Beritahu parent view untuk refresh data
fireEvent(new ProdukDisimpanEvent(this, produkSaatIni));
} catch (ValidationException e) {
// Binder otomatis menampilkan error inline di setiap field
Notification.show("Periksa kembali data yang dimasukkan")
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
}
private void batal() {
binder.readBean(produkSaatIni); // reset ke nilai awal
setVisible(false);
}
// Custom event untuk komunikasi antar komponen
public static class ProdukDisimpanEvent extends com.vaadin.flow.component.ComponentEvent<ProdukForm> {
private final Produk produk;
public ProdukDisimpanEvent(ProdukForm source, Produk produk) {
super(source, false);
this.produk = produk;
}
public Produk getProduk() { return produk; }
}
}
View CRUD Lengkap #
Menggabungkan Grid dan Form dalam satu view yang lengkap — pola yang sangat umum di aplikasi Vaadin:
@Route(value = "produk", layout = MainLayout.class)
@PageTitle("Manajemen Produk")
public class ProdukView extends HorizontalLayout {
private final Grid<Produk> grid = new Grid<>(Produk.class, false);
private final ProdukForm form;
private final ProdukService produkService;
// TextField untuk filtering
private final TextField filterTF = new TextField();
public ProdukView(ProdukService produkService) {
this.produkService = produkService;
this.form = new ProdukForm(produkService);
setSizeFull();
configureGrid();
configureFilter();
configureForm();
// Layout: grid di kiri, form di kanan
VerticalLayout kontenGrid = new VerticalLayout(buildToolbar(), grid);
kontenGrid.setSizeFull();
add(kontenGrid, form);
setFlexGrow(2, kontenGrid); // grid dapat 2/3 lebar
setFlexGrow(1, form); // form dapat 1/3 lebar
updateList();
tutupForm();
}
private com.vaadin.flow.component.Component buildToolbar() {
filterTF.setPlaceholder("Cari produk...");
filterTF.setClearButtonVisible(true);
filterTF.setPrefixComponent(
new com.vaadin.flow.component.icon.Icon("lumo", "search"));
filterTF.setValueChangeMode(
com.vaadin.flow.data.value.ValueChangeMode.LAZY);
filterTF.addValueChangeListener(e -> updateList());
Button tambahBtn = new Button("Tambah Produk",
new com.vaadin.flow.component.icon.Icon("lumo", "plus"));
tambahBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
tambahBtn.addClickListener(e -> tambahProduk());
return new HorizontalLayout(filterTF, tambahBtn);
}
private void configureGrid() {
grid.setSizeFull();
grid.addColumn(Produk::getNama).setHeader("Nama").setSortable(true).setFlexGrow(2);
grid.addColumn(Produk::getKategori).setHeader("Kategori").setSortable(true);
grid.addColumn(Produk::getHarga).setHeader("Harga").setSortable(true);
grid.addColumn(Produk::getStok).setHeader("Stok").setSortable(true);
// Klik baris untuk edit
grid.asSingleSelect().addValueChangeListener(event -> {
if (event.getValue() != null) {
editProduk(event.getValue());
} else {
tutupForm();
}
});
}
private void configureFilter() {
// Filter sudah dikonfigurasi di buildToolbar
}
private void configureForm() {
form.setWidth("380px");
// Dengarkan event dari form
form.addListener(ProdukForm.ProdukDisimpanEvent.class, e -> {
updateList();
tutupForm();
});
}
private void updateList() {
String filter = filterTF.getValue();
if (filter.isBlank()) {
grid.setItems(produkService.semuaProduk());
} else {
grid.setItems(produkService.cariByNama(filter));
}
}
private void tambahProduk() {
grid.asSingleSelect().clear();
form.setNovoProduk();
form.setVisible(true);
}
private void editProduk(Produk produk) {
form.setProduk(produk);
form.setVisible(true);
}
private void tutupForm() {
form.setVisible(false);
grid.asSingleSelect().clear();
}
}
Integrasi Spring Boot #
Vaadin berintegrasi sangat erat dengan Spring Boot. View bisa menjadi Spring Bean dan menerima dependency injection seperti service dan repository.
import org.springframework.context.annotation.Scope;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
// @UIScope — satu instance per browser tab (mengikuti lifecycle Vaadin UI)
// @VaadinSessionScope — satu instance per browser session
// @SpringComponent setara dengan @Component tapi khusus untuk Vaadin
@Route("laporan")
@PageTitle("Laporan Penjualan")
public class LaporanView extends VerticalLayout {
// Constructor injection bekerja normal di View
private final PenjualanService penjualanService;
private final ProdukService produkService;
public LaporanView(PenjualanService penjualanService, ProdukService produkService) {
this.penjualanService = penjualanService;
this.produkService = produkService;
buildUI();
}
private void buildUI() {
add(new H1("Laporan Penjualan"));
// Gunakan service yang di-inject
var data = penjualanService.getRekapBulanIni();
var grid = new Grid<RekapPenjualan>(RekapPenjualan.class, true);
grid.setItems(data);
add(grid);
}
}
Push — Update UI dari Background Thread #
Ketika data berubah di background (dari thread lain, schedule, atau event), kamu perlu memberitahu Vaadin untuk memperbarui UI. Gunakan @Push dan UI.access():
import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.shared.communication.PushMode;
// @Push harus ada di kelas UI atau MainLayout
@Push(PushMode.AUTOMATIC)
@Route("monitor")
@PageTitle("Monitor Real-time")
public class MonitorView extends VerticalLayout {
private final com.vaadin.flow.component.html.Span statusSpan = new com.vaadin.flow.component.html.Span("Menunggu data...");
private final com.vaadin.flow.component.UI ui;
public MonitorView() {
this.ui = com.vaadin.flow.component.UI.getCurrent();
add(new H1("Status Sistem"), statusSpan);
}
// Dipanggil dari background thread (scheduler, event listener, dll)
public void perbaruiStatus(String statusBaru) {
// UI.access() memastikan update thread-safe
ui.access(() -> {
statusSpan.setText(statusBaru);
// Vaadin otomatis push perubahan ke browser karena @Push(AUTOMATIC)
});
}
}
Pengujian View #
import com.vaadin.testbench.TestBenchTestCase;
import com.vaadin.flow.component.button.testbench.ButtonElement;
import com.vaadin.flow.component.textfield.testbench.TextFieldElement;
import com.vaadin.flow.component.grid.testbench.GridElement;
// Unit test tanpa browser — gunakan Vaadin testing utilities
import com.vaadin.flow.component.UI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class ProdukFormTest {
private ProdukForm form;
private ProdukService mockService;
@BeforeEach
void setUp() {
// Buat mock UI untuk test
UI ui = new UI();
UI.setCurrent(ui);
mockService = mock(ProdukService.class);
form = new ProdukForm(mockService);
}
@Test
void form_ketikaDiisiLengkap_simpanBerhasil() {
// Arrange
Produk produkBaru = new Produk();
form.setNovoProduk();
// Akses field via API internal (untuk unit test)
// Dalam integration test gunakan TestBench dengan browser sungguhan
// Set nilai via Binder
form.namaTF.setValue("Laptop Gaming");
form.hargaNF.setValue(15000000.0);
form.stokNF.setValue(5.0);
form.kategoriCB.setValue("Elektronik");
// Act — klik tombol simpan
form.simpanBtn.click();
// Assert
verify(mockService, times(1)).simpan(any(Produk.class));
}
@Test
void form_ketikaNamaKosong_tidakMemanggil_simpan() {
form.setNovoProduk();
form.namaTF.setValue(""); // nama kosong
form.simpanBtn.click();
verify(mockService, never()).simpan(any());
assertThat(form.namaTF.isInvalid()).isTrue();
}
}
Kapan Menggunakan Vaadin dan Kapan Tidak #
GUNAKAN VAADIN JIKA:
✓ Tim adalah Java developer murni tanpa keahlian frontend
✓ Aplikasi internal, dashboard admin, atau sistem back-office
✓ Kompleksitas UI sedang — form, tabel, filter, CRUD
✓ Butuh type-safety antara UI dan logika bisnis
✓ Integrasi Spring Boot ekosistem yang dalam
✓ Prototyping cepat aplikasi enterprise
✓ Keamanan penting — tidak ada API yang terekspos ke browser
PERTIMBANGKAN ALTERNATIF JIKA:
✗ Aplikasi publik dengan UI yang sangat custom → React/Vue lebih fleksibel
✗ Mobile-first atau butuh performa tinggi di klien → SPA modern lebih cocok
✗ Tim sudah expert di JavaScript framework → manfaatkan keahlian yang ada
✗ Banyak pengguna aktif bersamaan → state di server = banyak memori
✗ Butuh offline mode atau PWA yang kompleks → Vaadin terbatas di sini
✗ Interaksi UI sangat kompleks (animasi kustom, canvas, WebGL) → JS lebih natural
flowchart TD
A{Tim Java murni\ntanpa frontend?} -- Ya --> B{Aplikasi internal\natau back-office?}
A -- Tidak --> C{Butuh UI\nsangat kustom?}
B -- Ya --> VAADIN[Vaadin]
B -- Tidak --> D{Pengguna aktif\nbanyak bersamaan?}
D -- Ya --> SPA[React / Vue / Angular]
D -- Tidak --> VAADIN
C -- Ya --> SPA
C -- Tidak --> E{Java atau\nJavaScript?}
E -- Java lebih dikuasai --> VAADIN
E -- JavaScript lebih dikuasai --> SPARingkasan #
- Vaadin menjaga state UI di server — setiap browser tab punya instance UI tersendiri, semua event dan state dikelola Java di server, bukan JavaScript di browser.
@Routemendefinisikan URL untuk View, danMainLayoutmemungkinkan berbagi tampilan umum (navbar, sidebar) lintas view tanpa duplikasi kode.- Grid adalah komponen terpenting untuk data tabular — selalu definisikan kolom secara eksplisit (
falsedi constructor) dan gunakan lazy loading untuk dataset besar.Binderadalah cara idiomatic Vaadin untuk menghubungkan field UI ke objek model — ia menangani validasi, konversi tipe, dan two-way binding sekaligus. Lebih aman dari membaca nilai field satu per satu.- Constructor injection Spring Boot bekerja langsung di View Vaadin — cukup deklarasikan constructor dengan parameter service, Spring otomatis menginjeksikannya.
UI.access()wajib digunakan ketika memperbarui UI dari background thread — tanpa ini perubahan tidak thread-safe dan bisa menyebabkan race condition. Pasangkan dengan@Pushdi layout.- View CRUD dengan Grid + Form adalah pola paling umum — Grid di kiri untuk list data, Form di kanan untuk edit, keduanya berkomunikasi melalui custom event atau callback.
- Vaadin paling cocok untuk aplikasi internal, back-office, dan tim Java murni yang ingin produktivitas tinggi tanpa mempelajari ekosistem frontend modern.