Skip to content

How to Build Declarative Vaadin UI with Kotlin Reactive Signals

Purpose

I wanted a declarative UI style in Vaadin, not a pile of manual updates. I like the React pattern where UI reflects state, so I built the same idea with a Kotlin DSL plus a tiny reactive signal layer.

Environment

  • Java 21
  • Kotlin 1.9.x
  • Vaadin 24.x (Flow)
  • Gradle 8.x

What happened?

My first version was imperative and messy. I had to push values into components in every listener.

src/main/kotlin/com/example/MainView.kt
package com.example
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("")
class MainView : VerticalLayout() {
init {
val label = Span("Count: 0")
val button = Button("Add")
var count = 0
button.addClickListener {
count++
label.text = "Count: $count"
label.isVisible = count > 5
}
add(label, button)
}
}

This works, but I keep duplicating state and UI updates. I also can’t read the UI hierarchy cleanly because the structure and the state logic are mixed together.

How to solve it?

I kept the UI structure in a Kotlin DSL style, and I moved state changes into a Signal layer. The main idea is simple: the UI reads signals and never manually sets values.

First, I wrote a tiny Signal type. It is not a full library, but it shows the pattern clearly.

src/main/kotlin/com/example/Signal.kt
package com.example
class Signal<T>(initial: T) {
private var valueInternal: T = initial
private val listeners = mutableListOf<(T) -> Unit>()
val value: T
get() = valueInternal
fun subscribe(listener: (T) -> Unit) {
listeners.add(listener)
listener(valueInternal)
}
fun update(newValue: T) {
valueInternal = newValue
listeners.forEach { it(newValue) }
}
}
fun <T, R> Signal<T>.map(transform: (T) -> R): Signal<R> {
val mapped = Signal(transform(this.value))
this.subscribe { mapped.update(transform(it)) }
return mapped
}

Then I added a few binding helpers for Vaadin components. This keeps UI code clean.

src/main/kotlin/com/example/Bindings.kt
package com.example
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.HasText
import com.vaadin.flow.component.HasValue
fun HasText.text(signal: Signal<String>) {
signal.subscribe { this.text = it }
}
fun Component.visible(signal: Signal<Boolean>) {
signal.subscribe { this.isVisible = it }
}
fun <T> HasValue<*, T>.value(signal: Signal<T>) {
signal.subscribe { this.value = it }
}

Now the UI becomes declarative. The structure reads like a tree and the state lives in signals.

src/main/kotlin/com/example/MainView.kt
package com.example
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("")
class MainView : VerticalLayout() {
init {
val count = Signal(0)
val labelText = count.map { "Count: $it" }
val showBadge = count.map { it > 5 }
val label = Span()
label.text(labelText)
label.visible(showBadge)
val button = Button("Add")
button.addClickListener { count.update(count.value + 1) }
add(label, button)
}
}

I tried a smaller example first to confirm the binding works, before I moved to the counter.

src/main/kotlin/com/example/HelloView.kt
package com.example
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("hello")
class HelloView : VerticalLayout() {
init {
val name = Signal("Vaadin")
val label = Span()
label.text(name.map { "Hello, $it" })
add(label)
}
}

I run the app like this:

Terminal window
./gradlew bootRun

And I see the normal output:

Terminal window
> Task :bootRun
Started Application in 3.2 seconds
Vaadin application is running on http://localhost:8080

You can treat a signal like a small notebook that always holds the latest value. The UI just reads the notebook and never tries to rewrite the notes manually.

The reason

I think the key reason this feels better is the separation between structure and state changes. The DSL describes the UI tree, and the signal layer is a thin reactive pipe. That keeps the mental model simple for beginners.

Summary

In this post, I built a declarative Vaadin UI using a Kotlin DSL and reactive signals. The key point is binding component properties to state instead of manually updating UI.

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