Hello all,
Now when we have signals, I’ve been thinking about how to adapt the ViewModel pattern from Android to Vaadin.
In Android, the pattern serves many purposes that don’t apply to Vaadin. The idea that we could use in Vaadin as well is that the ViewModel contains all the state and all the actions of the UI. The UI (a view, a dialog, a component) then only binds to the ViewModel. In the best case, your UI implementation ends up being a single class with a single constructor that takes the ViewModel as its only parameter.
This forum post is me thinking out loud and asking for input.
UI State
The ViewModel would contain all the state of the UI in the form of signals. You could either have the class expose the signals directly as public methods like this:
public class MessageEditorVM implements Serializable {
private final ValueSignal<String> subject = new ValueSignal<>("");
private final ValueSignal<String> body = new ValueSignal<>("");
public Signal<String> subject() {
return subject;
}
public Signal<String> body() {
return body;
}
}
You could also put all the signals inside a separate State record. This makes it easier to see exactly which UI state a particular VM provides:
public class MessageEditorVM implements Serializable {
public record State(Signal<String> subject, Signal<String> body) {}
private final ValueSignal<String> subject = new ValueSignal<>("");
private final ValueSignal<String> body = new ValueSignal<>("");
private final State state = new State(subject, body);
public State state() {
return state;
}
}
Actions
Every action a user can perform should be represented in the ViewModel. This includes editing a single form field. You could expose the actions as public methods on the ViewModel:
public class MessageEditorVM implements Serializable {
private final ValueSignal<String> subject = new ValueSignal<>("");
private final ValueSignal<String> body = new ValueSignal<>("");
public Signal<String> subject() {
return subject;
}
public Signal<String> body() {
return body;
}
public void setSubject(String subject) {
this.subject.set(subject);
}
public void setBody(String body) {
this.body.set(body);
}
public void send() {
// TODO Call a backend service to send the message
}
}
In the UI, you could then bind the ViewModel with something like this:
subjectField.bindValue(messageEditorVM.subject(), messageEditorVM::setSubject);
bodyField.bindValue(messageEditorVM.body(), messageEditorVM::setBody);
sendBtn.addClickListener(_ -> messageEditorVM.send());
If you wanted to make it more explicit which actions are actually available in the VM, you could also use a sealed interface and records like this:
public class MessageEditorVM implements Serializable {
public record State(Signal<String> subject, Signal<String> body) {}
public sealed interface Action {
public record SetSubject(String subject) implements Action {}
public record SetBody(String body) implements Action {}
public record Send() implements Action {}
}
private final ValueSignal<String> subject = new ValueSignal<>("");
private final ValueSignal<String> body = new ValueSignal<>("");
private final State state = new State(subject, body);
public State state() {
return state;
}
public void onAction(Action action) {
switch (action) {
case Action.SetSubject(String subject) -> this.subject.set(subject);
case Action.SetBody(String body) -> this.body.set(body);
case Action.Send _ -> { /* Send the message */ }
}
}
}
This is a bit more verbose to write and especially calls to bindValue become more complicated. I’m also not sure whether it makes the ViewModel code easier to read and understand. I kind of like that you can see which actions are available by looking at the Action interface, and that there is a single entrypoint for actions – the onAction method.
In real applications, you typically want to nest ViewModels inside each other. For instance, a MessageViewVM might contain multiple instances of MessageEditorVM. This makes things more complicated but also provides more implementation alternatives, each with their own pros and cons. I will return to this topic in a forum post of its own.
What are your thoughts now after reading this post? Do we need something like the ViewModel pattern in Vaadin? Or does it only introduce unnecessary complexity?
-Petter-