0.19 is a breaking release. The persistence layer moved from
vok-orm(jdbi-orm) to ktorm + ktorm-vaadin. The published REST wire format also simplified to eq-only filters with?offset/?limit/?sort=col:asc,col2:desc. If you're upgrading from 0.18.x, check the per-module READMEs for the new shapes and see vok-example-crud for an end-to-end example.
Vaadin-on-Kotlin is a web-application framework for database-backed apps in Kotlin. Documentation lives at www.vaadinonkotlin.eu. The Accessing SQL Databases guide and the per-module READMEs are current; the tutorial and NoSQL/REST data sources page still describe pre-0.19 idioms and are awaiting a rewrite — refer to vok-example-crud for current patterns.
VoK does not enforce MVC, dependency injection, or service-oriented architecture. It uses neither Spring nor JavaEE
by default. The view layer leverages Vaadin's component-oriented programming model. The
persistence layer uses ktorm — typed-SQL DSL, entity sequences, no XML — wrapped by
ktorm-vaadin for EntityDataProvider, filter components, and the
ActiveEntity runtime. Of course you can swap in JPA/Hibernate or a NoSQL store instead by depending only on
vok-framework.
-
Install JDK 21 (required by Vaadin 25) and a git client if you don't already have them.
-
Clone the example app and run it:
git clone https://github.com/mvysny/vaadin-on-kotlin cd vaadin-on-kotlin ./gradlew vok-example-crud:run -
Visit http://localhost:8080. The CRUD demo backs a Vaadin Grid by a ktorm-vaadin
EntityDataProvider, with filter components attached to the grid header. -
See vok-example-crud for the demo source. For the canonical end-to-end ktorm-vaadin sample, see beverage-buddy-ktorm.
- vok-framework — VoK runtime core: bootstrap,
Session,Cookies, async executor, i18n. No DB dependency. Always pulled in transitively. - vok-framework-vokdb — Vaadin + SQL via ktorm + ktorm-vaadin. Provides
VaadinOnKotlin.dataSource(which also wiresActiveKtorm.database), thetoId(idColumn)Binder helper, and a reified-genericenumFilterField<E>()factory. - vok-rest — REST server support. Javalin 5 + Gson. Exposes ktorm tables via
Table<E>.getCrudHandler(). Read endpoints fully implemented; create/update/delete return 501 pending a Gson↔ktorm-Entity adapter. - vok-rest-client — REST client helpers built on the JDK
HttpClient. ORM-agnostic. - vok-example-crud — runnable demo and integration-test harness.
interface Person : ActiveEntity<Person> {
var id: Long?
@get:NotNull @get:Size(min = 1, max = 200) var name: String?
@get:NotNull @get:Min(15) @get:Max(100) var age: Int?
var dateOfBirth: LocalDate?
override val table: Table<Person> get() = Persons
companion object : Entity.Factory<Person>()
}
object Persons : Table<Person>("Person") {
val id = long("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val age = int("age").bindTo { it.age }
val dateOfBirth = date("dateOfBirth").bindTo { it.dateOfBirth }
}@WebListener
class Bootstrap : ServletContextListener {
override fun contextInitialized(sce: ServletContextEvent?) {
VaadinOnKotlin.dataSource = HikariDataSource(hikariConfig) // wires ActiveKtorm.database too
VaadinOnKotlin.init()
Flyway.configure().dataSource(VaadinOnKotlin.dataSource).load().migrate()
}
override fun contextDestroyed(sce: ServletContextEvent?) {
VaadinOnKotlin.destroy()
}
}val p = Person { name = "Leto"; age = 45 }.create() // insert
val one = db { database.sequenceOf(Persons).find { it.id eq p.id!! } }
val all = db { database.sequenceOf(Persons).toList() }private val nameFilter = FilterTextField()
private val ageFilter = NumberRangePopup()
private val dataProvider = Persons.dataProvider
personGrid = grid<Person>(dataProvider) {
appendHeaderRow()
val filterBar = appendHeaderRow()
columnFor(Person::name, key = Persons.name.e.key) {
filterBar.getCell(this).component = nameFilter
nameFilter.addValueChangeListener { updateFilter() }
}
columnFor(Person::age, key = Persons.age.e.key) {
filterBar.getCell(this).component = ageFilter
ageFilter.addValueChangeListener { updateFilter() }
}
}
private fun updateFilter() {
val conditions = mutableListOf<ColumnDeclaring<Boolean>?>()
if (nameFilter.value.isNotBlank()) conditions += Persons.name.ilike("${nameFilter.value.trim()}%")
conditions += Persons.age.between(ageFilter.value.asIntegerInterval())
dataProvider.setFilter(conditions.and())
}verticalLayout {
formLayout {
textField("Name:") { focus() }
textField("Age:")
}
horizontalLayout {
button("Save") {
onClick { okPressed() }
setPrimary()
}
}
}@WebServlet(urlPatterns = ["/rest/*"])
class JavalinRestServlet : HttpServlet() {
private val javalin = Javalin.createStandalone { it.gsonMapper(VokRest.gson) }.apply {
crud2("/rest/person", Persons.getCrudHandler(allowModification = true))
}.javalinServlet()
override fun service(req: HttpServletRequest, resp: HttpServletResponse) { javalin.service(req, resp) }
}Exposes GET /rest/person?name=Leto&offset=0&limit=20&sort=age:desc&count=true and so on. See
vok-rest for the full wire format.
Bug reports: VoK issue tracker.
- Vaadin troubleshooting
- ktorm docs
- ktorm-vaadin
- beverage-buddy-ktorm — canonical end-to-end example
Licensed under the MIT License.
Copyright (c) 2017-2026 Martin Vysny.