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/

git clone:

Comments

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java, Spring Boot Mini Project - Library Management System - Download

Java - Blowfish Encryption and decryption Example

Java - DES Encryption and Decryption example

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete

ReactJS - Bootstrap - Buttons

Spring Boot 3 + Spring Security 6 + Thymeleaf - Registration and Login Example

File Upload, Download, And Delete - Azure Blob Storage + Spring Boot Example

Java - How to Count the Number of Occurrences of Substring in a String

Top 5 Java ORM tools - 2024