Hi everyone!
I’d like to share a small experiment of mine: Flowik, a Kotlin library that brings
MobX-style reactive state to Vaadin Flow (and some other UI toolkits on JVM).
I built it purely out of academic interest. I wanted to see how far the MobX model can go on the
JVM, and how it compares to the new Vaadin Signals. It’s not a product or a competitor to anything,
just an exploration that I think the community might find interesting.
MobX in one sentence
Anything that can be derived from the application state should be derived. Automatically.
MobX is a popular state-management library from the JavaScript
world, built on three ideas:
- Observable state: mutable values that track who reads them.
- Derivations:
computed { }for cached values andautoRun { }for side effects such as UI
updates. They re-run when, and only when, something they read changes. - Actions:
action { }blocks that batch writes, so each derivation runs once at the end.
There are no listeners, no .subscribe() calls and no dependency graph to maintain by hand. Reading
an observable inside a derivation subscribes you to it.
The demo
The demo is a port of the master/detail example from the Vaadin docs’ “Compose with Components”
page: an employee directory with a list, search, a tabbed detail pane, add/edit dialogs and two more
pages (Teams and Locations) that group the same people. Everything sits under one router layout,
demo.vaadin.employees.MainLayout.
What it shows:
- One store, stateless components.
EmployeeStore(@UIScope) is the only place state changes.
The list, the detail pane and the editor dialog never reference the store or each other. Each gets
the slices of state it shows plus a callback for every event it raises: state goes down, events
come up. - Fine-grained entities. Each employee is an
ObservableEntity<Employee>with one atom per
property. Edit someone’s phone number and only the labels that show the phone re-render, on every
page. - Derived pages for free. Teams and Locations are a single
GroupsViewfed by
store.groupedBy { ... }. Change someone’s title on the Employees page and the Teams page regroups,
with no code connecting the two. - Edit buffers. The editor works on a
ViewModel<Employee>. Save isenabled { form.isDirty },
and Cancel needs no code because nothing reaches the model untilsubmit().
Some code
The store is a plain class:
@SpringComponent
@UIScope
class EmployeeStore : Store {
val employees = observables(*sampleEmployees().toTypedArray())
var search by observable("", name = "search")
var selected by observableRef<ObservableEntity<Employee>?>(null, name = "selected")
val visibleEmployees by employees.filter { employee ->
val query = search.trim().lowercase()
query.isEmpty()
|| employee.fullName.lowercase().contains(query)
|| employee[Employee::title].lowercase().contains(query)
}
fun select(employee: ObservableEntity<Employee>?) = action { selected = employee }
// ...
}
The route view wires the store to children that know nothing about it:
add(
register(EmployeeMaster(
employees = store::visibleEmployees, // a computed slice
query = store::search,
onQueryChange = store::updateSearch,
selected = store::selected,
onSelect = store::select,
onAddNew = ::addNew,
)),
Div().apply {
addClassName("detail-pane")
childSwitch(store::selected) { employee ->
if (employee == null) NoSelection() else detail(employee)
}
},
)
Bindings are just lambdas. Whatever they read, they follow:
H2().apply { text { employee.fullName } }
Paragraph("No one matches the search.").apply {
visible { employees.get().isEmpty() }
}
Button("Save") { onSave(); close() }.apply {
enabled { form.isDirty }
}
Derived helpers are written once as extensions, and each caller is subscribed to exactly the
properties they touch (here firstName and lastName):
val ObservableEntity<Employee>.fullName: String
get() = "${this[Employee::firstName]} ${this[Employee::lastName]}".trim()
And the Teams and Locations pages are just two groupings of the same store:
class TeamsView(store: EmployeeStore) : GroupsView(
title = "Teams",
groups = store.groupedBy { it[Employee::title] },
onOpen = store.openOnEmployeesPage(),
)
class LocationsView(store: EmployeeStore) : GroupsView(
title = "Locations",
groups = store.groupedBy { it.nested(Employee::address)[Address::city] },
onOpen = store.openOnEmployeesPage(),
)
How it compares to Vaadin Signals
Signals and Flowik share the core idea (automatic dependency tracking) but make different
trade-offs. Signals are Vaadin-native, thread-safe, and can be shared across sessions and cluster
nodes, which Flowik doesn’t try to do. Flowik focuses on modelling the state of large screens:
- Components take
Supplier<T>, not a framework type. Every observable andcomputedis a
Supplier, and so is any lambda that reads them, so a caller can pass a store property, an inline
derivation (isSelected = { selected.get() === employee }) or a constant, and the component can’t
tell the difference.get()works everywhere, including click handlers, so there’s no
get()/peek()choice to make. - Fine-grained objects out of the box.
observable(obj)splits a data class into one atom per
property. With Signals, a record in aListSignalis usually oneValueSignal, and any field
update re-runs every binding that reads that record. - Cached derivations by default.
computed { }caches until a dependency changes, and
computedStructnotifies only on a real change. - Actions batch writes, so derivations run once at the end of the outermost action.
- Lifetimes follow the composition tree.
register(child),items { }andchildSwitch { }
dispose of the children they drop. - Plain JUnit testing. Bindings run without a
UIor attaching, so you can construct a component,
push state into it and assert, with no browser harness.
In short: Signals let a component follow state, and Flowik tries to let you design the state
(entities, derived slices, edit buffers) so it can be handed to components that don’t know where it
comes from. The README has a full
side-by-side comparison.
Try it
Source, docs and the demo: GitHub - jreznot/flowik: Robust MobX for mutable UI on JVM: Swing, Vaadin, IntelliJ Platform · GitHub
The demo is a Spring Boot app in flowik-vaadin-demo. Releases are on JitPack
(com.github.jreznot.flowik:flowik-vaadin).
I’d love to hear what you think, especially from anyone who has tried Signals on a larger UI. Which
patterns worked for you, and where did it get awkward?
P.S. I promised Marcus Hellberg I’d post it at some point during Spring I/O 2026 in Barcelona.
