Blog

Reactive Chat App with Spring Boot, Project Reactor, and Vaadin

By  
Marcus Hellberg
Marcus Hellberg
·
On Mar 13, 2020 5:50:00 PM
·

In this tutorial, we learn how to build a reactive chat application. The backend consists of a Spring Boot Application, reactive data types from Project Reactor, and Vaadin for the UI layer.

The chat app with a header
Figure 1. The chat app with a header, messages, and an input layout

Downloading the starter project

Start by going to https://vaadin.com/start/latest/project-base-spring. You can leave the group id and name as they are unless you want to change them.

Vaadin Spring starter download page.
Figure 2. Vaadin Spring starter download page

The application is a Maven project that extends the Spring Boot starter project and adds some example Vaadin code.

Once downloaded, unzip the project and open it up in your editor of choice.

Add Project Reactor dependency

Before we can start, we need to add a dependency to Project Reactor for the reactive data types.

In the <dependencies>-section of pom.xml, add the following dependency:

pom.xml
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>

Delete unneeded code

Delete MessageBean.java, and clean out all the code from the MainView constructor, so we have a clean slate to start from.

MainVew.java
@Route
@PWA(name = "Vaadin Chat", shortName = "Vaadin Chat")
public class MainView extends VerticalLayout {

public MainView() {
// Delete all code from here
}
}

Set up main layout and header

By default, a VerticalLayout is only as big as the content in it requires. We need to make the main layout full size so that we can put the message input layout all the way at the bottom. We also want to center all the content.

Also, add a header and a class name so we can style it with CSS.

MainVew.java
@CssImport("./styles/styles.css")
public MainView() {
setSizeFull();
setDefaultHorizontalComponentAlignment(Alignment.CENTER);
addClassName("main-view");

H1 header = new H1("Vaadin Chat");
header.getElement().getThemeList().add("dark"); (1)

add(header);
}
  1. The dark theme variant will give the heading a dark background and light text.

Then, create a new CSS file for the styles:

frontend/styles/styles.css
.main-view h1 {
margin: 0;
padding: 16px;
width: 100%;
}

Run the Spring Boot application. You can do this either by right-clicking on Application.java and selecting "Run," or by running mvn spring-boot:run in the terminal. Spring Boot devtools will monitor for changes as you compile the project.

Open your browser to localhost:8080. You should see the following:

Application header.

Signing in

With the header in place, the next thing we need is to identify the person that opens the application.

In the MainView constructor, call askUsername() and implement the method underneath.

MainVew.java
public MainView() {
// omitted
askUsername();
}

private void askUsername() {
HorizontalLayout layout = new HorizontalLayout();
TextField usernameField = new TextField();
Button startButton = new Button("Start chat");
layout.add(usernameField, startButton);
add(layout);
}

Notice that we wrap the text field and button in a HorizontalLayout to place them next to each other. Then, we add that layout to the main VerticalLayout.

Next, add a click listener on the button to collect the username.

MainVew.java
private String username; (1)

private void askUsername() {
// omitted
startButton.addClickListener(click -> {
username = usernameField.getValue();
remove(layout);
showChat();
});
}

private void showChat() {

}
  1. Define the username field at the top of the file.

The listener saves the value of the username text field into a field on MainLayout, removes the layout, and finally calls showChat.

Build the application, and you should now see this in your browser. If you click the button, you should notice the layout disappear.

Username prompt

Build a custom component for the message list

We need to create a custom component to handle the message list. We need it to be able to scroll when the number of messages exceeds the available space. We also want to have the latest message scrolled into view on updates.

MessageList.java
public class MessageList extends Div {

public MessageList() {
addClassName("message-list");
}

@Override
public void add(Component... components) {
super.add(components); (1)

components[components.length-1]
.getElement() (2)
.callFunction("scrollIntoView"); (3)
}
}
  1. Let the parent take care of adding the components.

  2. Get the last added component and call getElement to get a handle to its DOM element.

  3. Use callFunction to call the JavaScript function scrollIntoView on the element.

Then, add the following to the stylesheet to enable scrolling.

styles.css
.message-list {
overflow-y: scroll;
width: 100%;
}

.message-list p {
width: 100%;
}

Build the main layout

Now that we have a component that can handle a long list of chat messages, we are ready to build the main chat layout.

MainVew.java
private void showChat() {
MessageList messageList = new MessageList();
add(messageList, createInputLayout());
}

private Component createInputLayout() {
HorizontalLayout layout = new HorizontalLayout();

TextField messageField = new TextField();
Button sendButton = new Button("Send");
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); (1)

layout.add(messageField, sendButton);
return layout;
}
  1. The LUMO_PRIMARY theme variant for the button makes it more visually distinct to help users understand its the primary action.

If you build the app and refresh your browser, you should now see this:

Unfinished main layout

Although we have all the components visible, they still need some tweaking.

Use expand to allocate size between components

Vaadin layouts have an expand method that can be used to instruct it how to allocate space to its children. If you call expand on one component in a layout, it will grow to take up all the space in a layout that is not needed by other components.

MainVew.java
private void showChat() {
// omitted
expand(messageList);
}
private Component createInputLayou() {
// omitted
layout.setWidth("100%");
layout.expand(messageField);
return layout;
}

Build and run the app, and you should now see the correct layout.

Finished layout

Reactive backend for handling the messages

The UI is now built as far as we can build it without having a backend.

Start off by creating a new file, ChatMessage.java, as the data object for a single chat message.

ChatMessage.java
class ChatMessage {
private String from;
private String message;

ChatMessage(String from, String message) {
this.from = from;
this.message = message;
}

String getFrom() {
return from;
}

String getMessage() {
return message;
}
}

Then, open Application and declare two beans:

ChatMessage.java
@Bean
UnicastProcessor<ChatMessage> publisher(){
return UnicastProcessor.create();
}
@Bean
Flux<ChatMessage> messages(UnicastProcessor<ChatMessage> publisher) {
return publisher.replay(30).autoConnect();
}

The UnicastProcessor is the central place to which all clients will post their messages.

The Flux listens to that UnicastProcessor, buffering the last 30 messages, and can be subscribed to by all connected clients.

To use these, we need to autowire them into MainView. Modify the constructor to take them as parameters, and bind them to fields.

MainView.java
private final UnicastProcessor<ChatMessage> publisher;
private final Flux<ChatMessage> messages;

public MainView(UnicastProcessor<ChatMessage> publisher,
Flux<ChatMessage> messages) {
this.publisher = publisher;
this.messages = messages;
//omitted
}
//omitted

We now have all the backend code we need to continue building the UI.

Send messages

Go back into createInputLayout in MainLayout and add a listener to the send button. The listener should create a new ChatMessage object and pass it to producer.onNext.

MainView.java
sendButton.addClickListener(click -> {
publisher.onNext(new ChatMessage(username, messageField.getValue()));
messageField.clear();
messageField.focus();
});
messageField.focus();

In addition to sending the message, clear out and focus the input field so that the user can continue writing their next message without having to clear out the input themselves.

Receive messages

Now that we’re able to send messages, the next step is to listen for incoming messages and displaying them. In the showChat method, subscribe to the messages flux, and append each message to the MessageList component we created earlier.

MainView.java
messages.subscribe(message -> {
messageList.add(
new Paragraph(message.getFrom() + ": " +
message.getMessage()))
});

Use websockets for two way communication

The final thing we need to take care of is sending messages out to all clients. By default, Vaadin uses XHR requests for communication. This means that by default all interactions are initiated by the browser. In our case, we want the server to be able to push out new messages to all connected clients. We can do this by using a websocket to send and receive messages.

Add a @Push annotation on the MainView class to instruct Vaadin to use a websocket.

MainView.java
@Push
public class MainView extends VerticalLayout {

Then, we need to assure Vaadin that we know what we’re doing when we are updating the UI from an outside thread (messages from other users are triggered outside the normal request-response cycle of the user). We can do this by using the access helper on the main UI class. It takes in a Command and ensures safe concurrent access and that Vaadin updates the changes to the client.

Change the message subscription in the showChat method to the following:

MainView.java
messages.subscribe(message -> {
getUI().ifPresent(ui -> (1)
ui.access(() -> (2)
messageList.add(
new Paragraph(message.getFrom() + ": " +
message.getMessage())
)
));
});
  1. The UI getter returns an Optional. It may be empty if the component is not attached at the moment. In our case it will always be attached.

  2. Pass the message adding logic into the access method.

Wrapup

Build the application and refresh your browser. You should now have a working chat application with a reactive backend and web socket support. Open a second window and try chatting with multiple participants.

The chat app with a header

Source code on GitHub.

Marcus Hellberg
Marcus Hellberg
Marcus is the VP of Developer Relations at Vaadin. His daily work includes everything from writing blogs and tech demos to attending events and giving presentations on all things Vaadin and web-related. You can reach out to him on Twitter @marcushellberg.
Other posts by Marcus Hellberg