Vaadin + Kotlin CRUD example
Hello everyone, today we will learn how to develop a full-stack web application that is a basic User Management Application using Kotlin, Vaadin, Spring, and JPA.
Vaadin is the only framework that allows you to write UI 100% in Java without getting bogged down in JS, HTML, and CSS. If you prefer, you can also create layouts in HTML or with a visual designer. Vaadin apps run on the server and handle all communication automatically and securely.
GitHub repository link is provided at the end of this tutorial. You can download the source code.
Following technologies stack being used:
- Spring Boot 2.5.5
- Kotlin
- Vaadin 14.7.0
- Maven 3
- npm package manager
- H2DB
After completing this tutorial what we will build?
We will build a full-stack web application that is a basic User Management Application with CRUD features:
• Create User
• List User
• Update User
• Delete User
• View User
Following is the User Interface of our application -
Let's begin building the application,
Project Structure:
Dependency Management -Maven - pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>kotlinspringvaadincrud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>kotlinspringvaadincrud</name>
<description>Demo project for Spring Boot + vaadin</description>
<properties>
<java.version>11</java.version>
<vaadin.version>14.7.0</vaadin.version>
<kotlin.version>1.5.31</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- tag::starter[] -->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<!-- end::starter[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- tag::bom[] -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- end::bom[] -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Create an Entity Class
package com.knf.dev.demo.kotlinspringvaadincrud.backend.model
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
@Entity
class User {
@Id
@GeneratedValue
var id: Long? = null
var firstName: String? = null
var lastName: String? = null
var email: String? = null
protected constructor() {}
constructor(firstName: String?, lastName: String?, email: String?) {
this.firstName = firstName
this.lastName = lastName
this.email = email
}
}
Create User Repository
package com.knf.dev.demo.kotlinspringvaadincrud.backend.repository
import com.knf.dev.demo.kotlinspringvaadincrud.backend.model.User
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<User?, Long?> {
fun findByEmailStartsWithIgnoreCase(email: String?): List<User?>?
}
Vaadin UI
Index.kt
package com.knf.dev.demo.kotlinspringvaadincrud.frontend.view
import com.knf.dev.demo.kotlinspringvaadincrud.backend.model.User
import com.knf.dev.demo.kotlinspringvaadincrud.backend.repository.UserRepository
import com.knf.dev.demo.kotlinspringvaadincrud.frontend.view.UserEditor.ChangeHandler
import com.vaadin.flow.component.AbstractField.ComponentValueChangeEvent
import com.vaadin.flow.component.ClickEvent
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.grid.Grid
import com.vaadin.flow.component.icon.VaadinIcon
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.data.value.ValueChangeMode
import com.vaadin.flow.router.Route
import org.springframework.util.StringUtils
@Route(value = "/")
class Index(
private val repo: UserRepository,
private val editor: UserEditor
) : VerticalLayout() {
val grid: Grid<User?>
val filter: TextField
private val addNewBtn: Button
fun listUsers(filterText: String?) {
if (StringUtils.isEmpty(filterText)) {
grid.setItems(repo.findAll())
} else {
grid.setItems(
repo.findByEmailStartsWithIgnoreCase
(filterText)
)
}
}
init {
grid = Grid(User::class.java)
filter = TextField()
addNewBtn = Button("New User", VaadinIcon.PLUS.create())
// build layout
val actions = HorizontalLayout(filter, addNewBtn)
add(actions, grid, editor)
grid.height = "300px"
grid.setColumns("id", "firstName", "lastName", "email")
grid.getColumnByKey("id").setWidth("60px").
flexGrow = 0
filter.placeholder = "Filter by email"
// Hook logic to components
// Replace listing with filtered content when user changes
// filter
filter.valueChangeMode = ValueChangeMode.EAGER
filter.addValueChangeListener{ e:
ComponentValueChangeEvent<TextField?, String?> ->
listUsers(e.value)
}
// Connect selected User to editor or hide if none is selected
grid.asSingleSelect()
.addValueChangeListener { e: ComponentValueChangeEvent
<Grid<User?>?,
User?> ->
editor.editUser(e.value)
}
// Instantiate and edit new User the new button is clicked
addNewBtn.addClickListener { e: ClickEvent<Button?>? ->
editor.editUser(User("", "", ""))
}
// Listen changes made by the editor,
// refresh data from backend
editor.setChangeHandler(object : ChangeHandler {
override fun onChange() {
editor.isVisible = false
listUsers(filter.value)
}
})
// Initialize listing
listUsers(null)
}
}
UserEditor.kt
package com.knf.dev.demo.kotlinspringvaadincrud.frontend.view
import com.knf.dev.demo.kotlinspringvaadincrud.backend.model.User
import com.knf.dev.demo.kotlinspringvaadincrud.backend.repository.UserRepository
import com.vaadin.flow.component.ClickEvent
import com.vaadin.flow.component.Key
import com.vaadin.flow.component.KeyNotifier
import com.vaadin.flow.component.KeyPressEvent
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.icon.VaadinIcon
import com.vaadin.flow.component.orderedlayout.HorizontalLayout
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.data.binder.Binder
import com.vaadin.flow.spring.annotation.SpringComponent
import com.vaadin.flow.spring.annotation.UIScope
import org.springframework.beans.factory.annotation.Autowired
@SpringComponent
@UIScope
class UserEditor @Autowired constructor
(private val repository: UserRepository) :
VerticalLayout(), KeyNotifier {
/* Fields to edit properties in User entity */
var firstName = TextField("First name")
var lastName = TextField("Last name")
var email = TextField("Email")
/* Action buttons */
var save = Button("Save", VaadinIcon.CHECK.create())
var cancel = Button("Cancel")
var delete = Button("Delete", VaadinIcon.TRASH.create())
var actions = HorizontalLayout(save, cancel, delete)
var binder = Binder(
User::class.java
)
private var user: User? = null
private var changeHandler: ChangeHandler? = null
fun delete() {
repository.delete(user)
changeHandler!!.onChange()
}
fun save() {
repository.save<User>(user!!)
changeHandler!!.onChange()
}
fun editUser(usr: User?) {
if (usr == null) {
isVisible = false
return
}
val persisted = usr.id != null
user = if (persisted) {
// Find fresh entity for editing
repository.findById(usr.id).get()
} else {
usr
}
cancel.isVisible = persisted
// Bind user properties to similarly named fields
// Could also use annotation or "manual binding"
// or programmatically
// moving values from fields to entities before saving
binder.bean = user
isVisible = true
// Focus first name initially
firstName.focus()
}
fun setChangeHandler(h: ChangeHandler?) {
// ChangeHandler is notified when either save or delete
// is clicked
changeHandler = h
}
interface ChangeHandler {
fun onChange()
}
init {
add(firstName, lastName, email, actions)
// bind using naming convention
binder.bindInstanceFields(this)
// Configure and style components
isSpacing = true
save.element.themeList.add("primary")
delete.element.themeList.add("error")
addKeyPressListener(Key.ENTER,
{ e: KeyPressEvent? -> save() })
// wire action buttons to save, delete and reset
save.addClickListener{ e:
ClickEvent<Button?>? -> save() }
delete.addClickListener{ e:
ClickEvent<Button?>? -> delete() }
cancel.addClickListener{ e:
ClickEvent<Button?>? -> editUser(user) }
isVisible = false
}
}
Spring Boot Main Class
package com.knf.dev.demo.kotlinspringvaadincrud
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
open class SpringvaadincrudApplication
fun main(args: Array<String>) {
runApplication<SpringvaadincrudApplication>(*args)
}
Run the application
$ mvn spring-boot:run
Access the URL: http://localhost:8080/
Download the source:
git clone: