Teil 11: Trainer for kids

Anwendungsentwicklung mit Vaadin

Sven Ruppert

© Shutterstock / HelenField

Nachdem sich die letzten Teilen von „Backend meets Frontend“ mit dem Testen einer Vaadin-Applikation auseinandergesetzt haben, werden wir uns nun der Anwendungsentwicklung selbst zuwenden. Auch bei unserem kleinen Beispielprojekt behalten wir dabei Themen wie Internationalisierung und Änderbarkeit im Auge.

Ziel dieser Beispielanwendung wird es sein, eine Web-App mit Vaadin zu entwickeln, mit der Grundschulkinder verschiedene Fähigkeiten trainieren können.

Und warum? Die Antwort ist recht einfach:

  • Ich habe schon zu viele Adressbücher als Demo-App gesehen
  • Mein Sohn ist kurz davor in die Schule zu kommen
  • Ich möchte die JavaFX-Anwendung ablösen, damit ich meinen Rechner wieder für mich habe
  • Das Anwendungsszenario eignet sich bestens, um immer wieder erweitert zu werden

Abb. 1: Die Anmeldung zeigt eine Eingabemaske mit der Aufforderung Username und Passwort einzugeben

Aber nun zurück zum Problem an sich. Als erstes kommt eine Anmeldung. Der Use Case ist schnell und einfach erklärt. Zeige eine Eingabemaske mit der Aufforderung Username und Passwort einzugeben (Abb. 1). Nachdem der Button OK gedrückt worden ist, werden die Eingaben verifiziert. Im Erfolgsfall wird auf die Hauptseite der Anwendung weitergeleitet. Ist es zu einem Fehlerfall gekommen – falsche Werte in jeglicher Form – sollen die Eingabefelder geleert werden und der Benutzer soll auf der Seite verbleiben. In dieser Version werden wir einige Dinge vereinfachend annehmen: Es wird nicht in einem Persistence Layer nachgesehen, ob die Daten verfügbar sind.

Abb. 2: Das Hauptfenster zeigt auf der linken Seite von oben nach unten orientiert eine Button-Leiste

Das Hauptfenster soll auf der linken Seite von oben nach unten orientiert eine Button-Leiste haben, mit der zu den jeweiligen Bereichen navigiert werden kann (Abb. 2). Der Bereich auf der rechten Seite ist dann der zur Verfügung stehende Arbeitsbereich. Wir werden in diesem Teil lediglich den LogOut Button erzeugen und in Funktion nehmen (Abb. 3).

Die Synthese in Java

Abb. 3: So funktioniert das Ganze im Endeffekt

Die Anwendung selbst wird, wie in den letzten Teilen erarbeitet, mittels Undertow zur Verfügung gestellt. Zum Starten reicht es wieder aus, die Main-Methode der Klasse org.rapidpm.vaadin.server.Main zu starten. Der Quelltext zu diesem Projekt befindet sich auf GitHub. Ich empfehle, bei einem praktischen Durcharbeiten dieses Artikels den Quelltext in der IDE zur Verfügung zu haben.

Wie beschrieben werden wir nun die benötigten Elemente auf dem Bildschirm platzieren. Hierzu werden gebraucht:

  • Label für Login required
  • TextField für Login
  • PasswordField für Password
  • Button für OK
  • Button für Cancel

In der ersten Version werden wir sehr geradlinig vorgehen. Ich gehe davon aus, dass wie bisher ein Servlet die Verbindung mittels Annotation zu der Klasse mit der UI (MainUI) herstellt. Die Klasse MainUI soll in unserem Fall lediglich die Log-in-Komponente initialisieren und als Inhalt der Seite setzen.

@Theme("valo")
public class MainUI extends UI {
  @Override
  protected void init(VaadinRequest request) {
    setContent(new LoginSimpleImplComponent());
    setSizeFull();
  }
}

Neu in diesem Fall ist, dass wir explizit ein Theme mittels Annotation setzen: @Theme("valo"). Der Rest ist wie gehabt. Wir erzeugen eine Komponente und setzen diese Instanz. Sicherlich kann man auch alles direkt innerhalb der Initialisierungsmethode realisieren. Davon rate ich aber auf jeden Fall ab, da damit schon von Beginn an mit Komponenten gearbeitet wird.

Als nächstes sehen wir uns die Klasse LoginSimpleImplComponent an. Eine Möglichkeit eine Vaadin-Komponente zu erstellen, besteht zum Beispiel darin, von einer Layout-Komponente zu erben und diese um die gewünschten Komponenten zu erweitern. In unserem Fall habe ich mich für ein Panel entschieden. Das Panel selbst bekommt die Beschriftung (Caption) mit dem Inhalt Login required und ersetzt damit das als erstes genannte Label aus der Liste. Die restlichen Elemente werden als Klassenattribute definiert und instanziiert.

public class LoginSimpleImplComponent extends Panel {

  private final TextField login = new TextField();
  private final PasswordField password = new PasswordField();
  private final Button ok = new Button();
  private final Button cancel = new Button();
  private final HorizontalLayout buttons = new HorizontalLayout(ok, cancel);
  
  private final VerticalLayout layout = new VerticalLayout(login, password, buttons);

  // SNIPP

}

Hiermit ist die Struktur der Seite vollständig definiert. Innerhalb des Konstruktors unserer Komponente werden nun die Elemente konfiguriert.

  public LoginSimpleImplComponent() {
    setCaption("Login required");
    setContent(layout);
    setSizeFull();

    login.setId("login");
    login.setCaption("Login");

    password.setId("password");
    password.setCaption("Password");

    ok.setId("ok");
    ok.setCaption("Ok");
    ok.addClickListener((Button.ClickListener) event -> {
      boolean isValid = checkLoginData(login.getValue(), password.getValue());
      clearInputFields();
      getCurrent().setContent((isValid) ? mainView() : this);
    });

    cancel.setId("cancel");
    cancel.setCaption("Cancel");
    cancel.addClickListener((Button.ClickListener) event -> clearInputFields());
  }

Nun besitzen alle Elemente eine Beschriftung, eine ID, die wir für jUnit- und TestBench-Tests benötigen, und falls nötig die Verknüpfung zu der gewünschten Logik.

Es gibt hier drei Methoden, die verwendet werden. Die erste ist clearInputFields. Hier werden lediglich die beiden Eigabefelder zurückgesetzt. Damit sind die gemachten Eingaben entfernt. Dies soll bei einem fehlerhaften Anmeldeversuch und Abbruch der Anmeldung geschehen.

  private void clearInputFields() {
    login.setValue("");
    password.setValue("");
  }

Die zweite Methode ist checkLoginData. Diese Methode ist mehr oder weniger als Mock oder Platzhalter zu verstehen, den wir verwenden, um die grundlegende Mechanik zu installieren und testen zu können. Das wird natürlich später durch einen LoginService ersetzt. Der Einfachheit halber halten wir die Benutzerinformationen nicht in der Session, um bei einem wiederholten Ladevorgang der Seite den Log-in-Vorgang zu überspringen.

  private boolean checkLoginData(String login, String password) {
    return ! (Objects.isNull(login) || Objects.isNull(password))
           && (! (login.isEmpty() || password.isEmpty())
               && (login.equals("admin") && password.equals("admin")));
  }

Die dritte Methode ist mainView. Wenn ein Anmeldevorgang erfolgreich ist, wird der bestehende Inhalt der Seite mit der Instanz der MainView ersetzt. Ab diesem Zeitpunkt ist der Anmeldevorgang beendet.

private Component mainView() {
    Button logOut = new Button("LogOut");
    logOut.addClickListener(e -> {
      getCurrent().getSession().close();
      getCurrent().getPage().setLocation("/");
    });
    return logOut;
  }

Die einzige Funktion dieses Buttons ist es, die Session zu beenden und auf die Log-in-Seite zurückzugelangen.

Der gerade realisierte Quelltext funktioniert. Allerdings gibt es einige Dinge, die bei größeren Projekten recht unbequem werden können. Aus diesem Grunde werden wir nun Schritt für Schritt die Implementierung neu aufsetzen. Kommen wir als erstes zur Struktur der Seite. Hier bleibt fast alles beim derzeitigen Stand der Dinge.

 
  private final TextField login = new TextField();
  private final TextField login = new TextField();
  private final PasswordField password = new PasswordField();
  private final Button ok = new Button();
  private final Button cancel = new Button();
  private final HorizontalLayout buttons = new HorizontalLayout(ok, cancel);

  private final VerticalLayout layout = new VerticalLayout(login, password, buttons);
  private final Panel panel = new Panel(PANEL_CAPTION_MAIN, layout);
  
  public LoginComponent() {
    setCompositionRoot(panel);
  }

Die Elemente werden wieder als Klassenattribute definiert und in der gewünschten Art miteinander
verschachtelt. Der Konstruktor besteht nun ausschließlich aus dem Setzen der final haltenden Komponente. Aber die Methode hier heißt nicht mehr setContent(..).

Zu Beginn hatten wir davon gesprochen, dass man eigene Komponenten durch das Ableiten von einer Vaadin-Komponente realisiert. Im der ersten Version haben wir uns für ein Panel entschieden. Es gibt aber genau für diesen Fall spezielle Komponenten. Bis Vaadin Version 8.0.x gibt es die Klasse CustomComponent, ab der Version 8.1 gibt es zusätzlich die Klasse Composite. Der Unterschied liegt darin, dass die neue Klasse keine umhüllende Ebene im DOM erzeugt. Also: Ab der Version 8.1 die Klase Composite verwenden. Zum Zeitpunkt der Veröffentlichung dieses Artikels gab es lediglich die Version 8.1.0.alpha6, weswegen ich davon abgesehen habe. Wir verwenden also die Klasse CustomComponent.

public class LoginComponent extends CustomComponent

Nun haben wir also die Struktur der Seite erzeugt. Kommen wir nun zu den Feinheiten. Alle weiteren Modifikationen an den Elementen sind in die Methode public void postProcess() ausgelagert. Hier werden die Beschriftungen und IDs gesetzt, genauso wie die ActionListener.

public void postProcess() {
    panel.setId(ID_PANEL_MAIN);
    panel.setCaption(PANEL_CAPTION_MAIN);
    panel.setSizeFull();

    login.setId(ID_TEXTFIELD_LOGIN);
    login.setCaption(BUTTON_CAPTION_LOGIN); //TODO i18n

    password.setId(ID_PASSWORDFIELD_PASSWORD);
    password.setCaption(BUTTON_CAPTION_PASSWORD); //TODO i18n

    ok.setCaption(BUTTON_CAPTION_OK);
    ok.setId(ID_BUTTON_OK);
    ok.addClickListener((Button.ClickListener) event -> {
      boolean isValid = loginService.check(login.getValue(), password.getValue());
      clearInputFields();
      UI
          .getCurrent()
          .setContent((isValid) ? mainViewSupplier.get() : this);
    });

    cancel.setId(ID_BUTTON_CANCEL);
    cancel.setCaption(BUTTON_CAPTION_CANCEL);
    cancel.addClickListener((Button.ClickListener) event -> clearInputFields());
  }

Auf den ersten Blick sieht es sicherlich nach einem reinen Extrahieren in eine Methode aus. Hierbei stellt sich die Frage, warum das passieren soll. Solange man sich in der Ausführung eines Konstruktors befindet, ist man in einer Lebenszyklusphase der Objekterstellung, die eine gewisse Sonderstellung inne hat. Eventuell gibt es ja noch 0..n static- und non-static-Blöcke. Werden die vor oder nach dem Konstruktor ausgeführt? Wenn es mehrere gibt, in welcher Reihenfolge? Fest steht auf jeden Fall, dass es recht oft vorkommt, dass man sich nicht hundert prozentig sicher ist, wie genau der Ablauf ist. Und wir haben noch nicht von Vererbung gesprochen. Nun kann es auch sein, dass bestimmte Komponenten erst nach dem Erzeugen der Instanz vorhanden sind. Der Fall kann eintreten, wenn mit Dependency-Injection-Mechanismen oder anderen Lebenszyklus-behafteten Technologien gearbeitet wird. Lange Rede, kurzer Sinn: Es wird ausgelagert, damit die Entscheidung, wann es durchgeführt wird, zu einem späteren Zeitpunkt getroffen oder auch geändert werden kann.

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Testbarkeit und i18n

Um später die Komponenten einfach adressieren zu können, muss man den Komponenten eindeutige IDs geben. In einem kleinen Projekt hat man sicherlich noch den Überblick. Da wir Komponenten bauen wollen, wissen wir nicht so recht welche IDs schon vergeben sind. Auch die Mehrsprachigkeit ist eine Anforderung, die recht viel Aufwand bedeutet, wenn diese erst zu einem späteren Zeitpunkt in einem Projekt berücksichtigt wird. Sehen wir uns hier das nun exemplarisch für den OK-Button an.

    ok.setCaption(BUTTON_CAPTION_OK);
    ok.setId(ID_BUTTON_OK);

Hier werden Kontstanten verwendet. Diese werden wie folgt definiert.

public static final String BUTTON_CAPTION_LOGIN = "Login";
public static final String ID_BUTTON_OK = buttonID().apply(LoginComponent.class, BUTTON_CAPTION_OK);

Die Konstante BUTTON_CAPTION_LOGIN wird in diesem Fall noch fest gesetzt. Das wird in einem späteren Artikel behandelt, in dem wir gesondert auf i18n eingehen werden, und durch einen PropertyService ersetzt. Wichtig an der Stelle ist erst einmal, das wir die betreffenden Stellen isoliert haben.

Die Konstante ID_BUTTON_OK allerdings wird von einer Funktion gesetzt. Eingangswerte sind zum einen die Klasse, in der sich das Element befindet, und zum anderen die Konstante, die für die Beschriftung verwendet worden ist.

Werfen wir nun einen Blick auf das Ergebnis der Methode buttonID(). Wir bekommen eine BiFunction<Class, String, String>, die selbst auf die Methode genericID() delegiert.

  static BiFunction<Class, String, String> buttonID() {
    return (uiClass, label) -> genericID().apply(uiClass, Button.class, label);
  }

Hier findet eine Argumentreduktion statt, indem der zweite Parameter konstant auf Button.class gesetzt wird.

  static TriFunction<Class, Class, String, String> genericID() {
    return (uiClass, componentClass, label)
        -> (uiClass.getSimpleName()
            + "-" + componentClass.getSimpleName()
            + "-" + label.replace(" ", "-"))
        .toLowerCase(Locale.US);
  }

Der Rückgabewert der Methode genericID(..) wiederum ist
TriFunction<Class, Class, String, String>. Hierbei handelt es sich um eine Klasse
aus dem Projekt functional-reactive-lib.

@FunctionalInterface
public interface TriFunction<T1, T2, T3, R> {
  R apply(T1 t1, T2 t2, T3 t3);

  default <V> TriFunction<T1, T2, T3, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T1 t1, T2 t2, T3 t3) -> after.apply(apply(t1, t2, t3));
  }
}

Die Aufgabe der TriFunction ist es, aus den Parametern Zielklasse, Typ und Beschriftung eine ID zu generieren. Hier handelt es sich um eine triviale Implementierung, die zu Beginn mehr als hinreichend ist und bei Bedarf später einfach durch eine dem Problem adäquate Implementierung ersetzt werden kann. Von Vorteil ist es, das der ComponentIDGenerator auch zum Erstellen von Tests verwendet werden kann, um Elemente zu adressieren. So kann eine ID für eine andere Komponente erzeugt werden, die in dem im Test verwendeten PageObject eventuell noch nicht vorhanden ist.

ButtonElement dashboard = pageObject
                              .btn()
                              .id(buttonID()
                                  .apply(MainView.class, "Dashboard"));

Um dies für die Entwicklung möglichst komfortabel zu gestallten, wird das Interface IdGenerator nach und nach um die verwendeten Typen erweitert.

public interface ComponentIDGenerator {

  static TriFunction<Class, Class, String, String> genericID() {
    return (uiClass, componentClass, label)
        -> (uiClass.getSimpleName()
            + "-" + componentClass.getSimpleName()
            + "-" + label.replace(" ", "-"))
        .toLowerCase(Locale.US);
  }

  static BiFunction<Class, String, String> buttonID() {
    return (uiClass, label) -> genericID().apply(uiClass, Button.class, label);
  }

  static BiFunction<Class, String, String> textfieldID() {
    return (uiClass, label) -> genericID().apply(uiClass, TextField.class, label);
  }

  static BiFunction<Class, String, String> passwordID() {
    return (uiClass, label) -> genericID().apply(uiClass, PasswordField.class, label);
  }
}

Fazit

In diesem Teil haben wir uns damit beschäftigt, wie grundsätzlich Komponenten erzeugt werden können. Schwerpunkt lag hierbei auf der Vorbereitung für komplexere Projekte. Sowohl die Adressierbarkeit als auch die Grundlagen von i18n greifen hier Hand in Hand. In den nächsten Teilen werden wir uns mit den Themen wie i18n und der Komposition von Komponenten beschäftigen. Das Testen von Komponenten und die Verbindung zwischen Logik und UI werden Thema sein. Ebenfalls werden wir uns wesentlich intensiver mit dem Vadin-Framework und seiner Komponenten selbst auseinandersetzen.

Hier schon mal ein Ausblick auf den nächsten Teil:

Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.