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.
package com.example
import com.vaadin.flow.component.button.Buttonimport com.vaadin.flow.component.html.Spanimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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.
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.
package com.example
import com.vaadin.flow.component.Componentimport com.vaadin.flow.component.HasTextimport 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.
package com.example
import com.vaadin.flow.component.button.Buttonimport com.vaadin.flow.component.html.Spanimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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.
package com.example
import com.vaadin.flow.component.html.Spanimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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:
./gradlew bootRunAnd I see the normal output:
> Task :bootRunStarted Application in 3.2 secondsVaadin application is running on http://localhost:8080You 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:
- π¨βπ» Vaadin Flow Overview
- π¨βπ» Vaadin Components
- π¨βπ» Kotlin Type-Safe Builders
- π¨βπ» Kotlin Flow
Oh, and if you found these resources useful, donβt forget to support me by starring the repo on GitHub!
Comments