Skip to content

How to Implement Two-Way Binding in Vaadin with Kotlin Signals

Purpose

I wanted a clean two-way binding for Vaadin forms. I did not want to write a manual listener for every field and then re-apply state changes back into the UI.

Environment

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

What happened?

My first version used listeners everywhere. It worked, but it was easy to create loops and inconsistent state.

src/main/kotlin/com/example/ManualFormView.kt
package com.example
import com.vaadin.flow.component.checkbox.Checkbox
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.router.Route
@Route("manual")
class ManualFormView : VerticalLayout() {
init {
var username = ""
var agreed = false
val field = TextField("Username")
val agreeBox = Checkbox("Agree")
field.addValueChangeListener { event ->
username = event.value
}
agreeBox.addValueChangeListener { event ->
agreed = event.value
}
add(field, agreeBox)
}
}

I tried to add more fields and validation, and the listeners started to duplicate state updates. I also had to remember where the real source of truth lived.

How to solve it?

I created a tiny MutableSignal class and bound it directly to components. The field writes to the signal, and the signal writes back to the field.

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

Then I added bindings for TextField and Checkbox.

src/main/kotlin/com/example/Bindings.kt
package com.example
import com.vaadin.flow.component.checkbox.Checkbox
import com.vaadin.flow.component.textfield.TextField
fun TextField.value(signal: MutableSignal<String>) {
signal.subscribe { this.value = it }
this.addValueChangeListener { event ->
signal.value = event.value
}
}
fun Checkbox.checked(signal: MutableSignal<Boolean>) {
signal.subscribe { this.value = it }
this.addValueChangeListener { event ->
signal.value = event.value
}
}

Now the form view is short and consistent. The signal is the only source of truth.

src/main/kotlin/com/example/SignalFormView.kt
package com.example
import com.vaadin.flow.component.checkbox.Checkbox
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.router.Route
@Route("signals")
class SignalFormView : VerticalLayout() {
init {
val username = MutableSignal("")
val agreed = MutableSignal(false)
val field = TextField("Username")
val agreeBox = Checkbox("Agree")
field.value(username)
agreeBox.checked(agreed)
add(field, agreeBox)
}
}

I tested a minimal case first to avoid loops. This tiny example is a good starting point.

src/main/kotlin/com/example/MinimalTwoWay.kt
package com.example
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("mini")
class MinimalTwoWay : VerticalLayout() {
init {
val name = MutableSignal("Ada")
val field = TextField("Name")
field.value(name)
add(field)
}
}

Run the app:

Terminal window
./gradlew bootRun

Expected output:

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

If you are new to this, start from the minimal example. Once it works, add validation by mapping a signal and binding it to enabled or visible properties.

The reason

I think the key reason two-way binding helps is that it removes duplicated state. The UI and the state stop fighting each other. It feels like a single pipe instead of two different clocks you must keep in sync.

Summary

In this post, I implemented two-way binding in Vaadin using Kotlin signals. The key point is using a MutableSignal as the single source of truth for form values.

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