Why I wrote this
I have spent most of my career writing UIs on the JVM. For nine years I built ERP and OLTP systems with Vaadin and Swing: order entry, warehouse screens, master/detail forms with dozens of fields that depend on each other. Today I work at JetBrains on the IntelliJ Platform team, and the IDE is still, to a large extent, a Swing application.
Along the way I also wrote frontends in GWT and React, and I fell in love with functional programming. That mix left me with an itch. On the web, state management is a solved and widely discussed problem. On the JVM desktop and in server-side UI frameworks, most of us still wire things together by hand.
This post introduces Flowik, a small Kotlin library that brings MobX-style reactive state to Swing, Vaadin and the IntelliJ Platform.
The problem: listeners all the way down
If you have built a non-trivial Swing or Vaadin form, you know this code:
quantityField.addChangeListener { recalcTotal(); updateSaveButton() }
priceField.addChangeListener { recalcTotal(); updateSaveButton() }
discountCheckBox.addActionListener { recalcTotal(); updateDiscountRow(); updateSaveButton() }
Every derived thing on the screen (a total, an enabled flag, a label) needs someone to remember every source it depends on. Add a field and you hunt down every listener that should now also call recalcTotal(). Forget one and the UI shows stale data. Remove a panel and forget to unregister, and you leak it.
The dependency graph is real, but it lives only in your head and is scattered across addXxxListener calls. In an ERP screen with 40 fields it is the main source of bugs. In an IDE with hundreds of settings panels and tool windows, the same pattern repeats at scale.
React taught a generation of developers a different model: describe the UI as a function of state and let the framework keep it in sync. MobX took that one step further for state itself.
MobX in one paragraph
MobX sums itself up in one line:
Anything that can be derived from the application state should be derived. Automatically.
You mark some values as observable. You write derivations, which are plain functions that read them. MobX records which observables each derivation actually read while it ran, and re-runs it when, and only when, one of those changes. There is no subscription code and no dependency list to maintain. Reading is subscribing.
What appeals to me as a fan of functional programming is that your UI becomes close to a pure function of state, while the state itself stays ordinary mutable objects. You get most of the benefit of the functional model without forcing immutable data structures onto a 15-year-old domain model.
The second thing I realised is that none of this is JavaScript-specific. The tracking mechanism needs only a place to record "who is reading right now" and a way to batch writes. The project that convinced me was RomanPozdeev/MobX, a compact Kotlin implementation of observables, computed, autorun and action with property delegates. Seeing the whole idea fit into a small, readable Kotlin library showed me that MobX is very portable, and that the JVM UI world could have it too. Flowik grew from that starting point, with a focus on the toolkits I work with every day.
Meet Flowik
A Flowik app has two parts. The store is a plain Kotlin class that holds observables and computed values. The view only reads from the store. It never registers a listener.
import flowik.core.*
class CounterStore {
val count = observable(0)
val doubled = computed { count.value * 2 }
fun inc() = action { count.value += 1 }
}
The store knows nothing about Swing or Vaadin. The same class drives both UIs below.
Swing
import flowik.core.*
import flowik.layout.uiFrame
import flowik.swing.*
fun main() {
val store = CounterStore()
val bindings = Bindings()
uiFrame("Counter", width = 240, height = 120, bindings = bindings) {
center {
vbox(gap = 4) {
Label { "Count: ${store.count.value} (×2 = ${store.doubled.value})" }
Button("Increment") { store.inc() }
}
}
}.disposeOnClose(bindings)
}
The Label lambda reads count and doubled, so it re-renders whenever either changes. That is the whole binding.
Vaadin Flow
@Route("counter")
class CounterView : VerticalLayout(), Bindings by Bindings() {
private val store = CounterStore()
init {
val label = Span()
label.text { "Count: ${store.count.value} (×2 = ${store.doubled.value})" }
add(label, Button("Increment") { store.inc() })
disposeOnDetach()
}
}
You will notice Bindings in every example. It is the owner of the subscriptions a window, view or panel creates, so they can be released together. Flowik makes that lifetime explicit instead of guessing from attach and detach events, which in Vaadin also fire when a component is merely moved.
Building blocks
The core module, flowik-core, has no UI dependency. Its vocabulary will be familiar to anyone who has used MobX:
| Block | Flowik API | What it does |
|---|---|---|
| Observable state |
observable, observables, observableSet
|
Mutable values that track who reads them |
| Cached derivation | computed { } |
A value derived from others, recomputed only when an input changes |
| Side effect |
autoRun { }, reaction, whenThen
|
Code that re-runs when what it read changes, usually a UI update |
| Batch | action { } |
Groups writes so derivations fire once, at the end |
A short tour:
val name = observable("Alice")
val greeting = computed { "Hello, ${name.value}" }
autoRun { println(greeting.value) } // prints "Hello, Alice"
action {
name.value = "Bob"
name.value = "Carol"
} // prints "Hello, Carol" once
Two things are worth noticing. The autoRun never says which values it depends on. And the action produced one update, not two, which matters a lot when a single user gesture touches ten fields of an order.
Kotlin helps make this pleasant. Observables work as property delegates (var name by observable("")), bindings accept property references (TextField(store::name)), and observable(person) turns each property of a data class into its own reactive atom, so a label showing only the name does not re-render when the age changes.
Where it pays off: screens with many parts
A counter proves nothing. The real test is whether you can split a large screen into small parts that know nothing about each other, and still have them stay in sync. That is exactly where my ERP screens used to rot.
The Vaadin demo contains a port of the master/detail page from the Vaadin docs' "Compose with Components" example: an employee list on the left, the selected person on the right, plus editing, adding and deleting. Its shape is:
EmployeeStore the only place state changes
└── EmployeesView the one stateful component: wires store → children
├── EmployeeMaster search + list, owns no state
│ └── EmployeeRow × N
└── EmployeeDetail tabs, manager card, Edit / Delete, owns no state
One store, state down, events up
The store is plain Kotlin. Its setters are private, so nothing writes state past it. The filtered list is a derivation, not something anyone updates:
@SpringComponent
@UIScope
class EmployeeStore : Store {
val employees = observables(*sampleEmployees().toTypedArray())
var search by observable("")
var selected by observableRef<ObservableEntity<Employee>?>(null)
val visibleEmployees by employees.filter { employee ->
val query = search.trim().lowercase()
query.isEmpty() || employee.fullName.lowercase().contains(query)
}
fun select(employee: ObservableEntity<Employee>?) = action { selected = employee }
fun updateSearch(query: String) = action { search = query }
}
The route view hands each child the slices it shows and a callback for each event it raises:
class EmployeesView(private val store: EmployeeStore) : Div(), Bindings by Bindings() {
init {
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 {
childSwitch(store::selected) { employee ->
if (employee == null) NoSelection() else detail(employee)
}
},
)
disposeOnDetach()
}
}
EmployeeMaster and EmployeeDetail never reference the store or each other. Both follow store.selected, so clicking a row, adding a person and deleting one all move the detail pane the same way. There is no code that keeps the two panes in sync, because there is nothing to sync.
Why the parts stay small
-
Inputs are
Supplier<T>, not framework types.EmployeeMastertakes aSupplier<List<ObservableEntity<Employee>>>. Every observable andcomputedis aSupplier, and so is any lambda that reads them. A caller can pass a store property, derive a value inline (isSelected = { selected.get() === employee }) or pass a constant, and the child cannot tell the difference. Tracking flows straight through the lambda. -
Entities are live, and fine-grained. A row, a person card and the detail tabs all receive the same
ObservableEntity<Employee>. Each field reads only the property it shows, so changing someone's phone number re-renders only the places that show the phone number, on every page that shows it. -
Editing is a transaction. The editor dialog gets a buffered
createViewModel(employee). Nothing reaches the model untilsubmit(), so Cancel needs no code, and the same dialog creates a new person from a draft that joins the list only on Save. -
Lifetimes follow the composition tree. Every part is
Bindings by Bindings().register(child),items { }andchildSwitch { }dispose what they drop. Whoever creates a component owns it.
Derived state is shared the same way. The Teams and Locations pages are one generic GroupsView fed with store.groupedBy { ... }. Change someone's title on the Employees page and the Teams page regroups, with no code connecting the two pages.
Testable without a browser
Because a part depends only on suppliers and callbacks, and bindings run without a Vaadin UI, it can be tested in plain JUnit, with no store and no browser harness:
val people = observables(*sampleEmployees().take(3).toTypedArray())
val selected = observableRef<ObservableEntity<Employee>?>(null)
val master = EmployeeMaster(
employees = people::items,
query = { "" },
onQueryChange = {},
selected = selected,
onSelect = { println("select ${it.fullName}") },
onAddNew = {},
)
master.rows()[1].click() // only reported: the master does not select by itself
action { selected.value = people.items[1] } // its owner does, and the row follows
assertTrue(master.rows()[1].hasClass("selected"))
This is the part I missed most in nine years of enterprise UIs. Flowik lets you design the state (fine-grained entities, derived slices, edit buffers) and hand it to components that do not know where it comes from. The more parts a screen has, the more that matters. The whole demo is about 900 lines of Kotlin across ten files, and the source is on GitHub.
Status and how to try it
Flowik is experimental. The API is still moving between releases, and I would not put it under a production system yet. What it does have: bindings for Swing, Vaadin Flow and the IntelliJ Kotlin UI DSL, tests, and several demos. These include a Todo app, an Obsidian-like notes UI in Swing, and an employees master/detail screen in Vaadin.
Releases are published through JitPack:
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.jreznot.flowik:flowik-swing:v0.5.0")
// or flowik-vaadin, flowik-intellij
}
The bindings use Kotlin context parameters, so modules that depend on them compile with -XXLanguage:+ContextParameters.
If you build UIs on the JVM, I would love to hear how you manage state today. Try a demo, open an issue, or tell me in the comments what you would want covered next.
Source: github.com/jreznot/flowik


Top comments (0)