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.
package com.example
import com.vaadin.flow.component.checkbox.Checkboximport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport com.vaadin.flow.component.textfield.TextFieldimport 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.
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.
package com.example
import com.vaadin.flow.component.checkbox.Checkboximport 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.
package com.example
import com.vaadin.flow.component.checkbox.Checkboximport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport com.vaadin.flow.component.textfield.TextFieldimport 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.
package com.example
import com.vaadin.flow.component.textfield.TextFieldimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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:
./gradlew bootRunExpected output:
> Task :bootRunStarted Application in 3.1 secondsVaadin application is running on http://localhost:8080If 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:
- π¨βπ» Vaadin Text Field
- π¨βπ» Vaadin Checkbox
- π¨βπ» Vaadin Binder
- π¨βπ» Kotlin Delegated Properties
Oh, and if you found these resources useful, donβt forget to support me by starring the repo on GitHub!
Comments