4. Januar 2016
Denny Israel
0

JavaFX Persistence – Teil 4 (Async Loading)

Im zweiten Teil haben wir uns angesehen wie man Instanzen von – mit JavaFX Properties versehen – Klassen mittels JPA persistieren kann. Der dritte Teil beschreibt das Nachladen von Daten wenn sie benötigt werden (lazy loading). Trotz lazy loading kann das initiale Laden der Daten einige Zeit beanspruchen, was dem Nutzer als „eingefrorene“ Benutzeroberfläche auffällt.

Bei einfrierenden Oberflächen sind Nutzer in der Regel sehr ungeduldig und die Anwendung selbst macht einen unprofessionellen Eindruck. Um dies zu vermeiden sollten aufwändige Aufgaben oder Ladeoperationen nicht im UI-Thread abgearbeitet werden, sondern asynchron im Hintergrund. Somit kann der UI-Thread sich um die Darstellung der Oberfläche kümmern. Dabei muss man jedoch ein paar Dinge beachten.

Zum einen muss nach dem erfolgreichen Laden der Schritt zurück in die „UI-Thread-Welt“ vollzogen werden, da Änderungen an der Oberfläche nur im UI-Thread durchgeführt werden dürfen. Dies ist die typische Exception, die man sieht, wenn die UI durch einen Background-Thread manipuliert wird:

java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-6

Zum anderen sollte der Nutzer eine Rückmeldung erhalten, dass die Daten geladen werden und er einen Moment warten muss. Controls – die für die Veränderung der Daten vorgesehen sind – sollten deaktiviert werden, sodass keine Inkonsistenzen zwischen Oberflächendaten und gespeicherten Daten entstehen (bspw. indem der Nutzer während des Ladevorgangs einen neuen Eintrag in einer Liste anlegt, den es bereits in der Datenbank gibt).

Für das erste Problem gibt es in JavaFX mehrere Möglichkeiten. Die einfachste ist die Methode Platform#runLater(). Dieser kann man ein Runnable übergeben, welches im UI-Thread ausgeführt wird. Für Aufgaben, die im Hintergrund laufen und zwischendurch und am Ende den Oberflächenzustand ändern, gibt es jedoch auch ein spezielles Konstrukt welches wir uns in der Folge ansehen.

Die Klasse javafx.concurrent.Task bildet die Grundlage für die Ausführung asynchroner Logik in JavaFX-Applikationen. Ähnlich wie Callable hat Task eine call()-Methode, welche in einem anderen Thread ausgeführt wird. Da der Task selbst keine Logik für das ansynchone Ausführen hat und nur einmal ausführbar ist, benötigt man einen javafx.concurrent.Service für dessen Ausführung. Ein Service kann mehrfach verwendet werden und erzeugt bei jeder Ausführung eine neue Instanz von Task. Zu diesem Zweck muss die Methode Service#createTask() implementiert werden. Sowohl Service als auch Task bieten diverse Properties und Callbacks um mit dem Ausführungszustand zu arbeiten und auf entsprechende Ereignisse zu reagieren.

Im Beispiel wird ein OnSucceeded-Callback am Service registriert, um auf das erfolgreiche Ende des Tasks zu warten und die geladenen Daten entsprechend in der Oberfläche anzuzeigen. Dieser Callback hat die Besonderheit, dass er im UI-Thread ausgeführt wird. Dadurch erspart man sich das manuelle Handling per Platform#runLater(). Die call()-Methode des Task hingegen wird, wie erwartet, in einem anderen Thread ausgeführt. Um das Ergebnis zwischen den beiden Welten auszutauschen, hat die call()-Methode des Task einen Rückgabewert, welcher nach erfolgter Rückgabe am Service erfragt werden kann: Service#getValue(). Kombiniert man die Nutzung des OnSucceeded-Callbacks mit dem Zugriff auf den Rückgabewert, erhält man eine elegante Möglichkeit die Ergebnisse asynchroner Aufgaben mit der Oberfläche zu sychronisieren.

package de.saxsys.javafx.persistence;

…
import javafx.concurrent.Service;
import javafx.concurrent.Task;

public class CarView {
 …
 private final CarDb carDb = new CarDb();
 private final ListProperty<Manufacturer> manufacturers = new SimpleListProperty<>(FXCollections.observableArrayList());

 @FXML
 private void initialize() {
  …
  loadManufacturers();
 }

 private void loadManufacturers() {
  final Service<List<Manufacturer>> service = new Service<List<Manufacturer>>() {
   @Override
   protected Task<List<Manufacturer>> createTask() {
    return new Task<List<Manufacturer>>() {
     @Override
     protected List<Manufacturer> call() throws Exception {
      System.out.println("loading manufacturer in " + Thread.currentThread().getName());
      return carDb.readManufacturer();
     }
    };
   }
  };
  service.setOnSucceeded(event -> {
   System.out.println("setting loaded manufacturers in " + Thread.currentThread().getName());
   manufacturers.addAll(service.getValue());
  });
  service.start();
 }
 …
}

Das Codebeispiel zeigt die Instanziierung eines Services, die Instanziierung des auszuführenden Tasks, die Registrierung des OnSucceeded-Callbacks und die Ausführung des Service (Service#start()). Um zu verdeutlichen welcher Thread welchen Code ausführt, wurden Konsolenausgaben an entsprechenden Stellen eingefügt.

Im Beispiel wurde der Callback des Services verwendet. Ob die Callbacks und Properties des Services oder des Tasks verwendet werden, hängt vom Kontext ab. Logik die in Bezug zum Task steht und Informationen über den Ausführungskontext eines bestimmten Tasks benötigt, sollte durch Callbacks des Tasks aufgerufen werden. Code, der allgemeine Anpassungen an der Oberfläche nach Ausführung eines Tasks ausführt (unabhängig von der konkreten Taskinstanz), sollte durch die Callbacks des Services aufgerufen werden.

Da die Ladelogik asynchron ausgeführt wird, bleibt noch das Problem, dass – obwohl die Oberfläche nicht mehr einfriert – der Nutzer aber kein Feedback hat, warum die Listen leer sind und was gerade passiert. Darüber hinaus sind die Bedienelemente weiterhin aktiv, sodass der Nutzer mit der Anwendung schon arbeiten und neue Daten eingeben kann, obwohl das initiale Laden noch nicht abgeschlossen wurde.

In der Beispielapplikation werden die Buttons zum Hinzufügen und Entfernen von Manufacturer- und Car-Instanzen deaktiviert, bis das initiale Laden abgeschlossen wurde. Dazu wird eine neue BooleanProperty loading angelegt, welche an die Property running des Service gebunden wird. Dadurch wird loading immer auf true gesetzt wenn der Service läuft und automatisch auch wieder zurückgesetzt. Die disable-Properties der Buttons wiederum können an loading gebunden werden, wodurch die Buttons immer dann deaktiviert sind, wenn der Service läuft. Die loading-Property im Beispiel existiert, um die Initialisierung der UI-Elemente sauber von der Servicedefinition trennen zu können. Alternativ könnten die disable-Properties der Buttons natürlich auch direkt an die running-Property des Service gebunden werden.

public class CarView {
 @FXML
 private Button btnAddManufacturer;
 @FXML
 private Button btnRemoveManufacturer;
 @FXML
 private Button btnAddCar;
 @FXML
 private Button btnRemoveCar;
 private final CarDb carDb = new CarDb();
 private final BooleanProperty loading = new SimpleBooleanProperty();
 private final ListProperty<Manufacturer> manufacturers = new SimpleListProperty<>(FXCollections.observableArrayList());

 @FXML
 private void initialize() {
  btnAddCar.disableProperty().bind(loading);
  btnAddManufacturer.disableProperty().bind(loading);
  btnRemoveCar.disableProperty().bind(loading);
  btnRemoveManufacturer.disableProperty().bind(loading);

  loadManufacturers();
 }

 private void loadManufacturers() {
  final Service<List<Manufacturer>> service = new Service<List<Manufacturer>>() {
   @Override
   protected Task<List<Manufacturer>> createTask() {
    return new Task<List<Manufacturer>>() {

     @Override
     protected List<Manufacturer> call() throws Exception {
      System.out.println("loading manufacturer in " + Thread.currentThread().getName());
      Thread.sleep(5000);
      return carDb.readManufacturer();
     }
    };
   }
  };
  service.setOnSucceeded(event -> {
   System.out.println("setting loaded manufacturers in " + Thread.currentThread().getName());
   manufacturers.addAll(service.getValue());
  });
  loading.bind(service.runningProperty());
  service.start();
 }
}

Um das Deaktivieren der Buttons erkennen zu können wurde in der call()-Methode ein Thread#sleep() eingebaut, um einen langen Ladevorgang zu simulieren.

Für weitere Callbacks und Properties, die beim Umgang mit asynchroner Logik und der Synchronisierung mit der UI hilfreich sein können, sei hier auf das Javadoc von Service und Task verwiesen.

Abschließend noch eine Quelle zum Thema Service und Task mit weiteren Informationen: https://docs.oracle.com/javafx/2/threads/jfxpub-threads.htm

Das vollständige Beispiel ist auf Github zu finden: https://github.com/sideisra/javafx-persistence/tree/async-loading.

 

Denny ist Senior Consultant für Softwareentwicklung im Bereich Java und JavaFX. Er beschäftigt sich vornehmlich mit allen Aspekten der Entwicklung von JavaFX Anwendungen, von der Oberfläche über die Architektur bis hin zum Backend und der Anbindung an andere Systeme. Darüber hinaus interessiert er sich für moderne Web-Technologien wie React und Angular 2.

Twitter Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0