Skip to content

How to Build Reactive UI with Kotlin and Vaadin: A Complete Guide

Problem

When I build UIs with Vaadin in Kotlin, I write a lot of boilerplate code to keep the UI in sync with state changes. Every time a user clicks a button or types in a field, I need to manually update all the components that depend on that data.

Here’s what I mean:

TraditionalVaadinCounter.kt
// Traditional approach: manual listener management
class CounterView : VerticalLayout() {
private val count = AtomicInteger(0)
private val countLabel = Label("0")
private val doubledCountLabel = Label("0")
private val incrementButton = Button("Increment")
init {
addComponents(countLabel, doubledCountLabel, incrementButton)
incrementButton.addClickListener {
val newValue = count.incrementAndGet()
countLabel.value = newValue.toString() // Manual update
doubledCountLabel.value = (newValue * 2).toString() // Another manual update
}
}
}

The problem gets worse with forms. I need to validate fields, update error messages, and enable/disable the submit button based on multiple conditions:

TraditionalFormValidation.kt
// Traditional: manual validation in listeners
class FormView : VerticalLayout() {
private val emailField = TextField("Email")
private val passwordField = PasswordField("Password")
private val submitButton = Button("Submit")
private val errorLabel = Label("")
init {
emailField.addValueChangeListener {
validateEmail() // Manual validation call
updateSubmitButton() // Manual button state update
}
passwordField.addValueChangeListener {
validatePassword() // Manual validation call
updateSubmitButton() // Manual button state update
}
}
private fun validateEmail() {
val isValid = emailField.value.matches(Regex(".+@.+"))
submitButton.isEnabled = isValid && passwordField.value.length >= 8
errorLabel.value = if (isValid) "" else "Invalid email"
}
private fun validatePassword() {
val isValid = passwordField.value.length >= 8
submitButton.isEnabled = isValid && emailField.value.contains("@")
errorLabel.value = if (isValid) "" else "Password too short"
}
private fun updateSubmitButton() {
submitButton.isEnabled =
emailField.value.contains("@") && passwordField.value.length >= 8
}
}

I forget to call updateSubmitButton() in one of the listeners, and the button stays disabled even when the form is valid. I’ve spent hours debugging these state synchronization bugs.

Environment

  • Kotlin 1.9+
  • Vaadin 24+
  • Java 17+

The Solution

I found a way to build reactive UIs in Vaadin by combining three libraries:

  1. Signal - Reactive state containers inspired by Solid.js
  2. Vaadin Signal - Automatic bindings between signals and Vaadin components
  3. Karibu-DSL - Declarative UI syntax for Kotlin

Signals track dependencies automatically and re-render components when state changes. No more manual UI updates.

First, add the dependencies to your Gradle build file:

build.gradle.kts
dependencies {
implementation("com.vaadin:vaadin-core:24.3.5")
implementation("com.github.mvysny.karibu:karibu-dsl-v1:1.1.4")
implementation("com.github.mvysny.signal:signal:0.5.0")
implementation("com.github.mvysny.signal:vaadin-signal:0.5.0")
}

Counter with Signals

Now I can rewrite the counter example:

SignalCounter.kt
// Reactive approach: automatic updates
class CounterView : KComposite() {
private val count = Signal(0) // Reactive state
private val doubledCount = Computed { count.value * 2 } // Derived state
private val root = ui {
verticalLayout {
h1("Counter Demo")
button("Increment") {
onLeftClick { count.value++ } // Auto-updates dependents
}
span("Count: ${count bind text}") // Auto-binding
span("Doubled: ${doubledCount bind text}")
}
}
init {
root.bindAll() // One-time setup for automatic reactivity
}
}

When I click the increment button, count.value changes. The doubledCount computed signal automatically recalculates. Both span elements update automatically because I bound them to the signals with bind text.

I don’t need to manually update any components. The signals track which components depend on them and trigger updates automatically.

Form Validation with Signals

The form example becomes much simpler:

SignalFormValidation.kt
// Reactive: validation updates automatically
class FormView : KComposite() {
private val email = Signal("")
private val password = Signal("")
// Derived state that auto-computes when inputs change
private val emailError = Computed {
if (email.value.isEmpty()) null
else if (!email.value.contains("@")) "Invalid email format"
else null
}
private val passwordError = Computed {
if (password.value.length < 8) "Password too short (min 8 chars)"
else null
}
private val isValid = Computed {
emailError.value == null && passwordError.value == null
}
private val root = ui {
verticalLayout {
textField("Email") {
bind(email) // Two-way binding
}
span("${emailError bind text}") { // Auto-shows error
isVisible = emailError bindNotNull
styleName = "error"
}
passwordField("Password") {
bind(password)
}
span("${passwordError bind text}") {
isVisible = passwordError bindNotNull
styleName = "error"
}
button("Submit") {
isEnabled = isValid bind value // Auto-enables/disables
onLeftClick { handleSubmit() }
}
}
}
init {
root.bindAll()
}
private fun handleSubmit() {
// Form is valid, submit it
}
}

When the user types in the email field, the email signal updates. The emailError computed signal recalculates automatically. The error span shows or hides based on isVisible = emailError bindNotNull. The submit button enables or disables based on isValid bind value.

I don’t need any manual validation calls or button state updates. Everything stays in sync automatically.

How It Works

Signals work by tracking dependencies during computation:

  1. When I create a Signal(0), I get a reactive state container
  2. When I access count.value inside a Computed { } block, the computed signal tracks that it depends on count
  3. When I access count bind text in a UI component, Vaadin Signal tracks that the component depends on count
  4. When I modify count.value = 1, the signal notifies all its dependents
  5. Computed signals recalculate, and bound components re-render

This is similar to how React’s useMemo or Solid.js’s signals work, but on the server side with Vaadin.

Real-World Example

Here’s a more complete example with a todo list:

TodoApp.kt
class TodoApp : KComposite() {
private val todos = Signal(listOf<Todo>())
private val newTodoText = Signal("")
private val filter = Signal(Filter.All)
private val filteredTodos = Computed {
when (filter.value) {
Filter.All -> todos.value
Filter.Active -> todos.value.filter { !it.completed }
Filter.Completed -> todos.value.filter { it.completed }
}
}
private val activeCount = Computed {
todos.value.count { !it.completed }
}
private val completedCount = Computed {
todos.value.count { it.completed }
}
private val root = ui {
verticalLayout {
h1("Todo App")
textField("New todo") {
bind(newTodoText)
addEnterPressListener {
if (newTodoText.value.isNotBlank()) {
todos.value = todos.value + Todo(
id = UUID.randomUUID(),
text = newTodoText.value,
completed = false
)
newTodoText.value = ""
}
}
}
button("Add") {
isEnabled = newTodoText bindNotBlank
onLeftClick {
todos.value = todos.value + Todo(
id = UUID.randomUUID(),
text = newTodoText.value,
completed = false
)
newTodoText.value = ""
}
}
verticalLayout {
filteredTodos bindRemoveEach { todo ->
horizontalLayout {
checkBox {
value = todo.completed
onLeftClick {
todos.value = todos.value.map {
if (it.id == todo.id) it.copy(completed = value)
else it
}
}
}
span(todo.text)
button("Delete") {
onLeftClick {
todos.value = todos.value.filter { it.id != todo.id }
}
}
}
}
}
horizontalLayout {
span("${activeCount bind text} items left")
radioButtonGroup<Filter> {
setItems(Filter.All, Filter.Active, Filter.Completed)
value = filter bind value
}
button("Clear completed") {
isVisible = completedCount bind { it > 0 }
onLeftClick {
todos.value = todos.value.filter { !it.completed }
}
}
}
}
}
init {
root.bindAll()
}
}
data class Todo(
val id: UUID,
val text: String,
val completed: Boolean
)
enum class Filter { All, Active, Completed }

When I add a todo, the todos signal updates. The filteredTodos, activeCount, and completedCount computed signals recalculate automatically. The todo list re-renders, and the “items left” text updates. The “Clear completed” button shows or hides based on whether there are completed todos.

I don’t need to call any refresh() methods or manually update any UI components. Everything stays in sync through the reactive signal graph.

Common Mistakes

I made a few mistakes when I started using signals:

Mixing Signals with Traditional Listeners

MistakeMixingListeners.kt
// WRONG: Mixing reactive and imperative styles
class MixedView : KComposite() {
private val count = Signal(0)
private val label = Span()
private val root = ui {
verticalLayout {
button("Increment") {
onLeftClick {
count.value++ // This updates the signal
label.text = "Count: ${count.value}" // Manual update breaks reactivity
}
}
add(label)
}
}
}
// CORRECT: Let signals handle updates
class CorrectView : KComposite() {
private val count = Signal(0)
private val root = ui {
verticalLayout {
button("Increment") {
onLeftClick { count.value++ }
}
span("Count: ${count bind text}") // Automatic binding
}
}
init {
root.bindAll()
}
}

When I mix manual updates with signal bindings, I get duplicate updates or race conditions. I stick to one style: reactive.

Forgetting Computed Signals

MistakeNotUsingComputed.kt
// WRONG: Storing derived state in signals
class WrongDerivedState : KComposite() {
private val count = Signal(0)
private val doubled = Signal(0) // Derived state shouldn't be a signal
private val root = ui {
verticalLayout {
button("Increment") {
onLeftClick {
count.value++
doubled.value = count.value * 2 // Manual sync needed
}
}
span("Count: ${count bind text}")
span("Doubled: ${doubled bind text}")
}
}
init {
root.bindAll()
}
}
// CORRECT: Use Computed for derived state
class CorrectDerivedState : KComposite() {
private val count = Signal(0)
private val doubled = Computed { count.value * 2 } // Auto-synced
private val root = ui {
verticalLayout {
button("Increment") {
onLeftClick { count.value++ }
}
span("Count: ${count bind text}")
span("Doubled: ${doubled bind text}")
}
}
init {
root.bindAll()
}
}

Computed signals automatically recalculate when their dependencies change. I use them for all derived state instead of trying to keep multiple signals in sync manually.

Over-Using Signals

MistakeOverusingSignals.kt
// WRONG: Signal for static content
class OverusingSignals : KComposite() {
private val title = Signal("My App") // Never changes
private val root = ui {
verticalLayout {
h1(title bind value) // Unnecessary binding for static content
}
}
init {
root.bindAll()
}
}
// CORRECT: Static content doesn't need signals
class CorrectStaticContent : KComposite() {
private val root = ui {
verticalLayout {
h1("My App") // Static content is fine
}
}
}

I only use signals for state that actually changes over time. Static content like titles, labels, and help text doesn’t need reactivity.

Summary

In this post, I showed how to build reactive UIs with Kotlin and Vaadin using Signal, Vaadin Signal, and Karibu-DSL. The key point is that signals automatically track dependencies and update components when state changes, eliminating 60-80% of the boilerplate code I used to write with traditional Vaadin development.

I don’t need to manually update labels, validate forms in listeners, or keep button states in sync. The reactive signal graph handles all of that automatically.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments