Documentation

Documentation versions (currently viewing)

First View

Learn how to implement a minimal chat user interface.

The first step in the exercises for this tutorial is to create a view. A view is an application’s user interface page, or rather a composite of text, images, interactive components, and other elements the user may view and with which the user may interact. Examples of this might be an interactive, dynamic web page or the screen of a desktop application.

This first view, though, should specifically allow the user to receive and post messages in a given channel. For the purposes here, it’ll contain the following three user interface components:

  • List of messages in the channel;

  • Input field for writing new messages; and

  • Button for sending new messages to the channel.

Furthermore, the view needs access to the ID of the channel, which is passed as a URL parameter (e.g., /channel/<channelId>). It will also need an instance of ChatService, injected through constructor injection.

To understand better what you’re working towards, once finished, the view should look like this:

A web application with a listing of chat messages and an input field for posting new messages.

This may seem very plain and lacking in luster. However, for learning purposes, it’s best to start with a simple goal so you may focus on the essential elements and steps in the process of creating a view and an application. You’ll develop this initial view as you go through this tutorial.

Routing in Flow

You can make any Vaadin component a routing target by adding a @Route("<path>") annotation.

You can pass parameters into any Vaadin route. There are various ways of doing this, but the easiest is to implement the HasUrlParameter interface. This interface defines a single URL parameter that is appended to the path of the route. The type of the parameter can be Long, Integer, String, or Boolean.

You can find more information about routing in the Routing page of the Flow documentation.

Incidentally, adding @Route("<path>") annotation also makes it possible to inject Spring beans into the component through constructor parameter injection. However, the component won’t become a Spring bean itself. The component cannot be injected into other Spring beans, and its lifecycle is still managed by Vaadin — not by Spring.

Create the Channel View

To create the channel view, create a class named, ChannelView in the com.example.application.views.channel package. You would do that like this:

package com.example.application.views.channel;

import com.example.application.chat.ChatService;
import com.vaadin.flow.component.messages.MessageList;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.Route;

@Route(value = "channel") // (1)
public class ChannelView extends VerticalLayout // (2)
        implements HasUrlParameter<String> { // (3)

    private final ChatService chatService;
    private final MessageList messageList;

    public ChannelView(ChatService chatService) { // (4)
        this.chatService = chatService;
    }

    @Override
    public void setParameter(BeforeEvent event, String channelId) { // (5)
    }
}
  1. This line makes the view accessible from the /channel path.

  2. VerticalLayout is one of the built-in layouts in Vaadin that vertically organizes components on top of each other.

  3. This says that the view accepts a single URL parameter of the type, String — the channel ID.

  4. The ChatService is injected by Spring.

  5. This method is provided by the HasUrlParameter interface. It will be called whenever the URL parameter is changed.

Now that you’ve created the channel view, you’ll next need to set up the user interface. Update the constructor like this:

public ChannelView(ChatService chatService) {
    this.chatService = chatService;
// tag::snippet[]
    setSizeFull(); // (1)

    messageList = new MessageList(); // (2)
    messageList.setSizeFull();
    add(messageList);

    var messageInput = new MessageInput(); // (3)
    messageInput.setWidthFull();
    add(messageInput);
// end::snippet[]
}
  1. This makes the view fill the entire screen by setting both its width and height to 100%.

  2. MessageList is a built-in component for displaying messages from different users.

  3. MessageInput is a built-in component for entering and sending messages.

Get Channel ID

You’ll need the channel ID for posting and for receiving messages. You can get the channel ID from the setParameter() method. You should verify that it’s valid with ChatService. Then you’d store it in a private field to use later.

Regarding this last point, start by declaring a private field that’ll contain the channel ID. You can do that like so:

private String channelId;

Then, implement the setParameter() method like this:

@Override
public void setParameter(BeforeEvent event, String channelId) {
// tag::snippet[]
    if (chatService.channel(channelId).isEmpty()) {
        throw new IllegalArgumentException("Invalid channel ID"); // (1)
    }
    this.channelId = channelId;
// end::snippet[]
}
  1. In a future iteration, you’ll navigate away from this view if the channel ID is invalid. For now, throwing an exception as shown here is enough.

Post a Message

You now have almost everything you need to start posting messages to a channel. You’ll need to add a listener, though, to the MessageInput component that gets called whenever the user sends a message. Then you’ll call the postMessage() method of ChatService.

It’s a good practice to put the user interface logic in private methods rather than inside event listeners. Therefore, start by creating this method:

private void sendMessage(String message) {
    if (!message.isBlank()) {
        chatService.postMessage(channelId, message);
    }
}

Next, inside the constructor of ChannelView, add a SubmitEvent listener to the MessageInput component. You can do this by either calling the addSubmitListener() method, or by passing the listener as a constructor parameter, like this:

var messageInput = new MessageInput(event -> sendMessage(event.getValue()));

Server Push in Flow

Since messages can be received at any time, you’ll use server push to update the user interface. When server push is enabled, Vaadin will use a websocket connection to push updates to the browser. In order to enable server push, you have to add the @Push annotation to your application shell class.

The application shell class is an application that implements the AppShellConfigurator interface. In Spring Boot applications, the main Application class is often used for this.

Now open com.example.application.Application and change it accordingly:

package com.example.application;

// tag::snippet[]
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.component.page.Push;
// end::snippet[]
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.time.Clock;

@SpringBootApplication
// tag::snippet[]
@Push // (1)
public class Application implements AppShellConfigurator { // (2)
// end::snippet[]

    @Bean
    public Clock clock() {
        return Clock.systemUTC();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
  1. This is where the @Push annotation is added.

  2. This line implements the AppShellConfigurator interface.

Once you’ve enabled server push, you can trigger it in various ways. The easiest is to use the UI.access() method, which can be called from any thread. The method takes a lambda or a function pointer as its parameter and will run it at the next suitable moment.

Vaadin will make sure the session is properly locked while the user interface is being updated. Once the method has returned, Vaadin will automatically push the updates to the browser.

You can find more information about server push in the Flow documentation.

Receive Messages

Returning to the ChannelView, you need to make some additions. In order to receive messages from the server, you’ll have to do a few things:

  1. Subscribe to a Flux returned by the liveMessages() method of ChatService;

  2. Update the MessageList component using server push whenever new messages arrive; and

  3. Unsubscribe when leaving the view to avoid memory leaks.

Since you’ll want to keep the messages you’ve already received, you’ll have to start by creating a new field that will contain them:

private final List<Message> receivedMessages = new ArrayList<>();

The list contains objects of type, Message. You have to convert them, though, to MessageListItem before you can add them to the MessageList component:

private MessageListItem createMessageListItem(Message message) {
    var item = new MessageListItem(
        message.message(),
        message.timestamp(),
        message.author()
    );
    return item;
}

Next, create the method that gets called whenever new messages arrive:

private void receiveMessages(List<Message> incoming) { // (1)
    getUI().ifPresent(ui -> ui.access(() -> { // (2)
        receivedMessages.addAll(incoming);
        messageList.setItems(receivedMessages.stream()
            .map(this::createMessageListItem)
            .toList()); // (3)
    }));
}
  1. The server is providing messages in batches rather than one at a time. This is to improve performance in cases where a plenty of messages are being received in a short amount of time.

  2. You have to use UI.access() whenever you update a Vaadin user interface from a thread other than the HTTP request thread. The method will make sure the session is locked properly during the update, and it’ll push the changes to the browser once finished.

  3. There’s currently no way of adding individual items to a MessageList. You have to re-create all of them.

Next, create the method that subscribes to the service:

private Disposable subscribe() {
    var subscription = chatService
            .liveMessages(channelId)
            .subscribe(this::receiveMessages); // (1)
    return subscription; // (2)
}
  1. Whenever the Flux emits a new batch of messages, the receiveMessages() method is called.

  2. You need a reference to the subscription to be able to cancel it when you don’t need it any longer.

Finally, you have actually to call the newly created subscribe() method. However, you only want to receive messages while the view is visible to the user. You can use component lifecycle callbacks to achieve this.

You can think of a Flow user interface as a tree of components. All components get notified when they are added to this tree (attached) or removed from it (detached). A component can execute code when this happens by overriding the onAttach() and onDetach() methods provided by the Component class, which is the abstract base class for all components.

In addition to overriding onAttach() and onDetach(), you can use the methods addAttachListener() and addDetachListener() to register listeners that will get notified whenever a component is attached or detached.

When you want to register a listener with an object that will outlive the view itself, you should do this when the view is attached and unregister it when the view is detached. If you forget to unregister, you might end up with a memory leak that will slow your application down and eventually crash it.

Next, override the onAttach() method. Inside it, call the subscribe method and also register a detach listener that cancels the subscription:

@Override
protected void onAttach(AttachEvent attachEvent) {
    var subscription = subscribe(); // (1)
    addDetachListener(event -> subscription.dispose()); // (2)
}
  1. When the view is attached to a UI and becomes visible, this says to subscribe to the backend service.

  2. Whenever the view is detached from the UI, this line says to cancel the subscription.

You can find more information about component lifecycle callbacks in the Component Lifecycle Callbacks page of the Flow documentation. For more information about what a Flux is, see the Project Reactor reference documentation.

Try It!

Assuming you followed along closely with the explanations and descriptions above, and you added the text as instructed to your development environment, you’re ready to try the channel view. Start the application by running the following command in the root directory of your project:

./mvnw spring-boot:run

The application will generate some channels for you during startup. Each channel gets a UUID as its ID. Check the log for the URLs, they should look something like this:

http://localhost:8080/channel/28ca4624-81b6-48bd-8090-82efa26cfd02

Now, open your browser with one of the URLs. You should see an interface for entering text. Send some messages. They should appear in the list.

Open another browser window using the same URL and send some more messages from there. Those should appear in the list of both windows.

Stop the application by pressing Ctrl-C.