Vaadin Listener

Hey guys im new to vaadin. I have a object that i inject into my UI view at the very start. This listens for database updates from firestore and then triggers a function on a database change but i can’t seem to figure out how i can dynamically renrender the UI components on the screen based on this database change. I have a few function that render different components and i try to call these from the listener that is triggered when there is a database change

If I correctly understand your question, you have to update the UI from a background thread.
If so, you might take a look at Server Push documentation.
The Asynchronous Updates chapter shows an example of how to send changes from the server to the client.

The function which gets triggered based on any Firestore change is another directory in the modules with all the views. The server is a REST API deployed to the google cloud which communicates with the Firestore DB. the client code is listening to any DB changes. How do i trigger dynamic changes in the UI from within in the client code.

Can you share how the client code that listens to DB changes is integrated in the Flow UI?

Thats the thing im trying to do it doesn’t work.

What is the object you mentioned?
My understanding was that you already have the client side listening for DB events, but you don’t know how to propagate them to the Flow application to re-render the UI.

Can you share some more details on how the integration should work? Do you have some JavaScript library that you need to integrate in the Vaadin application?

This is my gameService that is injected into the game.

package com.example.application.service;

import FirestoreHelpers.FirestoreIntializer;
import com.google.cloud.firestore.*;
import io.micronaut.core.annotation.Nullable;
import model.Game;
import model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.ArrayList;
import java.util.List;

@Service
public class GameService {
    private final Firestore firestore;
    private final WebClient webClient;
    private Game currentGame;
    private User currentUser;
    private List<GameChangeListener> listeners = new ArrayList<>();
    private ListenerRegistration listenerRegistration;

    @Autowired
    public GameService(WebClient.Builder webClientBuilder, FirestoreIntializer firestoreInitializer) {
        this.webClient = webClientBuilder.baseUrl("http://localhost:8080").build();
        this.firestore = firestoreInitializer.DB;
        this.currentGame = new Game();
    }

    public synchronized void setCurrentGame(Game currentGame) {
        this.currentGame = currentGame;
    }

    public synchronized Game getCurrentGame() {
        return currentGame;
    }

    public void updateGameView(Game updatedGame) {
        System.out.println("gameViewUpdating....");
        notifyListeners(updatedGame);
    }

    public void startListeningToGameChanges(String gameId) {
        final DocumentReference docRef = firestore.collection("games").document(gameId);
        docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
            @Override
            public void onEvent(@Nullable DocumentSnapshot snapshot, @Nullable FirestoreException e) {
                if (e != null) {
                    System.err.println("Listen failed: " + e);
                    return;
                }
                System.out.println("change");
                if (snapshot != null && snapshot.exists()) {
                    System.out.println("changeInside");
                    Game updatedGame = snapshot.toObject(Game.class);
                    setCurrentGame(updatedGame);
                    updateGameView(updatedGame);
                    System.out.println(updatedGame);
                } else {
                    System.out.print("Current data: null");
                }
            }
        });
    }

    public void setCurrentUser(User user) {
        this.currentUser = user;
    }

    public User getCurrentUser() {
        return currentUser;
    }

    public void persistNewGame() {
        String error = "";
        try {
            error = webClient.post().uri("/newGame")
                    .bodyValue(this.currentGame)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();
        } catch (Exception e) {
            throw new RuntimeException("Error persisting new game", e);
        }
    }

    public void addListener(GameChangeListener listener) {
        listeners.add(listener);
    }

    public void removeListener(GameChangeListener listener) {
        listeners.remove(listener);
    }

    private void notifyListeners(Game updatedGame) {
        for (GameChangeListener listener : listeners) {

            listener.onGameChange(updatedGame);
        }
    }
}

This is the gameView code.

package com.example.application.views.helloworld;

import com.example.application.service.GameChangeListener;
import com.example.application.service.GameService;
import com.example.application.views.MainLayout;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import model.Game;
import model.User;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

@Route(value = "game", layout = MainLayout.class)
@CssImport("./themes/Scrum-Poker-App/GameView.css")
public class GameView extends VerticalLayout implements GameChangeListener {
    private final GameService gameService;
    private Map<String, Div> userCardMap = new HashMap<>();

    @Autowired
    public GameView(GameService gameService) {
        this.gameService = gameService;
        this.gameService.addListener(this);
        setSizeFull();
        setAlignItems(Alignment.CENTER);
        setJustifyContentMode(JustifyContentMode.CENTER);
        renderUsers();
        renderVotingNumbers();
    }

    public void renderUsers() {
        System.out.println("renderingUsers");
        HorizontalLayout usersCardLayout = new HorizontalLayout();
        for (User user : this.gameService.getCurrentGame().getUsers()) {
            Div userCard = createUserCard(user);
            usersCardLayout.add(userCard);
            userCardMap.put(user.getName(), userCard);
        }
        updateCardState();
        usersCardLayout.getStyle().set("justify-self", "center");
        usersCardLayout.getStyle().set("align-self", "center");
        usersCardLayout.getStyle().set("margin-top", "20%");

        add(usersCardLayout);
    }

    public void updateCardState() {
        for (Map.Entry<String, Integer> entry : this.gameService.getCurrentGame().getCurrentVote().getUserVote().entrySet()) {
            if (userCardMap.containsKey(entry.getKey())) {
                Div userCard = userCardMap.get(entry.getKey());
                userCard.addClassName("user-card-selected");
            }
        }
    }

    private Div createUserCard(User user) {
        Div userCard = new Div();
        Div userView = new Div();
        userView.addClassName("user-view");
        userCard.addClassName("user-card");
        userCard.setId(user.getName());
        Span userName = new Span(user.getName());
        userName.addClassName("user-name");
        userView.add(userName, userCard);
        return userView;
    }

    public void renderVotingNumbers() {
        System.out.println("renderingVotingNumbers");
        ArrayList<Button> buttons = new ArrayList<>();
        for (Integer number : this.gameService.getCurrentGame().getVotingSystem()) {
            Button button = new Button(String.valueOf(number));
            buttons.add(button);
            button.addClassName("VoteButtons");
            button.addClickListener(e -> {
                buttons.forEach(btn -> btn.removeClassName("selected"));
                button.addClassName("selected");
                String selectedValue = button.getText();
                gameService.getCurrentGame().getCurrentVote().getUserVote().put(gameService.getCurrentUser().getName(), Integer.parseInt(selectedValue));
                updateCardState();
            });
        }
        Button refreshButton = new Button("click-me");
        add(refreshButton);
        refreshButton.addClickListener(event ->{
            removeAll();
            renderUsers();
            renderVotingNumbers();
        });
        HorizontalLayout buttonLayout = new HorizontalLayout(buttons.toArray(new Button[0]));
        buttonLayout.add(refreshButton);
        buttonLayout.setWidthFull();
        buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
        buttonLayout.getStyle().set("margin-bottom", "5%");
        buttonLayout.getStyle().set("margin-top", "7.5%");
        add(buttonLayout);
    }

    @Override
    public void onGameChange(Game updatedGame) {
        System.out.println("on Game Change");
        removeAll();
        this.renderUsers();
        this.renderVotingNumbers();

    }
}

So, I think Server Push is what you are looking for:

  • Enable it by adding @Push annotation to the AppShellConfigurator implementor
  • Use UI.access() when updating the UI

For example, instead of providing this as a GameListener in the constructor, add an attach listener, get the UI instance and create a listener that wraps the UI update into an UI.access() call (it is a lambda in the example, but you can also create a class for it).

        this.gameService = gameService;
        addAttachListener(event -> {
            UI ui = event.getUI();
            this.gameService.addListener(game -> ui.access(() ->
                    onGameChange(game)
            ));
        });

Please take a look at the documentation to better understand how PUSH works.

I agree with Marco that @Push is the missing piece. But the suggested Listener refactoring is not required. You can make it work like this:

private UI ui = null;

public GameView(GameService gameService) {
        this.gameService = gameService;
        this.gameService.addListener(this);
        // get ui instance in attach event
        addAttachListener(event -> this.ui = event.getUI());
        ...
}

public void onGameChange(Game updatedGame) {
        System.out.println("on Game Change");
        // use UI::access to alter the ui state from background thread
        ui.access(() -> {
                removeAll();
                this.renderUsers();
                this.renderVotingNumbers();
        });
    }

Something else that I can see in this code which could lead to rare bugs is that if a game update happens while the view is still rendering the last state, there could be mix ups in the shown view state, because the code calls gameService.getCurrentGame() multiple times. Also, the ui is instantiated in the attachEvent, what if the game has an update inbetween this.gameService.addListener(this) and the actual attach event - my previous code would lead to an NPE.

I have rewritten the code to try to fix these bugs.

public class GameView extends VerticalLayout implements GameChangeListener {
    private final GameService gameService;
    private Map<String, Div> userCardMap = new HashMap<>();

    private UI ui = null;
    private boolean gameUpdatedBeforeViewAttach = false;

    @Autowired
    public GameView(GameService gameService) {
        this.gameService = gameService;
        this.gameService.addListener(this);

        setSizeFull();
        setAlignItems(Alignment.CENTER);
        setJustifyContentMode(JustifyContentMode.CENTER);

        addAttachListener(event -> {
            this.ui = event.getUI();
            if(this.gameUpdatedBeforeViewAttach) {
                this.onGameChange(gameService.getCurrentGame());
            }
        });

        // avoid calling gameService.getCurrentGame() multiple times, 
        // so we do it once and pass the received Game on into the different methods
        Game currentGame = gameService.getCurrentGame();
        renderUsers(currentGame);
        renderVotingNumbers(currentGame);
    }

    public void renderUsers(Game game) {
        System.out.println("renderingUsers");
        HorizontalLayout usersCardLayout = new HorizontalLayout();
        for (User user : game.getUsers()) {
            Div userCard = createUserCard(user);
            usersCardLayout.add(userCard);
            userCardMap.put(user.getName(), userCard);
        }
        updateCardState(game);
        usersCardLayout.getStyle().set("justify-self", "center");
        usersCardLayout.getStyle().set("align-self", "center");
        usersCardLayout.getStyle().set("margin-top", "20%");

        add(usersCardLayout);
    }

    public void updateCardState(Game game) {
        for (Map.Entry<String, Integer> entry : game.getCurrentVote().getUserVote().entrySet()) {
            if (userCardMap.containsKey(entry.getKey())) {
                Div userCard = userCardMap.get(entry.getKey());
                userCard.addClassName("user-card-selected");
            }
        }
    }

    private Div createUserCard(User user) {
        Div userCard = new Div();
        Div userView = new Div();
        userView.addClassName("user-view");
        userCard.addClassName("user-card");
        userCard.setId(user.getName());
        Span userName = new Span(user.getName());
        userName.addClassName("user-name");
        userView.add(userName, userCard);
        return userView;
    }

    public void renderVotingNumbers(Game game) {
        System.out.println("renderingVotingNumbers");
        ArrayList<Button> buttons = new ArrayList<>();
        for (Integer number : game.getVotingSystem()) {
            Button button = new Button(String.valueOf(number));
            buttons.add(button);
            button.addClassName("VoteButtons");
            button.addClickListener(e -> {
                buttons.forEach(btn -> btn.removeClassName("selected"));
                button.addClassName("selected");
                String selectedValue = button.getText();
                game.getCurrentVote().getUserVote().put(gameService.getCurrentUser().getName(), Integer.parseInt(selectedValue));
                updateCardState();
            });
        }
        Button refreshButton = new Button("click-me");
        add(refreshButton);
        refreshButton.addClickListener(event ->{
            removeAll();
            renderUsers();
            renderVotingNumbers();
        });
        HorizontalLayout buttonLayout = new HorizontalLayout(buttons.toArray(new Button[0]));
        buttonLayout.add(refreshButton);
        buttonLayout.setWidthFull();
        buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
        buttonLayout.getStyle().set("margin-bottom", "5%");
        buttonLayout.getStyle().set("margin-top", "7.5%");
        add(buttonLayout);
    }

    @Override
    public void onGameChange(Game updatedGame) {
        System.out.println("on Game Change");

        if (ui != null) {
            this.gameUpdatedBeforeViewAttach = false;
            ui.access(() -> {
                removeAll();
                this.renderUsers(updatedGame);
                this.renderVotingNumbers(updatedGame);
            });
        } else {
            this.gameUpdatedBeforeViewAttach = true;
        }
    }
}

I’m not sure if I got Kaspar’s point correctly, but UI changes in UI.access() cannot happen simultaneous because a VaadinSession lock is held during the command execution.

About the listener being invoked before UI reference is kept, I think it depends on the application; if events fired before the UI is attached are not relevant, just register the listener on attach event (and probably remove it on detach event).

I was trying to say that the two methods renderUsers and renderVotingNumbers should not call gameService.getCurrentGame() multiple times since the returned value could change inbetween. You’d want renderUsers and renderVotingNumbers to render one single game state. So one should load gameService.getCurrentGame() once and from then on use the already loaded instance.

yeah you’ve got a point here. I was just scared of ui.access(..) leading to a NullPointerException. If you register the listener on attach this should be prevented.

@Kaspar4 now I got your point. Thanks for clarification