Skip to content

mvysny/vaadin-on-kotlin

Repository files navigation

Powered By Vaadin on Kotlin GitHub tag Maven Central Build Status

Welcome to Vaadin-on-Kotlin

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.

Getting started

  1. Install JDK 21 (required by Vaadin 25) and a git client if you don't already have them.

  2. 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
  3. Visit http://localhost:8080. The CRUD demo backs a Vaadin Grid by a ktorm-vaadin EntityDataProvider, with filter components attached to the grid header.

  4. See vok-example-crud for the demo source. For the canonical end-to-end ktorm-vaadin sample, see beverage-buddy-ktorm.

Modules

  • 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 wires ActiveKtorm.database), the toId(idColumn) Binder helper, and a reified-generic enumFilterField<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.

Code examples

Define a ktorm entity + table

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 }
}

Bootstrap

@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()
    }
}

Save / query

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() }

Grid with ktorm-vaadin DataProvider + filters

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())
}

UI DSL

verticalLayout {
    formLayout {
        textField("Name:") { focus() }
        textField("Age:")
    }
    horizontalLayout {
        button("Save") {
            onClick { okPressed() }
            setPrimary()
        }
    }
}

REST server (vok-rest)

@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.

Contributing

Bug reports: VoK issue tracker.

Further links

License

Licensed under the MIT License.

Copyright (c) 2017-2026 Martin Vysny.

About

Writing full-stack statically-typed web apps on JVM at its simplest

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages