Skip to content

9. RxJava in der Android-Umgebung

9.1. Introduction

Wir greifen hier eine Anwendung auf, die bereits in mehreren Dokumenten behandelt wurde:

  1. [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (Kapitel 4);
  2. [Introduction à la programmation de tablettes Android par l'exemple] (Kapitel 9);
  3. [Introduction à la programmation de tablettes Android par l'exemple - version 2] (Abschnitt 1.11);

Es handelt sich dabei um eine Client-Server-Anwendung, bei der der Server asynchron Zufallszahlen liefert, die der Android-Client anzeigt:

  • In Dokument 1 verwendet der Android-Client eine nicht standardmäßige Technologie;
  • In Dokument 2 verwendet der Android-Client die Standardtechnologie von Android für asynchrone Vorgänge;
  • In Dokument 3 verwendet der Android-Client dieselbe Technologie wie in Dokument 2, jedoch vereinfacht durch die Verwendung von Annotationen aus der Android-Bibliothek „Annotations“;

Der Android-Client sieht wie folgt aus:

Die Schicht [DAO] kommuniziert mit dem Server, der die vom Android-Tablet angezeigten Zufallszahlen generiert. Dieser Server weist die folgende zweischichtige Architektur auf:

Die Clients fragen bestimmte URL der Schicht [web / JSON] ab und erhalten eine Textantwort im Format JSON (JavaScript Object Notation).

Wir werden die Untersuchung der Anwendung in zwei Schritte unterteilen:

Der Webserver / jSON

  • seine Schicht [métier];
  • ihren mit Spring implementierten Dienst [web / JSON];

Der Android-Client

  • seine Schicht [DAO];
  • seine Aktivität;
  • seine Ansichten;

9.2. Der Webdienst / jSON

Hinweis: Der Webdienst / jSON wird mithilfe der Spring-Technologie MVC implementiert. Leser, die mit dieser Technologie nicht vertraut sind, können entweder:

  • sich damit begnügen, Abschnitt 9.2.1 zu lesen, in dem erklärt wird, wie der Server gestartet und abgefragt wird;
  • das Dokument [Spring MVC et Thymeleaf par l'exemple] konsultieren, insbesondere Kapitel 4, in dem die wichtigsten im Code verwendeten Annotationen vorgestellt werden;

9.2.1. Das IntelliJ IDEA-Projekt

Der Webdienst / jSON weist folgende Architektur auf:

Diese Architektur wird durch das folgende IntelliJ-Idea-Projekt [1] implementiert:

Der Server wird von [2-3] gestartet. Daraufhin werden Protokolle auf der Konsole angezeigt:

2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • Zeile 12: zeigt an, dass der Dienst auf Port 8080 verfügbar ist;
  • Zeile 10: Der einzige Webdienst URL / jSON, der über die Transaktion HTTP GET verfügbar ist. Seine Parameter lauten wie folgt:
    • [a,b]: Intervall für die Erzeugung von Zufallszahlen;
    • [minCount, maxCount]: count Zufallszahlen werden generiert, wobei count eine Zufallszahl im Intervall [minCount, maxCount] ist;
    • [minDelay, maxDelay]: Der Dienst wartet delay Millisekunden, bevor er die angeforderten Zahlen zurückgibt, wobei delay eine Zufallszahl im Bereich [minDelay, maxDelay] ist;

Fordern wir in einem Browser diese URL an:

 

Angefordert wurden:

  • Zufallszahlen im Intervall [100, 200];
  • n Zufallszahlen mit n im Intervall [10, 20];
  • eine Wartezeit von x Millisekunden mit x im Intervall [300, 400];

In der Antwort:

  • aleas: Liste der generierten Zufallszahlen;
  • delay: die vom Server festgelegte Wartezeit in Millisekunden;
  • Fehler: ein Fehlercode – 0, wenn kein Fehler vorliegt;
  • message: eine Fehlermeldung – null, wenn kein Fehler vorliegt;

9.2.2. Die Gradle-Abhängigkeiten des Projekts

  

Das Projekt [serveur] ist ein Gradle-Projekt, das durch die folgende Datei [build.gradle] [1] konfiguriert wird:


// generiert von http://start.spring.io/ (Mai 2016)
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

jar {
  baseName = 'serveur'
  version = '0.0.1-SNAPSHOT'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
}
  • Zeile 1: Ein Kommentar, der angibt, wie diese Konfigurationsdatei generiert wurde;
  • Zeilen 4 und 10: eine Abhängigkeit vom Framework [Spring Boot], einem Zweig des Spring-Ökosystems. Dieses Framework [http://projects.spring.io/spring-boot/] ermöglicht eine minimale Konfiguration von Spring. Anhand der im Classpath des Projekts vorhandenen Dateien leitet [Spring Boot] eine plausible oder wahrscheinliche Konfiguration für das Projekt ab. Befindet sich also Hibernate im Classpath des Projekts, so leitet [Spring Boot] daraus ab, dass die verwendete Implementierung JPA Hibernate sein wird, und konfiguriert Spring entsprechend. Der Entwickler muss dies nicht mehr tun. Er muss dann nur noch die Konfigurationen vornehmen, die [Spring Boot] nicht standardmäßig vorgenommen hat, oder diejenigen, die [Spring Boot] zwar standardmäßig vorgenommen hat, die aber noch präzisiert werden müssen. In jedem Fall hat die vom Entwickler vorgenommene Konfiguration das letzte Wort;
  • Zeilen 14–15: zwei Gradle-Plugins, die erforderlich sind, um den Inhalt dieser Gradle-Datei zu nutzen;
  • Zeilen 17–20: Definieren die Eigenschaften des für dieses Projekt generierten Archivs;
  • Zeilen 22–23: zur Kompatibilität mit Java 8;
  • Zeilen 25–27: Die Abhängigkeiten werden im globalen Maven-Repository oder im lokalen Repository des Rechners gesucht;
  • Zeile 30: Definiert eine Abhängigkeit vom Artefakt [spring-boot-starter-web]. Dieses Artefakt enthält alle für ein Spring-Projekt MVC erforderlichen Archive. Darunter befindet sich auch das Archiv eines Tomcat-Servers. Dieser wird für die Bereitstellung der Webanwendung verwendet. Es ist zu beachten, dass die Version der Abhängigkeit nicht angegeben wurde. Es wird die im importierten Projekt [spring-boot] angegebene Version verwendet;

Um das Projekt zu aktualisieren, muss der Download der Abhängigkeiten [1-3] erzwungen werden:

Sehen wir uns die Abhängigkeiten an, die diese Datei [build.gradle] mit sich bringt:

 

Es sind sehr viele. Spring Boot für das Web hat die Abhängigkeiten bereits integriert, die eine Spring-Webanwendung MVC wahrscheinlich benötigen wird. Das bedeutet, dass einige davon möglicherweise überflüssig sind. Spring Boot eignet sich ideal für ein Tutorial:

  • Es stellt die Abhängigkeiten bereit, die wir wahrscheinlich benötigen werden;
  • wir werden sehen, dass es die Konfiguration des Spring-Projekts MVC erheblich vereinfacht;
  • es enthält einen eingebetteten Tomcat-Server [1], wodurch wir uns die Bereitstellung der Anwendung auf einem externen Webserver sparen;
  • es ermöglicht die Erstellung einer ausführbaren JAR-Datei, die alle oben genannten Abhängigkeiten enthält. Diese JAR-Datei kann ohne Neukonfiguration von einer Plattform auf eine andere übertragen werden.

Auf der Website des Spring-Ökosystems [http://spring.io/guides] finden sich zahlreiche Beispiele für die Verwendung von Spring Boot. Da wir nun die Abhängigkeiten des Projekts kennen, können wir uns dem Code zuwenden.

9.2.3. Die [métier]-Schicht

  

Die Schicht [métier] wird die folgende Schnittstelle [IMetier] haben:


package dvp.rxjava.server.metier;

public interface IMetier {
  // Zufallszahlen im Intervall [a,b]
  // n Zahlen werden generiert, wobei n selbst eine Zufallszahl im Intervall [minCount, maxCount] ist
  // Die Zahlen werden nach einer Wartezeit von delay Millisekunden generiert,
  // wobei [delay] selbst eine Zufallszahl im Intervall [minDelay, maxDelay] ist
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Diese Schnittstelle ist nahezu identisch mit der in Abschnitt 8.4 in der Swing-Umgebung behandelten. In Zeile 8 gibt die Methode [getAleas] den folgenden Typ [AleasMetier] zurück:


package dvp.rxjava.server.metier;

import java.util.List;

public class AleasMetier {
  // Felder
  private int delay;
  private List<Integer> aleas;

  // Konstruktoren
  public AleasMetier(){

  }

  public AleasMetier(int delay, List<Integer> aleas){
    this.delay=delay;
    this.aleas=aleas;
  }

  public AleasMetier(AleasMetier aleasMetier){
    this.delay=aleasMetier.delay;
    this.aleas=aleasMetier.aleas;
  }

  // Getter und Setter
...
}

Der Code der Klasse [Metier], die die Schnittstelle [IMetier] implementiert, lautet wie folgt:


package dvp.rxjava.server.metier;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class Metier implements IMetier {

  @Autowired
  private ObjectMapper mapper;

  @Override
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // Zufallszahlen im Intervall [a,b]
    // n Zahlen werden generiert, wobei n selbst eine Zufallszahl im Intervall [minCount, maxCount] ist
    // Die Zahlen werden nach einer Wartezeit von delay Millisekunden generiert,
    // wobei [delay] selbst eine Zufallszahl im Intervall [minDelay, maxDelay] ist

    // einige Überprüfungen
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // Fehler?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // Zufallszahlengenerator
    Random random = new Random();
    // Warten?
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        String message = null;
        try {
          message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
        } catch (JsonProcessingException e1) {
          throw new AleasException(e1,512);
        }
        throw new AleasException(message, 1024);
      }
    }
    // Ergebnisgenerierung
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // Ergebnisrückgabe
    return new AleasMetier(delay,nombres);
  }

}

Wir gehen nicht näher auf die Klasse ein: Sie entspricht derjenigen, die in der Swing-Umgebung in Abschnitt 8.4 behandelt wurde. Es sei lediglich auf folgende Punkte hingewiesen:

  • Zeile 10: Die Spring-Annotation [@Service] sorgt dafür, dass Spring die Klasse als Singleton instanziiert und ihre Referenz für andere Spring-Komponenten verfügbar macht. Hier hätten auch andere Spring-Annotationen mit demselben Effekt verwendet werden können;
  • Zeilen 13–14: Es wird ein Mapper namens jSON injiziert. Spring ist ein Objektcontainer. Dieser Container wird beim Start der Webanwendung instanziiert, und die in einer Konfigurationsdatei definierten Objekte werden daraufhin instanziiert, standardmäßig als Einzelinstanz (Singleton). Ein Spring-Singleton kann Referenzen auf andere Spring-Objekte enthalten. Dies ist hier der Fall: Das Singleton [metier] (Zeilen 10–11) verfügt über eine Referenz auf das Singleton [mapper] (Zeilen 13–14). Dies wird als Abhängigkeitsinjektion bezeichnet. Es gibt zwei Möglichkeiten, ein Singleton in ein anderes Singleton zu injizieren:
    • über seinen Typ: Dies ist möglich, wenn das einzubindende Singleton das einzige Spring-Objekt dieses Typs ist. Dies ist hier bei der Einbindung in den Zeilen 13–14 der Fall (Typ ObjectMapper);
    • über seinen Namen, wenn mehrere Spring-Objekte denselben Typ haben. In diesem Fall muss die Annotation @Qualifier(„nomDuSingleton“) hinzugefügt werden, um den Namen des Singletons anzugeben;

Die Klasse [Metier] löst Ausnahmen vom Typ [AleaException] aus:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // Fehlercode
  private int code;

  // Konstruktoren
  public AleaException() {
  }

  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }

  // Getter und Setter

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • Zeile 3: [AleasException] erweitert die Klasse [RuntimeException]. Es handelt sich also um eine unkontrollierte Ausnahme (es besteht keine Verpflichtung, sie mit einem try/catch-Block abzufangen);
  • Zeile 6: Der Klasse [RuntimeException] wird ein Fehlercode hinzugefügt;

9.2.4. Der Webdienst / JSON

  

Der Webdienst / JSON wird von Spring MVC implementiert. Spring MVC implementiert das sogenannte MVC-Architekturmodell (Model-View-Controller) wie folgt:

Die Bearbeitung einer Client-Anfrage läuft wie folgt ab:

  1. Anfrage – die angeforderten URL haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Die [Dispatcher Servlet] ist die Spring-Klasse, die eingehende URL verarbeitet. Sie „leitet“ die URL an die Aktion weiter, die sie verarbeiten soll. Diese Aktionen sind Methoden bestimmter Klassen, die als [Contrôleurs] bezeichnet werden. Das „C“ in MVC ist hier die Zeichenfolge [Dispatcher Servlet, Contrôleur, Action]. Wenn keine Aktion zur Verarbeitung des eingehenden URL konfiguriert wurde, antwortet das Servlet [Dispatcher Servlet], dass das angeforderte URL nicht gefunden wurde (Fehler 404 NOT FOUND);
  1. Verarbeitung
  • Die ausgewählte Aktion kann die Parameter parami nutzen, die ihr das Servlet [Dispatcher Servlet] übermittelt hat. Diese können aus verschiedenen Quellen stammen:
    • aus dem Pfad [/param1/param2/...] des URL,
    • aus den Parametern [p1=v1&p2=v2] des URL,
    • aus Parametern, die der Browser mit seiner Anfrage übermittelt hat;
  • Bei der Bearbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die Schichten [metier] und [2b]. Sobald die Anfrage des Clients bearbeitet wurde, kann diese verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
  • die Aktion fordert die Anzeige einer bestimmten Ansicht an: [3]. Diese Ansicht zeigt Daten an, die als Modell der Ansicht bezeichnet werden. Das ist das M in MVC. Die Aktion erstellt dieses Modell M [2c] und fordert eine Ansicht V auf, sich anzuzeigen: [3];
  1. Antwort – die ausgewählte Ansicht V verwendet die von der Aktion erstellte Vorlage M, um die dynamischen Teile der Antwort HTML zu initialisieren, die sie an den Client senden muss, und sendet diese Antwort anschließend.

Bei einem Webservice / JSON wird die vorstehende Architektur leicht modifiziert:

  • In [4a] wird das Modell, bei dem es sich um eine Java-Klasse handelt, durch eine Bibliothek JSON in die Zeichenkette JSON umgewandelt;
  • in [4b] wird diese Zeichenfolge JSON an den Browser gesendet;

Kehren wir zur Schicht [web] unserer Anwendung zurück:

In unserer Anwendung gibt es nur einen Controller:

  

Der Webdienst /JSON sendet seinen Clients eine Antwort vom Typ [AleasResponse] wie folgt:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // Fehlercode
  private int erreur;
  // Fehlermeldung
  private String message;

  // Konstruktoren
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // Getter und Setter

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • Zeile 5: Die Klasse [AleasResponse] erweitert die Klasse [AleasMetier] und übernimmt somit alle deren Attribute (aleas, delay);
  • Zeile 8: ein Fehlercode (0, wenn kein Fehler vorliegt);
  • Zeile 10: bei erreur!=0 eine Fehlermeldung, bei keinem Fehler null;

Der Prüfer [AleasController] lautet wie folgt:


package dvp.rxjava.server.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;

@Controller
public class AleasController {

    // Geschäftslogikschicht
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // Zufallszahlen in [a,b]
    // n Zahlen werden mit n im Intervall [minCount, maxCount] generiert
    // Die Zahlen werden nach einer Wartezeit von delay Millisekunden generiert,
    // wobei [delay] eine Zufallszahl im Intervall [minDelay, maxDelay] ist
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
            @PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
            @PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {

        // die Antwort wird vorbereitet
        AleasResponse response = new AleasResponse();
        // die Geschäftsschicht wird zur Generierung der Zufallszahlen verwendet
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // Fehlerfall (Code und Meldung)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // Die Antwort wird zurückgegeben jSON
        return mapper.writeValueAsString(response);
    }
}
  • Zeile 16: Die Anmerkung [@Controller] macht die Klasse [AleasController] zu einem Spring-Singleton. Sie gibt außerdem an, dass die Klasse Methoden enthält, die Anfragen für bestimmte URL der Webanwendung verarbeiten. Hier gibt es nur eine in Zeile 29;
  • Zeilen 20–21: Die Annotation [@Autowired] weist Spring an, eine Komponente vom Typ [IMetier] in das Feld zu injizieren. Dies ist die zuvor genannte Klasse [Metier]. Da wir dieser die Annotation [@Service] zugewiesen haben, wird sie als Spring-Komponente behandelt;
  • Zeilen 22–23: Die Annotation [@Autowired] weist Spring an, eine Komponente vom Typ [ObjectMapper] in das Feld zu injizieren. Diese werden wir in Kürze definieren;
  • Zeile 31: Die Methode [getAleas] generiert die Zufallszahlen. Ihr Name spielt keine Rolle. Bei ihrer Ausführung wurden die Parameter in den Zeilen 31–33 von Spring MVC initialisiert. Wir werden sehen, wie das funktioniert. Übrigens wird sie nur ausgeführt, weil der Webserver eine Anfrage HTTP GET für das URL aus Zeile 29 (Attribut method);
  • Zeile 30: Die Anmerkung [@ResponseBody] gibt an, dass das Ergebnis der Methode unverändert an den Client gesendet werden soll. Hier senden wir ihm eine Zeichenkette, bei der es sich um die Zeichenkette jSON vom Typ [AleasResponse] handelt;
  • Zeile 29: Die verarbeitete Zeichenfolge „URL“ hat die Form /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, wobei {x} eine Variable darstellt. Diese verschiedenen Variablen werden in den Zeilen 32–33 den Parametern der Methode zugewiesen. Dies geschieht über die Annotation @PathVariable("x"). Es ist zu beachten, dass die Werte von {x} Komponenten einer URL sind und somit vom Typ String sind. Die Konvertierung von String in den Typ der Methodenparameter kann fehlschlagen. Spring MVC löst dann eine Ausnahme aus. Fassen wir zusammen: Wenn ich mit einem Browser den URL /100/200/10/20/300/400 anfordere, wird die Methode getAleas in Zeile 31 mit den Parametern a=100 (Zeile 31), b=200 (Zeile 31), minCount=10 (Zeile 31), maxCount=20 (Zeile 32), minDelay=300 (Zeile 32), maxDelay=400 (Zeile 33);
  • Zeile 39: Von der Ebene [métier] wird eine Liste von Zufallszahlen angefordert. Es sei daran erinnert, dass die Methode [metier].getAleas eine Ausnahme auslösen kann;
  • Zeilen 42–43: Fehlerfall;
  • Zeile 46: Die Antwort vom Typ [AleasResponse] wird als Zeichenkette jSON zurückgegeben;

9.2.5. Konfiguration des Spring-Projekts

  

Es gibt verschiedene Möglichkeiten, Spring zu konfigurieren:

  • mit XML-Dateien;
  • mit Java-Code;
  • mit einer Kombination aus beidem;

Wir entscheiden uns dafür, unsere Webanwendung mit Java-Code zu konfigurieren. Diese Konfiguration wird durch die oben genannte Klasse [Config] gewährleistet:


package dvp.rxjava.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
  // -------------------------------- Konfiguration der Schicht [web]
  @Autowired
  private ApplicationContext context;

  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // Mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • Zeile 15: Wir teilen Spring mit, in welchen Paketen es Objekte zum Instanziieren finden wird. Es wird zwei finden:
    • die Klasse [Metier], die mit [@Service] annotiert ist;
    • die Klasse [AleasController], die mit [@Controller] annotiert ist;
  • Zeile 16: Die Annotation [@EnableWebMvc] führt zu automatischen Konfigurationen für das Spring-Framework MVC;
  • Zeilen 19–20: Injektion des Spring-Kontexts (Spring-Objekt-Container). Diese Injektion ist erforderlich, da das Objekt in den Zeilen 22–26 darauf angewiesen ist;
  • Die Spring-Konfigurationsdatei kann mithilfe von mit [@Bean] annotierten Methoden neue Spring-Objekte definieren. Das Ergebnis der Methode wird dann zu einem Spring-Objekt;
  • Zeilen 22–26: Definition des Spring-Framework-Servlets MVC, das die Anfragen HTTP an den richtigen Controller und die richtige Methode weiterleitet. [DispatcherServlet] ist eine Spring-Klasse;
  • Zeilen 28–31: Hier wird festgelegt, dass dieses Servlet alle URL-Anfragen verarbeitet;
  • Zeilen 33–36: Das Vorhandensein dieser Bean aktiviert den Tomcat-Server, der sich im Projektarchiv befindet. Er wartet auf Anfragen am Port 8080;
  • Zeilen 39–42: Ein Mapper jSON. Dieser wurde in die Spring-Objekte [Metier] und [AleasController] injiziert;

9.2.6. Ausführung des Webservers

  

Das Projekt wird über die folgende ausführbare Klasse [Application] ausgeführt:


package android.exemples.server.boot;

import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;

public class Application {
  public static void main(String[] args) {
    // Anwendung ausführen
    SpringApplication.run(Config.class, args);
  }

}
  • Zeile 6: Die Klasse [Application] ist eine ausführbare Klasse (Zeilen 7–10);
  • Zeile 9: Die statische Methode [SpringApplication.run] ist eine Methode von [spring Boot] (Zeile 4), die die Anwendung startet. Ihr erster Parameter ist die Java-Klasse, die das Projekt konfiguriert. Hier handelt es sich um die Klasse [Config], die wir gerade beschrieben haben. Der zweite Parameter ist das Argument-Array, das an die Methode [main] übergeben wird (Zeile 7). In diesem Fall gibt es keine Argumente;

Für die eigentliche Ausführung wird der Leser gebeten, zu Abschnitt 9.2.1 zurückzukehren.

9.3. Der Android-Client

Hinweis: Das folgende Android-Projekt ist recht komplex. Es erfordert fundierte Android-Kenntnisse, die beispielsweise in [Introduction à la programmation de tablettes Android avec Android Studio ] zu finden sind.

Aktivität

Ansichten

Ebene

[DAO]

Benutzer

Server

Der Client verfügt über zwei Komponenten:

  1. eine Schicht [Présentation] (Ansichten + Aktivität);
  2. eine Schicht [DAO], die den zuvor behandelten Dienst [web / JSON] anspricht.

9.3.1. RxAndroid

Um asynchron mit dem Zufallszahlengenerator zu kommunizieren, nutzt der Android-Client die Bibliothek RxAndroid. Diese erweitert RxJava auf die Android-Umgebung. Wie bereits bei der Swing-Anwendung werden wir nur eine einzige von RxAndroid bereitgestellte Erweiterung nutzen, nämlich den Scheduler [AndroidSchedulers.mainThread()]. Eine Android-Benutzeroberfläche folgt denselben Prinzipien wie eine Swing-Oberfläche:

  • Ereignisse werden in einem einzigen Thread verarbeitet, der als „Event Loop“ oder „UI-Thread“ bezeichnet wird;
  • wenn ein Ereignis asynchrone Aktionen auslöst, müssen deren Ergebnisse im UI-Thread abgerufen werden, sofern sie zur Aktualisierung der Benutzeroberfläche dienen sollen;

Der Android-Client:

  • wird mehrere asynchrone Anfragen an den Zufallszahlengenerator senden. Diese Anfragen werden auf der Client-Seite mit den Threads des Schedulers [Schedulers.io()] ausgeführt;
  • diese asynchronen Anfragen liefern Observables, die zu einem einzigen zusammengefasst werden (Merge);
  • dieses Observable wird auf der Client-Seite im Scheduler [AndroidSchedulers.mainThread()] beobachtet, der von RxAndroid bereitgestellt wird;

9.3.2. Das IntelliJ IDEA-Projekt

Das Android-Projekt heißt [client]:

Es wird über [2] ausgeführt.

Hinweis: Die Ausführung hängt stark von der Konfiguration der verwendeten IntelliJ Idea-Version ab. Es ist wahrscheinlich, dass die oben genannte Ausführung [2] auf einem anderen Rechner als meinem eigenen nicht auf Anhieb funktioniert. Die korrekte Konfiguration von IntelliJ IDEA zur Ausführung dieses Projekts kann für Anfänger eine große Herausforderung darstellen. Hier sind einige Punkte, auf die Sie achten sollten:

  • Rufen Sie in [3] die Projektstruktur auf;
  • in [4-5], JDK und die auf meinem Rechner vorhandenen Android-Versionen SDK. Beachten Sie, dass JDK 1.8 nicht unbedingt erforderlich ist. Android unterstützt bestimmte Funktionen von Java 8 nicht, darunter Lambda-Ausdrücke. Um funktionale Schnittstellen zu instanziieren, verwenden wir daher anonyme Klassen. Ein JDK 1.6 ist daher ausreichend. Das Projekt wurde jedoch in der bereitgestellten Form mit einem JDK 1.8 konfiguriert;

Die Datei [build.gradle] [6], die das Android-Projekt konfiguriert, lautet wie folgt:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // durch die aktuelle Version des Android-Plugins ersetzen
    classpath 'com.android.tools.build:gradle:1.5.0'
  }
}
apply plugin: 'com.android.application'
dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
  jcenter()
}
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "android.aleas"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_6
    targetCompatibility JavaVersion.VERSION_1_6
  }
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

Je nach den vorhandenen Android-SDK-Dateien müssen die Versionen in den Zeilen 8, 24–25 und 29 möglicherweise angepasst werden.

Um neue Android-SDK zu installieren, verwenden Sie den SDK-Manager wie folgt:

Das Projekt wurde für Folgendes konfiguriert:

  • SDK, API, 23, [2];
  • SDK Build-Tools 23.0.3 [3];
  • SDK Tool 25.1.3 [4]

Überprüfen Sie abschließend den Pfad für SDK Android in der Datei [local.properties] [4], Zeile 11 unten:


## Diese Datei wird automatisch von Android Studio generiert.
# Diese Datei darf nicht geändert werden -- YOUR CHANGES WILL BE ERASED!
#
# Diese Datei darf *NOT* nicht in Versionskontrollsysteme eingecheckt werden,
# da sie Informationen enthält, die spezifisch für Ihre lokale Konfiguration sind.
#
# Speicherort der Datei SDK. Diese wird nur von Gradle verwendet.
# Für Anpassungen bei der Verwendung eines Versionskontrollsystems lesen Sie bitte den
# Kopfzeile.
#Do, 07. Apr. 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Ausführung des IntelliJ IDEA-Projekts

Sobald eine korrekte Umgebung für das Projekt erstellt wurde, kann dieses wie folgt ausgeführt werden:

  • Bei [1] wird der Android-Emulator Genymotion gestartet;
  • mit [2] wird die Ausführungskonfiguration [app] ausgeführt;
  • in [3], um eine Ausführungskonfiguration zu erstellen;
 
  • In [1, 3] wurde die Konfiguration als [app] benannt;
  • in [2], sie entspricht der Ausführung des Moduls mit dem Namen [app];
  • Bei [4] wird festgelegt, dass bei der Ausführung das Modul IDE ein Ausführungsgerät vorschlägt. Hier handelt es sich immer um den Genymotion-Emulator;
  • In [5] wird festgelegt, dass dieses Gerät für alle Ausführungen der Konfiguration beibehalten werden soll;

Die Ausführung des Projekts auf dem Genymotion-Emulator beginnt mit dem folgenden Startbefehl:

Image

Um zu erfahren, was in [1] einzutragen ist, öffnen Sie ein Befehlsfenster DOS und geben Sie den folgenden Befehl [ipconfig] ein:


C:\Program Files\Console2>ipconfig

Configuration IP de Windows


Carte Ethernet Ethernet :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr

Carte réseau sans fil Connexion au réseau local* 3 :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Carte Ethernet VirtualBox Host-Only Network :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte Ethernet Ethernet 2 :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1


Geben Sie in [1] eine der Adressen IP Ihres Computers ein (Zeilen 20, 28, 32). Wenn Sie eine Windows-Firewall verwenden, müssen Sie diese wahrscheinlich deaktivieren, damit der Android-Emulator den Zufallszahlengenerator erreichen kann.

Die Ausführung der asynchronen Abfragen mit den oben genannten Informationen liefert folgende Ergebnisse:

Image

Jede Anfrage führt zu einer Antwort jSON mit den folgenden Feldern:

  • aleas: die vom Server generierten Zufallszahlen;
  • idClient: die Nummer der Anfrage;
  • on: der Ausführungs-Thread der Anfrage auf der Client-Seite;
  • requestAt: Zeitpunkt der Anfrage;
  • responseAt: Zeitpunkt des Empfangs der Antwort;
  • delay: die Wartezeit, die der Server eingehalten hat, bevor er seine Antwort zurückgesendet hat;
  • Fehler: ein Fehlercode – 0, wenn kein Fehler vorliegt;
  • message: eine Fehlermeldung – null, wenn kein Fehler vorliegt;
  • observedAt: Zeitpunkt der Beobachtung der Antwort;
  • observedOn: Thread, in dem die Antwort beobachtet wurde. Hier ist dies immer [main], was den Thread der Benutzeroberfläche bezeichnet;

Da die Anfragen asynchron sind und die dem Server auferlegten Wartezeiten zufällig sind, kommen die Antworten in ungeordneter Reihenfolge zurück.

9.3.4. Die Gradle-Abhängigkeiten des Projekts

Das Projekt benötigt Abhängigkeiten, die wir in die Datei [app / build.gradle] eintragen:

  

dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
  • Die Abhängigkeiten in den Zeilen 2–3 sind Standardabhängigkeiten eines Android-Projekts mit der Datei SDK 23;
  • Die Abhängigkeit der Zeile 5 führt zum Spring-Objekt [RestTemplate], das den Dialog der Ebene [DAO] mit dem Server verwaltet;
  • Die Abhängigkeit in Zeile 6 führt zur Bibliothek JSON [Jackson], die von der Anwendung verwendet wird;
  • Die Abhängigkeit in Zeile 7 führt zur Bibliothek RxAndroid (und damit auch zur Bibliothek RxJava), die die Ui-Schicht für die Kommunikation mit der Schicht [DAO] verwendet;

9.3.5. Das Manifest der Android-Anwendung

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="android.aleas">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name="android.aleas.activity.MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • Zeile 5: Der Internetzugriff muss erlaubt sein;

9.3.6. Die Schicht [DAO]

 

9.3.6.1. Die Schnittstelle [IDao] der Schicht [DAO]

Die Schnittstelle der Schicht [DAO] sieht wie folgt aus:


package android.aleas.dao;

import android.aleas.fragments.Request;
import rx.Observable;

public interface IDao {

  // Zufallszahlen im Intervall [a,b]
  // n Zahlen werden generiert, wobei n selbst eine Zufallszahl im Intervall [minCount, maxCount] ist
  // Die Zahlen werden nach einer Wartezeit von delay Millisekunden generiert,
  // wobei [delay] selbst eine Zufallszahl im Intervall [minDelay, maxDelay] ist
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL des Webdienstes
  public void setUrlServiceWebJson(String url);

  // maximale Wartezeit (ms) für die Antwort des Servers auf eine Verbindungsanfrage
  // Maximale Wartezeit (ms) für die Antwort des Servers auf eine Anfrage
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • Zeile 12: die Methode der Schicht [DAO], die die Zufallszahlen asynchron liefert;
  • Zeile 15: um der Implementierung [DAO] den URL des Zufallszahlengenerierungsdienstes mitzuteilen;
  • Zeile 19: um in der Implementierung [DAO] maximale Wartezeiten festzulegen, um eine zu lange Wartezeit zu vermeiden, wenn der Server nicht antwortet;

Die Methode [getAleas] erhält alle ihre Parameter im folgenden Objekt [Request]:


package android.aleas.fragments;

public class Request {

  // Anfragenummer
  int id;
  // Benutzereingaben
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // Konstruktoren
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // Getter und Setter
...
}

Hier erkennt man die meisten Parameter des Objekts URL des abzufragenden Servers.

Die Methode [getAleas] gibt einen Typ „Observable<AleasDaoResponse>“ zurück, wobei die Klasse [AleasDaoResponse] wie folgt aussieht:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // Fehlercode
  private int erreur;
  // Fehlermeldung
  private String message;
  // Server-Zeitlimit
  private int delay;
  // vom Server ausgegebene Zufallszahlen
  private List<Integer> aleas;
  // Client-Status
  private ClientState clientState;

  // Konstruktoren

  public AleasDaoResponse() {
  }

  public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
    this.erreur = erreur;
    this.message = message;
    this.delay = delay;
    this.aleas = aleas;
    this.clientState = clientState;
  }

  // Getter und Setter
...
}

Der Typ [ClientState] lautet wie folgt:


package android.aleas.dao;

import org.codehaus.jackson.map.annotate.JsonFilter;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class ClientState {

  // Name des Ausführungsthreads
  private String on;
  // Zeitpunkt der Anfrage
  private String requestAt;
  // Zeitpunkt der Antwort
  private String responseAt;
  // Client-ID
  private int idClient;

  // Konstruktor
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }

  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }

  // private Methoden

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // Getter und Setter
...
}
  • Zeile 11: Ausführungsthread der Schicht [DAO];
  • Zeile 13: Zeitpunkt der Anfrage;
  • Zeile 15: Zeitpunkt der Antwort;
  • Zeile 17: Nummer der Anfrage;

Die Felder [on, requestAt, idClient] werden vom Client zu Beginn der Anfrage initialisiert. Das Feld [responseAt] wird initialisiert, wenn der Client die Antwort vom Server erhält.

9.3.6.2. Implementierung der Schicht [DAO]

  

Die Schnittstelle [IDao] wird mit der folgenden Klasse [Dao] implementiert:


package android.aleas.dao;

import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class Dao implements IDao {

  // Client REST
  private RestTemplate restTemplate;
  // URL Dienst
  private String urlServiceWebJson;

  // Mapper jSON
  private ObjectMapper mapper;

  // Konstrukteure
  public Dao() {
    // Mapper jSON
    mapper = new ObjectMapper();
  }

  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // Der URL des Dienstes REST wird festgelegt
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • Zeile 22: das Objekt [RestTemplate], das den Dialog mit dem Zufallszahlengenerator sicherstellt;
  • Zeile 24: das Objekt URL des Generierungsdienstes – wird durch die Methode [setUrlServiceWebJson] in Zeile 41 festgelegt;
  • Zeile 27: der Mapper jSON, der zur Deserialisierung der vom Zufallszahlenserver gesendeten Zeichenkette jSON dient;
  • Zeilen 30–33: der Konstruktor der Klasse;
  • Zeile 32: Der Mapper jSON aus Zeile 27 wird erstellt;

Die Methode [setClientTimeouts] lautet wie folgt:


  // Client REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // Das Timeout für die Anfragen des Kunden REST wird festgelegt
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • Der Dialog zwischen dem Client und dem Webserver / JSON wird durch das Objekt [RestTemplate] aus Zeile 2 gewährleistet. Bislang haben wir es noch nicht initialisiert. Dies erfolgt durch die Methode [setClientTimeouts];
  • Zeile 8: Die Klasse [HttpComponentsClientHttpRequestFactory] wird durch die Abhängigkeit [spring-android-rest-template] bereitgestellt. Mit ihr können wir die maximalen Wartezeiten für die Antwort des Servers festlegen (Zeilen 9–10);
  • Zeile 11: Wir erstellen das Objekt vom Typ [RestTemplate], das als Grundlage für die Kommunikation mit dem Webdienst dient. Als Parameter übergeben wir ihm das soeben erstellte Objekt [factory];
  • Zeile 12: Der Dialog zwischen Client und Server kann verschiedene Formen annehmen. Der Austausch erfolgt über Textzeilen, und wir müssen dem Objekt vom Typ [RestTemplate] mitteilen, was es mit dieser Textzeile tun soll. Dazu stellen wir ihm Konverter zur Verfügung, also Klassen, die Textzeilen verarbeiten können. Die Auswahl des Konverters erfolgt in der Regel über die Header HTTP, die die Textzeile begleiten. Anhand dieser Header wählt das Objekt [RestTemplate] aus seinen Konvertern denjenigen aus, der für die jeweilige Situation am besten geeignet ist. In diesem Fall gibt es nur einen einzigen Konverter, nämlich einen String-zu-String-Konverter, was bedeutet, dass der vom Server empfangene Typ String keiner Umwandlung unterzogen wird.

Die Methode [getAleas] ist die komplexeste Methode:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // Ausführung des Dienstes
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL des Dienstes: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
          String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
            urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
            request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
          // Kundeninformationen
          ClientState clientState = new ClientState(request.getId());
          // synchrone HTTP-Anfrage
          String response = executeRestService("get", urlService, null);
          // Deserialisierung der Antwort jSON vom Server
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // Fehler?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // Die Ausnahme wird weitergeleitet
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // Empfangszeit wird protokolliert
            clientState.setResponseAt();
            // Das Ergebnis wird an den Teilnehmer weitergeleitet
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // Die Ausnahme wird an den Abonnenten weitergeleitet
          subscriber.onError(ex);
        } finally {
          // Das Ende des Observables wird gemeldet
          // Bei der Ausführung stellt man fest, dass diese Methode keine Wirkung hat, wenn zuvor die Methode [onError] aufgerufen wurde – was der Theorie entspricht – daher könnte man diese Anweisung ausschließlich im „try“-Block platzieren
          subscriber.onCompleted();
        }
      }
    });
  }
  • Zeile 2: Man muss daran denken, dass ein Typ [Observable<AleasResponse>] erzeugt werden muss;
  • Zeile 3: eine Protokollzeile auf der Android-Konsole;
  • Zeile 5: Das Objekt [RestTemplate] gewährleistet einen synchronen Dialog mit dem Server. Das bedeutet, dass der Ausführungs-Thread, der die Anfrage stellt, bis zum Empfang der Antwort blockiert ist. Im Swing-Beispiel haben wir gesehen, wie man mithilfe der Methode [Observable.create] eine synchrone Aktion in eine asynchrone umwandeln kann. Diesen Weg verfolgen wir auch hier;
  • Zeile 7: die Methode [call] der Schnittstelle [Observable.OnSubscribe<AleasDaoResponse>] aus Zeile 5. Diese Methode wird aufgerufen, wenn ein Beobachter das Beobachtbare abonniert;
  • Zeilen 10–12: Erstellung des URL des Zufallszahlengenerators;
  • Zeile 14: Initialisierung des Objekts [ClientState]. Hier wird der Zeitpunkt der Anfrage notiert;
  • Zeile 16: Synchrone Anfrage HTTP. Man erhält eine Antwort jSON. Die Methode [executeRestService] erwartet drei Parameter:
      1. die Methode HTTP, die zur Abfrage des Dienstes verwendet werden soll;
      2. die URL des Dienstes;
      3. das zu sendende Objekt vom Typ Object, null, falls die Methode HTTP nicht POST ist;
  • 18–21: Deserialisierung der empfangenen Zeichenfolge jSON in einen Typ [AleasServerResponse]. Dieser Typ lautet wie folgt:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // Fehlercode
  private int erreur;
  // Fehlermeldung
  private String message;
  // Server-Zeitüberschreitung
  private int delay;
  // Zufallszahlen
  private List<Integer> aleas;

  // Getter und Setter
...
}
  • Zeile 23: Der vom Server gesendete Fehlercode wird abgerufen;
  • Zeilen 24–26: Im Fehlerfall wird eine Ausnahme an den Teilnehmer weitergeleitet;
  • Zeile 29: [clientState] wird aktualisiert, das Teil der an den Teilnehmer gesendeten Antwort sein wird;
  • Zeilen 31–33: Versand der Antwort an den Teilnehmer. Sie hat das Format [AleasDaoResponse];
  • Zeilen 35–37: Behandeln alle Fehlerfälle unterschiedslos. Der wahrscheinlichste Fehler ist ein Netzwerkfehler;
  • Zeile 41: Benachrichtigung über das Ende der Übertragung;

9.3.7. Die Ansichten der Anwendung

  

Die Anwendung verfügt über die folgenden beiden Ansichten:

Die Ansicht der Anfrage

Image

Die Antwortansicht

Image

9.3.7.1. Die Klasse [MyFragment]

Es gibt zwei Fragmente:

  • [RequestFragment] für die Anfrage;
  • [ResponseFragment] für die Antwort;

Beide Fragmente erweitern die folgende Klasse [MyFragment]:


package android.aleas.fragments;

import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;

public abstract class MyFragment extends Fragment {

  // ------------- Gemeinsame Daten der Fragmente
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • Zeile 7: Die Klasse [MyFragment] erweitert die Android-Klasse [Fragment];
  • Zeilen 10–11: die für alle Fragmente gemeinsamen Daten;
  • Zeile 10: Jedes Fragment kennt die einzige Aktivität der Anwendung;
  • Zeile 11: Zur Kommunikation untereinander nutzen die Fragmente eine Sitzung;
  • Zeile 13: Bevor ein Fragment angezeigt wird, wird es aufgefordert, sich mit dem Inhalt der Sitzung zu aktualisieren. Diese Methode ist als abstrakt deklariert, da sie von den untergeordneten Klassen implementiert wird. Aus diesem Grund ist die Klasse selbst als abstrakt deklariert (Zeile 7);

Die Klasse [Session] enthält die Daten, die sich die verschiedenen Fragmente der Anwendung gemeinsam nutzen. Ihr Code lautet wie folgt:

  

package android.aleas.activity;

import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;

public class Session {

  // Anwendungsaktivität
  private MainActivity activity;
  // Anzahl der Anfragen
  private int nbRequests;
  // Anfrageeigenschaften
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL Webdienst / jSON
  private String urlWebJson;
  // Vorgang gestartet
  private boolean onAir;
  // dasselbe, jedoch etwas später
  private boolean operationStarted;
  // Der vom Benutzer aus der Beispielliste ausgewählte Name
  private String exampleName;
  // seine Nummer in der Fragmentliste
  private int examplePosition;
  // der Spinner-Adapter aus den Beispielen in der Abfrageansicht
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // Methoden
  public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
    this.urlWebJson = urlWebJson;
    this.exampleName = exampleName;
    this.examplePosition = examplePosition;
  }

  public Request getRequest() {
    return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
  }

  // Getter und Setter
...
}

Mit der Methode in Zeile 46 lässt sich das Objekt [Request] erstellen, das alle vom Benutzer in der Abfrageansicht eingegebenen Informationen kapselt:

  

package android.aleas.fragments;

public class Request {

  // Abfragenummer
  int id;
  // Benutzereingaben
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // Konstruktoren
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // Getter und Setter
....
}

9.3.7.2. Das Fragment [RequestFragment] der Abfrage

Das Fragment der Abfrage enthält folgende Komponenten:

Image

Die Anwendung verfügt über eine einzige Ansicht, die aus zwei Registerkarten besteht:

  • [1]: die Registerkarte der Anfrage;
  • [2]: die Registerkarte „Antwort“;

Die Komponenten des Fragments [RequestFragment] sind folgende:

Nr.
Typ
Name
Rolle
3
EditText
edtNbRequests
Anzahl der Anfragen an den Zufallszahlengenerator
4
EditText
edtA, edtB
die Grenzen [a,b] des Intervalls für die Zahlengenerierung;
5
EditText
edtMinCount, edtMaxCount
Der Dienst generiert die Zahl count, wobei count eine Zufallszahl im Intervall [minCount, maxCount] ist
6
EditText
edtMinDelay, edtMaxDelay
Der Dienst wartet delay Millisekunden, bevor er die Zahlen generiert, wobei delay eine Zufallszahl im Intervall [minDelay, maxDelay] ist
7
EditText
edtUrlServiceRest
URL des Zufallszahlengenerators;
8
Spinner
spinnerExemples
die Dropdown-Liste mit den Beispielen. Jedes Beispiel veranschaulicht eine bestimmte Methode der Klasse [Observable];
8
Button
btnExecuter
die Schaltfläche, die die Aufrufe an den Dienst zur Zahlengenerierung auslöst;

Eingabefehler werden gemeldet:

Image

Die Komponenten 1 bis 6 sind [TextView]-Komponenten mit den folgenden Namen (in dieser Reihenfolge): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. Das Fragment [ResponseFragment] der Antwort

Das Fragment der Antwort enthält folgende Komponenten:

Image

Nr.
Typ
Name
Rolle
1
TextView
infoReponses
Anzahl der eingegangenen Antworten
2
ListView
listReponses
Liste der vom Server empfangenen Kanäle jSON
3
Schaltfläche
btnAnnuler
zum Abbrechen der Anfragen an den Server

9.3.7.4. Die Android-Aktivität [MainActivity]

  

Die Klasse [MainActivity] zeigt die folgende Ansicht [] an:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context="android.arduinos.ui.activity.MainActivity">

  <!-- Anwendungsleiste -->
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <!-- Symbolleiste -->
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">

      <!-- Ladebild -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
    </android.support.v7.widget.Toolbar>

    <!-- Registerkarten-Container -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- Ansichtscontainer -->
  <android.aleas.activity.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:layout_marginBottom="100dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

Die Komponenten dieser Ansicht sind folgende:

Zeilen
Typ
Name
Rolle
20–34
Symbolleiste
Toolbar
Symbolleiste der Anwendung
29–34
ProgressBar
loadingPanel
Ladebild, das angezeigt wird, solange die Anfrage des Benutzers bearbeitet wird
37–40
TabLayout
Registerkarten
die Registerkartenleiste der Anwendung
44–51
MyPager
Container
der Container, in dem die verschiedenen Fragmente der Anwendung angezeigt werden

Die Klasse [MyPager] lautet wie folgt:


package android.aleas.activity;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // Steuert das Wischen
  private boolean isSwipeEnabled;

  // Konstruktoren
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // Methoden-Umschreibung
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // Wischen erlaubt?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Wischen erlaubt?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // Setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • Die Klasse [MyPager] erweitert die Android-Standardklasse [ViewPager]. Die Klasse [MyPager] wird anstelle der Klasse [ViewPager] verwendet, ausschließlich um das Wischen zu unterbinden: Standardmäßig kann man mit der Klasse [ViewPager] durch Wischen (durch Wischen nach links oder rechts). Dieses Verhalten soll hier jedoch nicht auftreten;
  • Zeile 11: Der boolesche Wert, der das Wischen steuert (Zeilen 26 und 36);
  • Zeilen 44–46: die Methode, mit der das Feld aus Zeile 11 initialisiert wird;

Das Grundgerüst der Android-Aktivität [MainActivity] sieht wie folgt aus:


package android.aleas.activity;

import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;

public class MainActivity extends AppCompatActivity implements IDao {

  // Schicht [DAO]
  private IDao dao;
  // die Sitzung
  private Session session;

  // Konstruktor
  public MainActivity() {
    // Elternteil
    super();
    // Sitzung
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // Getter

  public Session getSession() {
    return session;
  }

  // Implementierung IDao ----------------------------------------
  @Override
  public Observable<AleasDaoResponse> getAleas(Request request) {
    return dao.getAleas(request);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    dao.setClientTimeouts(connectTimeout, readTimeOut);
  }

}
  • Zeile 21: Die Klasse [MainActivity] erweitert die Android-Standardklasse [AppCompatActivity]. Es handelt sich also um eine Standard-Android-Aktivität;
  • Zeile 21: Die Klasse [MainActivity] implementiert die Schnittstelle [IDao];

Kehren wir zur Architektur der Anwendung zurück:

Die Tatsache, dass die Aktivität die Schnittstelle der Schicht [DAO] implementiert, ermöglicht es den Ansichten, die Schicht [DAO] nicht zu kennen: Ihre Ereignisbehandler wenden sich an die Schicht [activité], wenn sie mit dem Server kommunizieren möchten.

  • Zeile 24: ein Verweis auf die Schicht [DAO], initialisiert durch den Konstruktor in Zeile 35;
  • Zeile 26: eine Referenz auf die von den Fragmenten gemeinsam genutzte Sitzung, die vom Konstruktor in Zeile 33 initialisiert wurde;
  • Zeilen 46–59: Implementierung der Schnittstelle [IDao];

Die Klasse [MainActivity] initialisiert die Komponenten der ihr zugeordneten Ansicht wie folgt:


  // Symbolleiste
  private Toolbar toolbar;
  // Fragment-Manager
  private MyPager mViewPager;
  // Registerkarten-Container
  private TabLayout tabLayout;
  // Ladebild
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // klassisch
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Sitzung
    session.setActivity(this);
    // Konfiguration der Timeouts der Schicht [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

    // Komponenten
    mViewPager = (MyPager) findViewById(R.id.container);
    toolbar = (Toolbar) findViewById(R.id.toolbar);
    loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
    tabLayout = (TabLayout) findViewById(R.id.tabs);

    // Symbolleiste
    setSupportActionBar(toolbar);

    // Zu Beginn gibt es nur eine Registerkarte
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // Ereignismanager
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // Eine Registerkarte wurde ausgewählt – das vom Fragment-Container angezeigte Fragment wird geändert
        int position = tab.getPosition();
        if (position == 0) {
          // Registerkarte „Abfrage“
          showView(0);
        } else {
          // Registerkarte „Antwort“ – hängt vom gewählten Beispiel ab
          showView(session.getExamplePosition());
        }
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
    });

    // Erstellung der Antwortfragmente
    createResponseFragments();

    // Verwaltung des Ladebildes
    loadingPanel.setVisibility(View.INVISIBLE);
}

Dieser Code ist in einer Aktivität recht typisch. Lassen Sie uns einige Punkte näher erläutern:

  • Zeile 19 verweist auf die folgende Klasse [Constants]:

package android.aleas.activity;

abstract public class Constants {

  final static public int VUE_REQUEST = 0;
  final static public int VUE_RESPONSE = 1;
  final static public int CONNECT_TIMEOUT = 1000;
  final static public int READ_TIMEOUT = 6000;
  final static public int DELAY_MAX = 5000;
  final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
  • Zeilen 31–33: Hier wird die erste Registerkarte mit dem Titel [Request] erstellt. Zu einem bestimmten Zeitpunkt befinden sich im Speicher:
    • das Fragment [Request];
    • n Fragmente vom Typ [ExampleXXFragment];

Die erste Registerkarte zeigt immer das Fragment [Request] an. Die zweite Registerkarte zeigt das Fragment [ExampleXXFragment] an, das dem vom Benutzer ausgewählten Beispiel entspricht. Das von der zweiten Registerkarte angezeigte Fragment ändert sich also im Laufe der Zeit;

  • Zeilen 37–48: Der Code, der ausgeführt wird, wenn der Benutzer auf eine der Registerkarten klickt;
  • Zeile 43: Das Fragment Nr. 0 wird angezeigt;
  • Zeile 46: Das derzeit verwendete (angezeigte) Fragment wird angezeigt. Seine Nummer wird in der Sitzung ermittelt;
  • Zeile 62: Es werden die Fragmente aller Beispiele erstellt, die im Beispiel-Spinner der Ansicht [RequestFragment] (1. Registerkarte) vorhanden sind;
  • Zeile 65: Das Ladebild wird vorerst ausgeblendet;

Um die Methode [showView] (Zeilen 43, 46) und die Methode [createResponseFragments] zu verstehen, müssen wir zunächst den Fragment-Manager im Speicher vorstellen (Klasse enthalten in der Java-Datei von MainActivity):


  // Der Fragment-Manager – muss die Methoden getItem und getCount definieren
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // Verwaltete Fragmente
    private MyFragment[] fragments;

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }

    // muss das Fragment an Position Nr. zurückgeben
    @Override
    public MyFragment getItem(int position) {
      // das Fragment
      return fragments[position];
    }

    // gibt die Anzahl der zu verwaltenden Fragmente zurück
    @Override
    public int getCount() {
      // Anzahl der Fragmente
      return fragments.length;
    }
  }
}
  • Die Klasse [SectionsPagerAdapter] erweitert die Android-Klasse [FragmentPagerAdapter]. Sie definiert zwei Methoden ihrer übergeordneten Klasse neu:
    • die Methode [getItem], Zeile 15;
    • die Methode [getCount], Zeile 22;
  • Die Klasse [SectionsPagerAdapter] enthält alle Fragmente der Anwendung. Diese werden in Zeile 5 gespeichert. Es sei angemerkt, dass sie vom Typ [MyFragment] sind, der in Abschnitt 9.3.7.1 vorgestellt wurde;
  • Zeile 8: Zur Erstellung erhält die Klasse [SectionsPagerAdapter] die Fragmente, die sie verwalten soll;
  • Zeilen 14–18: Die Methode [getItem] gibt das Fragment an der Position [position] zurück;
  • Zeilen 21–25: Die Methode [getCount] gibt die Gesamtzahl der Fragmente zurück;

Die Methode [createResponseFragments] erstellt alle Fragmente, die die Anwendung benötigt:


private void createResponseFragments() {
    // Beispiel-Spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Das Layout angeben, das verwendet werden soll, wenn die Auswahlliste angezeigt wird
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // Der Adapter wird in die Sitzung eingefügt, damit die Ansicht [Request] ihn abrufen kann
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • Zeile 3: Es wird ein Adapter für den Spinner der Beispiele erstellt, hier eine Liste von String, die die Namen der Beispiele enthält. Diese Namen sind in der Datei [layout/exemples.xml] enthalten:
  

Die Datei [exemples.xml] enthält den folgenden Code:


<!-- Beispiele -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
  </string-array>
</resources>

Zeile 1: Diese Datei ist der zweite Parameter der Methode [createFromResource]. In [R.array.exemples] ist [exemples] der Name des Arrays (siehe Zeile 3 oben), nicht der Name der Datei.

  • Zeile 5: Dem Adapter wird ein layout (Anzeigehandler) zugeordnet. Nun verfügt der Adapter sowohl über die Daten als auch über deren Anzeigemodus;
  • Zeile 7: Der Adapter wird in die Sitzung aufgenommen. Dort wird er von dem Fragment [RequestFragment] abgerufen, das ihn benötigt;

Fahren wir mit dem Code der Methode [createResponseFragments] fort:


private void createResponseFragments() {
    // Beispiele für Spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Legen Sie das Layout fest, das verwendet werden soll, wenn die Auswahlliste angezeigt wird
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // Der Adapter wird in die Sitzung eingefügt, damit die Ansicht [Request] ihn abrufen kann
    session.setSpinnerExemplesAdapter(adapter);
    // Erstellung der Fragmenttabelle (1 Abfrage, n Antworten)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // Fragment der Abfrage
    tFragments[0] = new RequestFragment();
    // Fragmente der Antworten
    for (int i = 1; i < tFragments.length; i++) {
      // Der Name des zu instanziierenden Fragments, das dem vom Benutzer ausgewählten Beispiel entspricht, wird gebildet
      // Dieser Name muss der vollständige Name einschließlich des Pakets sein – hier wird er direkt mit der Nummer des Beispiels im Spinner verknüpft
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // Das dem Beispiel zugeordnete Fragment wird instanziiert
      MyFragment fragment;
      try {
        // Instanziierung der Klasse
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // Das Fragment wurde erstellt – es wird in das Array aufgenommen
      tFragments[i] = fragment;
    }
    // Instanziierung des Fragment-Managers mit diesen neuen Fragmenten
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Einrichtung des ViewPager mit dem Abschnittsadapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // Navigation zwischen Seiten – diese Anweisung ist wichtig
    // Hier wird angegeben, dass auf beiden Seiten der angezeigten Ansicht initialisierte Ansichten beibehalten werden müssen
    // Dies hat zur Folge, dass alle von der Anwendung verwendeten Fragmente im Speicher vorhanden und initialisiert sind
    // Wenn man dies nicht tut, ist der Wert [OffscreenPageLimit] standardmäßig 1
    // Wenn also das angezeigte Fragment die Nr. 3 ist, werden nur die Fragmente 2 und 4 initialisiert
    // Dies geschieht durch den Aufruf der Methode [onCreateView] dieser beiden Fragmente – das bedeutet, dass in dieser Methode vorgesehen werden muss,
    // die visuelle Darstellung des Fragments so wiederherzustellen, wie sie bei seiner letzten Verwendung war – außerdem darf es in dieser Methode
    // Code enthalten, der nicht zweimal ausgeführt werden darf – das führt zu einem riesigen Durcheinander und ist schwer zu handhaben
    // Hier hat man es vorgezogen, diese Schwierigkeiten zu vermeiden – in den Protokollen ist zu sehen, dass beim Start der Anwendung alle Fragmente erstellt werden
    // und ihre Methode [onCreateView] ausgeführt – danach wird sie nie wieder ausgeführt –
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // das Wischen zwischen den Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);
  }
  • Zeile 9: Erstellung des Arrays, das alle Fragmente der Anwendung enthalten wird;
  • Zeile 11: Das erste Fragment ist das der Anfrage;
  • Zeilen 13–28: Es werden so viele Fragmente erstellt, wie es Beispiele gibt. Diese Fragmente erweitern alle das Antwortfragment [ResponseFragment] und implementieren nur das, was für das jeweilige Beispiel spezifisch ist: die Erstellung der beobachteten Werte. Diese unterscheiden sich nämlich von Beispiel zu Beispiel;
  • Zeile 16: Das Fragment eines Beispiels trägt einen Standardnamen: ExampleXXFragment, wobei XX seine Position im Beispiel-Spinner ist, erhöht um 1. XX ist ebenfalls die Nummer des Beispielfragments im Fragment-Manager;
  • Zeile 21: Instanziierung des Beispielfragments Nr. i aus dem Spinner:
    • Class.forName(exampleName): Lädt das Fragment in den Speicher;
    • Class.forName(exampleName).getConstructors()[0]: Ruft die Referenz auf den ersten Konstruktor der Klasse ab. Die Klasse ExampleXXFragment hat nur einen Konstruktor. Es wird daher eine Referenz auf diesen Konstruktor abgerufen;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instanziiert ein Objekt vom Typ ExampleXXFragment unter Verwendung des Konstruktors aus dem vorherigen Schritt. new Object[]{} stellt die an diesen Konstruktor übergebenen Parameter dar. Da der Konstruktor der Klasse ExampleXXFragment keine Parameter erwartet, wird ein leeres Objektarray übergeben;
  • Zeile 27: Dieses Fragment wird dem Fragment-Array hinzugefügt;
  • Zeile 30: Wir haben gesehen, dass der Konstruktor des Fragment-Managers [SectionsPagerAdapter] in seinen Parametern das Array der Fragmente erwartet, die er verwalten soll. Dieses wird ihm nun übergeben;
  • Zeile 22: Der Fragmentcontainer [mViewPager] der Ansicht, die der Aktivität [MainActivity] zugeordnet ist, wird hier mit dem Fragmentmanager verknüpft: Der Fragmentcontainer [mViewPager] zeigt die Fragmente des Fragmentmanagers an;
  • Zeile 43: Man beachte die Kommentare – die Anweisung besagt im Grunde, dass alle Fragmente in dem Zustand verbleiben müssen, in den sie der Code versetzt, unabhängig davon, welches Fragment gerade angezeigt wird. Wenn man also zu ihm zurückkehrt, findet man es in dem Zustand vor, in dem man es zurückgelassen hat;
  • Zeile 45: Der Fragment-Container [mViewPager] ist vom Typ [MyPager], wodurch das Wischen unterbunden wird;

Die Methode [MainActivity.showView] lautet wie folgt:


  // Anzeige Ansicht Nr. [position]
  private void showView(int position) {
    // Das Fragment wird vor der Anzeige aktualisiert
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // Die angeforderte Ansicht wird angezeigt – es wird direkt zur Ansicht gewechselt (zweiter Parameter auf „false“)
    // Ohne diesen Parameter springt man standardmäßig zur gewünschten Ansicht, wobei die Zwischenansichten kurz angezeigt werden – unerwünschtes Verhalten
    mViewPager.setCurrentItem(position, false);
}
  • Zeile 3: Das Fragment mit der Position Nr. soll angezeigt werden;
  • Zeile 4: Dieses Fragment wird vom Fragment-Manager angefordert und anschließend aktualisiert. Denn seit der letzten Anzeige könnte sich die Sitzung geändert haben. Das Fragment muss daher die Sitzung überprüfen, um festzustellen, ob es aktualisiert werden muss;
  • Zeile 7: Das Fragment wird von [ViewPager] angezeigt. Da dieses dem Fragment-Manager zugeordnet wurde, wird das Fragment mit der Nummer [position] angezeigt, also dasjenige, das gerade in Zeile 4 aktualisiert wurde;

Abschließend noch die beiden Methoden zur Wartungsverwaltung:


  public void beginWaiting() {
    // Verwaltung des Ladebildes
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // Verwaltung des Ladebildes
    loadingPanel.setVisibility(View.INVISIBLE);
    // Beendigung der Ausführung
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. Das Fragment [RequestFragment]

Die Klasse [RequestFragment] sieht wie folgt aus:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.net.URI;
import java.net.URISyntaxException;

public class RequestFragment extends MyFragment {

  // URL des Webdienstes
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // Anzahl der Anfragen
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // Generierungsintervall
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // Verzögerung
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // Anzahl der generierten Werte
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // Schaltfläche
  private Button btnExecuter;
  // Liste der Antworten
  private ListView listReponses;
  private TextView infoReponses;
  // Beispiel-Spinner
  private Spinner spinnerExemples;

  // die Eingaben
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

  // Konstruktor
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // Aktivität und Sitzung abrufen
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // Erstellen der Fragmentansicht anhand ihrer Definition XML
    View view = inflater.inflate(R.layout.request, container, false);
    // Komponenten
    edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
    txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
    edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
    txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
    edtA = (EditText) view.findViewById(R.id.edt_a);
    edtB = (EditText) view.findViewById(R.id.edt_b);
    txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
    edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
    edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
    txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
    edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
    edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
    txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
    btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);

    // Schaltfläche [Exécuter]
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });

    // zunächst keine Fehlermeldungen
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // Beispiel-Spinner
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // Ergebnis
    return view;
  }
...
}
  • Zeile 16: Die Klasse [RequestFragment] erweitert die Klasse [MyFragment] (siehe Abschnitt 9.3.7.1);
  • Zeilen 18–42: die visuellen Komponenten des Fragments (siehe Abschnitt 9.3.7.2);
  • Zeilen 45–52: die vom Benutzer im Formular vorgenommenen Eingaben;
  • Der Konstruktor (Zeilen 55–58) und die Methode [onCreateView] werden ausgeführt, wenn die Aktivität [MainActivity] alle Fragmente der Anwendung erstellt. Dies geschieht nur einmal;
  • Zeile 61: Der Code der Methode [onCreateView] ist klassisch. In Zeile 102 ist zu beachten, dass der Spinner-Adapter aus den Beispielen in die Sitzung aufgenommen wird. Beachten Sie außerdem in Zeile 91, dass der Klick auf die Schaltfläche [Exécuter] von der Methode [doExecuter] verarbeitet wird;
  • Zeilen 64–65: Die Felder [activity] und [session] gehören zur übergeordneten Klasse [MyFragment];

Die Methode [doExecuter] lautet wie folgt:


  // die Eingaben
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

...

  private void doExecuter() {
    // Sind die Eingaben gültig?
    if (isPageValid()) {
      // Die Informationen werden in der Sitzung gespeichert
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // Die URL des Webdienstes wird gespeichert
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // Aktion läuft
      session.setOnAir(true);
      // aber noch nicht begonnen
      session.setOperationStarted(false);
      // Das Antwortfragment wird angezeigt
      activity.selectTab(Constants.VUE_RESPONSE);
      // Wartephase beginnt
      beginWaiting();
    }
}
  • Zeile 15: Wir werden die Methode [ispageValid] nicht näher erläutern. Sie überprüft die Gültigkeit der Eingaben und gibt nur dann „true“ zurück, wenn alle gültig sind. In diesem Fall werden sie zur Initialisierung der Felder in den Zeilen 2–9 verwendet;
  • Zeile 17: Die verschiedenen Eingaben werden in die Sitzung übernommen:
    • [spinnerExemples.getSelectedItem().toString()] ist der Name des vom Benutzer ausgewählten Beispiels und wird in [session.exampleName] gespeichert;
    • [spinnerExemples.getSelectedItemPosition() + 1] ist die Nummer des Fragments, das dem Beispiel zugeordnet ist und vom Fragmentmanager gespeichert wurde (das Fragment). Diese Nummer wird in [session.examplePosition] gespeichert;
  • Zeile 19: Die URL des Webdienstes / jSON wird an die Aktivität weitergeleitet, die sie wiederum an die Schicht [DAO] weiterleitet;
  • Zeilen 21–24: Es wird vermerkt, dass ein Vorgang gestartet wird;
  • Zeile 26: Die Registerkarte der Antwort wird angezeigt. Um zu verstehen, was nun geschieht, muss man sich den Code [MainActivity.selectTab] in Erinnerung rufen:

  // Auswahl einer Registerkarte
  public void selectTab(int position) {
    // Es gibt höchstens 2 Registerkarten
    // Zu Beginn gibt es nur eine, nämlich die der Anfrage
    // Wenn die angeforderte Registerkarte die Nr. 1 ist und diese noch nicht existiert, muss sie angelegt werden
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 Registerkarte mehr
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // Die Registerkarte wird programmgesteuert ausgewählt, wodurch das Ereignis [onTabSelected] ausgelöst wird
    //, das dieser Registerkarte die richtige Ansicht zuordnet
    tabLayout.getTabAt(position).select();
}
  • Zunächst hatte die Aktivität nur die Registerkarte der Anfrage (Registerkarte Nr. 0) erstellt;
  • Zeilen 6–11: Die Antwortregisterkarte (Registerkarte Nr. 1) wird erstellt, falls sie noch nicht erstellt wurde;
  • Zeile 14: Es wird die Registerkarte Nr. position (0 oder 1) ausgewählt. Dadurch wird das Ereignis [onTabSelected] in die Warteschlange der Ereignisschleife der Android-Anwendung gestellt;

Der Handler für das Ereignis [onTabSelected] in [MainActivity] lautet wie folgt:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // Eine Registerkarte wurde ausgewählt – das vom Fragment-Container angezeigte Fragment wird geändert
        int position = tab.getPosition();
        if (position == 0) {
          // Registerkarte „Anfrage“
          showView(0);
        } else {
          // Registerkarte „Antwort“ – hängt vom gewählten Beispiel ab
          showView(session.getExamplePosition());
        }
}

Im Fall der Registerkarte [Response] wird Zeile 9 ausgeführt. Das Fragment Nr. [session.getExamplePosition()] wird angezeigt. Im Beispiel [exemple-03] beispielsweise ist die in [session.examplePosition] gespeicherte Nummer 3. In Zeile 10 wird dann das Fragment Nr. 3 angezeigt. Die Tabelle der Fragmente, die ursprünglich von der Aktivität erstellt wurde, lautet [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Es wird also tatsächlich das Fragment [Exemple03Fragment] angezeigt. Dies geschieht durch den folgenden Code:


  // Anzeige der Ansicht Nr. [position]
  private void showView(int position) {
    // Das Fragment wird vor der Anzeige aktualisiert
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // Die angeforderte Ansicht wird angezeigt – es wird direkt zur Ansicht gewechselt (zweiter Parameter auf „false“)
    // Ohne diesen Parameter springt man standardmäßig zur gewünschten Ansicht, wobei die Zwischenansichten kurz angezeigt werden – unerwünschtes Verhalten
    mViewPager.setCurrentItem(position, false);
}

Man sieht, dass das Fragment aktualisiert wird (Zeile 4), bevor es angezeigt wird (Zeile 7).

9.3.7.6. Das Fragment [ResponseFragment]

Die Klasse [ResponseFragment] zeigt die Antworten des Servers an. Ihr Code lautet wie folgt:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public abstract class ResponseFragment extends MyFragment {

  // Liste der Antworten
  private ListView listReponses;
  private TextView infoReponses;
  // Schaltfläche
  private Button btnAnnuler;

  // Mapper jSON
  private ObjectMapper mapper;

  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Aktivität und Sitzung werden abgerufen
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // die Fragmentansicht wird anhand ihrer Definition erstellt XML
    View view = inflater.inflate(R.layout.response, container, false);
    // Komponenten
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // Schaltfläche [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // Ergebnis
    return view;
  }
...
  // Methode, die (durch expliziten Code) vor jeder Anzeige des Fragments ausgeführt werden soll
  public void onRefresh() {
...
  }
}
  • Zeile 21: Die Klasse [ResponseFragment] erweitert die Klasse [MyFragment];
  • Zeilen 23–27: die Komponenten des Fragments;
  • Zeilen 32–36: Der Konstruktor wird nur einmal ausgeführt, und zwar bei der erstmaligen Erstellung der Beispielfragmente durch die Aktivität. Tatsächlich erben alle Beispielfragmente vom Fragment [ResponseFragment]. Bei ihrer Instanziierung wird der Konstruktor ihrer übergeordneten Klasse [ResponseFragment] aufgerufen;
  • Zeile 35: Initialisiert den Mapper jSON aus Zeile 30, der zur Anzeige der Zeichenkette jSON aus einem Ausnahmestapel verwendet wird;
  • Zeilen 38–59: Die Methode [onCreateView] wird nur einmal ausgeführt, und zwar bei der erstmaligen Erstellung der Beispielfragmente durch die Aktivität. Hier findet sich klassischer Code einer Android-Anwendung;
  • Zeilen 52–56: Die Methode, die beim Klicken auf die Schaltfläche „[Annuler]“ ausgeführt wird, ist die Methode „[doAnnuler]“;
  • Zeilen 62–64: Die Methode [onRefresh] wird jedes Mal ausgeführt, wenn die Registerkarte [Response] angezeigt wird;

Dank der verschiedenen Log-Einträge in den wichtigen Methoden lässt sich nachvollziehen, was beim Start der Anwendung geschieht:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • Zeile 1: Erstellung des Fragments [RequestFragment];
  • Zeilen 2–9: Erstellung der Fragmente der vier Anwendungsbeispiele;
  • Zeile 10: Initialisierung des Fragments [RequestFragment];
  • Zeilen 11–14: Initialisierung der Fragmente der vier Anwendungsbeispiele;

Danach tauchen keine Aufrufe dieser Methoden mehr auf.

Die Methode [ResponseFragment.onRefresh] lautet wie folgt:


  // Methode, die (durch expliziten Code) vor jeder Darstellung des Fragments ausgeführt werden soll
  public void onRefresh() {
    Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
    // Wird gerade ausgeführt?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // Anfrage wird ausgeführt
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • Zeile 5: Es wird geprüft, ob das Fragment [RequestFragment] eine Anfrage gestellt hat (session.isOnAir) und ob diese bereits gestartet wurde (isOperationStarted). Wenn das Fragment [RequestFragment] eine Anfrage gestellt hat und diese noch nicht ausgeführt wird, wird der Vorgang gestartet (Zeilen 7–8);
  • Sobald der Vorgang gestartet wurde, kann der Benutzer – da dieser asynchron abläuft – zwischen den beiden Registerkarten wechseln. Wenn er erneut zur Registerkarte [Response] wechselt und ein Vorgang läuft, werden die Zeilen 7–8 nicht ausgeführt;

Die Methode [doExecuter] in Zeile 8 führt den vom Benutzer angeforderten Vorgang aus:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // Wartephase beginnt
    beginWaiting();
    // Vorbereitung der Ausführung
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // Die Observables des ausgewählten Beispiels werden erstellt und ausgeführt
    createAndExecuteObservables();
}

// Methode durch untergeordnete Klassen implementiert
protected abstract void createAndExecuteObservables();
  • Zeile 10: Erstellt, führt aus und beobachtet Observables. Diese sind für jedes Beispiel unterschiedlich. Aus diesem Grund ist die Methode [createAndExecuteObservables] abstrakt (Zeile 14). Sie wird durch die Fragmente [ExampleXXFragment] implementiert, die die Klasse [ResponseFragment] erweitern;
  • Zeile 6: Die Liste der Abonnements wird geleert;
  • Zeile 7: Die Liste, die die Antworten anzeigt, wird geleert;
  • Zeile 8: Zählt die Anzahl der empfangenen Antworten;

Die untergeordneten Klassen [ExampleXXFragment] übertragen der folgenden Methode [showAlea] die Aufgabe, die von ihnen beobachteten Elemente anzuzeigen:


  protected void showAlea(String data) {
    // Eine weitere Information
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 weitere Antwort
    reponses.add(0, data);
    Log.d("rxjava", data);
    // Aktualisierung von UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • Zeile 1: Man sieht, dass das beobachtete Element in Form einer Zeichenkette eintrifft. Dabei handelt es sich tatsächlich um die Zeichenkette jSON des beobachteten Elements. Dies ermöglicht es uns, das beobachtete Element unabhängig von seinem genauen Java-Typ mit einer einzigen Methode anzuzeigen;
  • Zeile 6: Das beobachtete Element [data] wird an erster Stelle der Antwortliste hinzugefügt. Der Benutzer sieht somit am Anfang der Liste die neuesten Antworten;

Die Warteschlange wird durch die folgenden Methoden [beginWaiting] und [cancelWaiting] verwaltet:


  private void beginWaiting() {
    // Die Sanduhr wird angezeigt
    activity.beginWaiting();
    // Die Schaltfläche [Annuler] wird angezeigt
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // Wartezeit beendet
    activity.cancelWaiting();
    // Die Schaltfläche [Annuler] wird ausgeblendet
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Sie greifen auf die gleichnamigen Methoden der Aktivität zurück und beschränken sich darauf, die Schaltfläche [Annuler] ein- bzw. auszublenden.

Der Klick auf die Schaltfläche [Annuler] wird durch den folgenden Code verarbeitet:


  protected void doAnnuler() {
    // Alle Abonnements werden gekündigt
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // Wartezeit beendet
    cancelWaiting();
}
  • Zeilen 3–7: Alle Abonnements werden nacheinander gekündigt;

9.3.8. Beispiele für Observables

9.3.8.1. Exemple-01

Die Klassen [ExampleXXFragment] dienen dazu, Observables zu erstellen, auszuführen und zu beobachten. Die Anzeige der beobachteten Werte erfolgt durch die übergeordnete Klasse [ResponseFragment].

Die Klasse [Example01Fragment] sieht wie folgt aus:

  

package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example01Fragment extends ResponseFragment {

    // Mapper jSON
    private ObjectMapper mapperAleasUiResponse;

    // Konstruktor
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // Filter jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // Zufallszahlen werden angefordert
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // beobachtbare Konfiguration Nr. i
            // Anfrage an den Server
            Request request = session.getRequest();
            request.setId(i);
            // Observable, das auf einem Berechnungs-Thread ausgeführt wird
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // Beobachtung im Thread der Ereignisschleife;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // Alle diese Observables werden ausgeführt
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
...
        }, new Action0() {
...
    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // Die anzuzeigenden Informationen werden extrahiert
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • Zeile 36: die einzige Beobachtungsgröße, die generiert wird;
  • Zeilen 37–44: Generierung und Konfiguration der verschiedenen Observables, die (in Zeile 43) in das Observable aus Zeile 36 zusammengeführt werden;
  • Zeile 43: Das Observable wird in einem Thread des Schedulers [Schedulers.io()] ausgeführt. Der Aufruf HTTP an den Server wird in diesem Thread ausgeführt;
  • Zeile 46: Das endgültige Observable wird im Thread der Event-Loop beobachtet;
  • Zeilen 48–57: Ausführung der Observables, also der Anfragen an den Zufallszahlengenerator. Android unterstützt Java 8 und dessen Lambdas noch nicht. Daher werden hier anonyme Klassen verwendet, um die funktionalen Schnittstellen von RxJava zu instanziieren;
  • Zeilen 49–52: Aktion, die ausgeführt wird, wenn der Beobachter ein neues Element vom Typ [AleasDaoResponse] vom Beobachtbaren erhält (siehe Abschnitt 9.3.6.1);
  • Zeile 51: Aufruf der Methode [showAlea] der übergeordneten Klasse. Wie bereits erwähnt, erwartet diese Methode eine Zeichenkette. Diese wird von der Methode [getDataFrom] in den Zeilen 59–68 bereitgestellt;
  • Zeile 63: Die Zeichenkette jSON vom Typ [AleasUiResponse] wird wie folgt zurückgegeben:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class AleasUiResponse {

  // Antwort [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // Beobachtungs-Thread
  private String observedOn;
  // Beobachtungszeitpunkt
  private String observedAt;

  // Konstruktoren
  public AleasUiResponse() {
    observedOn = Thread.currentThread().getName();
    observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
    this.aleasDaoResponse = aleasDaoResponse;
    this.observedOn = on;
    this.observedAt = at;
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// Getter und Setter
...
}
  • Zur Antwort der Schicht [DAO] (Zeile 11) werden zwei Informationen hinzugefügt:
    • Zeile 13: der Beobachtungs-Thread;
    • Zeile 15: der Beobachtungszeitpunkt;

Kehren wir zum Abonnementcode zurück:


    @Override
    public void createAndExecuteObservables() {
...
        // alle diese Beobachtbaren werden ausgeführt
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                // Die Ausnahme wird angezeigt
                showAlea(getMessagesFromThrowable(th));
                // Nach dem Auftreten einer Ausnahme erhält das Observable weder onNext noch onCompleted
                // Das Abonnement muss manuell gekündigt werden
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // Wartezeit beendet
                cancelWaiting();
            }
        }));
}
  • Zeilen 11–18: Fall, in dem der Beobachter eine Ausnahme erhält;
  • Zeile 14: Es wird erneut die Methode [showAlea] der übergeordneten Klasse verwendet, um die Ausnahme anzuzeigen. Die Methode [getMessagesFromThrowable] ist eine Methode der übergeordneten Klasse [ResponseFragment], die aus einer Ausnahme eine Zeichenkette erzeugt:

  // Meldungen einer Ausnahme
  protected String getMessagesFromThrowable(Throwable ex) {
    // Es wird eine Liste mit den Fehlermeldungen aus dem Ausnahmestapel erstellt
    List<String> messages = new ArrayList<String>();
    Throwable th = ex;
    while (th != null) {
      messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
      th = th.getCause();
    }
    try {
      return mapper.writeValueAsString(messages);
    } catch (IOException e) {
      return e.getMessage();
    }
}
  • Zeile 11: Die Zeichenkette jSON wird aus einer Liste von Fehlermeldungen (Zeile 4) zurückgegeben;

Kehren wir zum Code für das Abonnieren des Observables zurück:

  • Zeilen 19–25: Der Code, der ausgeführt wird, wenn der Beobachter die Benachrichtigung über das Ende der Übertragung erhält. Die Warteschleife wird dann abgebrochen (Zeile 23), wodurch die grafische Benutzeroberfläche aktualisiert wird;

Die Ausführung von Beispiel 01 liefert ein Ergebnis, das in etwa wie folgt aussieht:

Image

Jedes Element der Liste ist die Zeichenkette jSON eines beobachteten Werts. Die Felder der Zeichenkette jSON sind wie folgt:

  • aleas: die vom Server gelieferte Liste von Zufallszahlen;
  • idClient: die Nummer der Anfrage (man sieht, dass die Antworten in ungeordneter Reihenfolge zurückkamen);
  • on: der Ausführungsthread des Observables, der diesen Wert ausgegeben hat;
  • requestAt: Zeitpunkt der Client-Anfrage;
  • responseAt: Zeitpunkt der Serverantwort;
  • delay: vom Server festgestellte Wartezeit;
  • Fehler: vom Server zurückgegebener Fehlercode (0 = kein Fehler);
  • message: vom Server zurückgegebene Fehlermeldung (null = kein Fehler);
  • observedAt: Zeitpunkt der Erfassung des gemessenen Werts;
  • observedOn: Thread, in dem der gemessene Wert erfasst wurde;

9.3.8.2. Exemple-02

Die Klasse [Example02Fragment] lautet wie folgt:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example02Fragment extends ResponseFragment {

    // Mapper jSON
    private ObjectMapper mapperAleasUiResponse;

    // Konstruktor
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // Filter jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // Zufallszahlen werden angefordert
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // Anfragevorbereitung
            Request request = session.getRequest();
            request.setId(i);
            // Es werden nur die Observables mit einer geraden Kundennummer beibehalten
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // Ausführung auf dem E/A-Thread
                            .subscribeOn(Schedulers.io()));
        }
        // Beobachtung im Thread der Event-Loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // Diese Observables werden ausgeführt
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // Wartezeit beendet
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // Die anzuzeigenden Informationen werden extrahiert
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Dieses Beispiel entspricht dem vorherigen (Zeile 38). Von den im vorherigen Beispiel erhaltenen Beobachtungsgrößen werden jedoch nur diejenigen mit einer geraden Kundennummer (Zeilen 42–46) beibehalten, und zwar mithilfe der Methode [filter] (Zeile 41).

Die erhaltenen Ergebnisse lauten wie folgt (für 10 Abfragen):

Image

9.3.8.3. Exemple-03

Die Klasse [Example03Fragment] lautet wie folgt:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;
import java.util.List;

public class Example03Fragment extends ResponseFragment {

  // Mapper jSON
  private ObjectMapper mapper;

  // Konstruktor
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // Filter jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // Zufallszahlen werden angefordert
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // Anfragevorbereitung
      Request request = session.getRequest();
      request.setId(i);
      // Konfiguration des Observables
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).map(new Func1<AleasDaoResponse, List<Integer>>() {
        @Override
        public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getAleas();
        }
      })
        // Ausführung auf einem E/A-Thread
        .subscribeOn(Schedulers.io()));
    }
    // Beobachtung im Thread der Ereignisschleife
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // Diese Beobachtbaren werden ausgeführt
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // Wartezeit beendet
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // Die anzuzeigenden Informationen werden extrahiert
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Dieses Beispiel entspricht Beispiel-02:

  • Zeile 40: Es werden dieselben Observablen wie in Beispiel-02 definiert;
  • Zeile 45: Jeder der von den vorherigen Observablen ausgegebenen Werte wird durch die Methode [map] in einen Typ List<Integer> umgewandelt, der die Liste der vom Server generierten Zufallszahlen darstellt;
  • Zeile 58: Der beobachtete Wert ist nun vom Typ List<Integer>;

Das Ergebnis für 10 Anfragen lautet wie folgt:

Image

9.3.8.4. Exemple-04

Die Klasse [Example04Fragment] sieht wie folgt aus:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

public class Example04Fragment extends ResponseFragment {

  // Mapper jSON
  private ObjectMapper mapper;

  // Konstruktor
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // Filter jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // Zufallszahlen werden angefordert
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // Anfragevorbereitung
      Request request = session.getRequest();
      request.setId(i);
      // Konfiguration der Beobachtungsgrößen
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // Ausführung auf einem E/A-Thread
        .subscribeOn(Schedulers.io()));
    }
    // Beobachtung auf einem Thread der Event-Loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // Diese Observables werden ausgeführt
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // Wartezeit beendet
            cancelWaiting();
          }
        }
      ));

  }
}

Dieses Beispiel entspricht Beispiel-03, mit dem Unterschied, dass in Zeile 42 statt der Methode [map] die Methode [flatMap] verwendet wird.

  • Zeile 55: Es ist zu beachten, dass der Typ des beobachteten Werts nun Integer lautet;

Für 10 Abfragen erhält man folgende Ergebnisse:

Image

Diesmal gibt es mehr beobachtete Werte als Abfragen.

9.3.8.5. Exemple-05

Wir erläutern nun die Vorgehensweise, um der Anwendung ein neues Beispiel für Beobachtungsgrößen hinzuzufügen.

Nehmen wir an, wir möchten das Beispiel [Exemple22h] aus Abschnitt 7.6.4 nachbilden:


package dvp.rxjava.observables.exemples;

import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;

public class Exemple22h {
    public static void main(String[] args) throws InterruptedException {
        // Prozess
        Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
        Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
        // Abonnements
        ProcessUtils.subscribe(1, process);
    }
}
  • Die Werte des Observables [Observable.range(1, 10)] werden zunächst durch die Methode [groupBy] (Zeile 11) in gerade und ungerade Werte gruppiert und anschließend durch die Methode [concatMap] (Zeile 12) zu einem einzigen Observable zusammengefasst;

Schritt 1

Es wird ein neues Beispiel in der Datei [exemples.xml] erstellt:

  

<!-- Beispiele -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
    <item>Exemple-05</item>
  </string-array>
</resources>

Oben wurde Zeile 8 hinzugefügt. Der Name des Beispiels kann beliebig gewählt werden.

Schritt 2

Die Klasse [Example04Fragment] wird in [Example05Fragment] dupliziert. Hier ist der Name vorgegeben.

Schritt 3

Der Code von [Example05Fragment] wird wie folgt geändert:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;

public class Example05Fragment extends ResponseFragment {

  // Mapper jSON
  private ObjectMapper mapper;

  // Konstruktor
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // Filter jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // Instanzen der Funktionsschnittstellen
    // Filter
    Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
      @Override
      public Boolean call(AleasDaoResponse aleasDaoResponse) {
        return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
      }
    };
    // flatMap
    Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
        return Observable.from(aleasDaoResponse.getAleas());
      }
    };
    // groupBy
    Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
      @Override
      public Boolean call(Integer integer) {
        return integer % 2 == 0;
      }
    };
    // concatMap
    Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
        return integerIntegerGroupedObservable.asObservable();
      }
    };
    // Zufallszahlen werden angefordert
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // Anfrage wird vorbereitet
      Request request = session.getRequest();
      request.setId(i);
      // Konfiguration des Observables
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // Ausführung auf einem E/A-Thread
        .subscribeOn(Schedulers.io());
    }
    // Beobachtung auf einem Thread der Event-Loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // Diese Observables werden ausgeführt
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // Wartezeit beendet
            cancelWaiting();
          }
        }
      ));

  }
}
  • Zeile 67: stellt das Observable aus Beispiel 04 dar: einen Strom von Ganzzahlen;
  • Zeile 68: Wir gruppieren diesen Strom von Ganzzahlen nach einem booleschen Kriterium, das wir definieren werden. Wir erhalten eine Observable vom Typ `Observable<GroupedObservable<Boolean, Integer>>`, die somit Elemente vom Typ `GroupedObservable<Boolean, Integer>` ausgibt;
  • Zeile 68: Die Methode [concatMap] erzeugt Elemente vom Typ Integer aus Elementen vom Typ GroupedObservable<Boolean, Integer>;
  • Zeilen 32–59: Um die Erstellung des Observables in den Zeilen 67–69 übersichtlicher zu gestalten, haben wir die Instanzen der funktionalen Schnittstellen, die von den verschiedenen Operatoren [filter, flatMap, groupBy, concatMap] benötigt werden, isoliert;
  • Zeilen 47–52: Die Methode [groupBy] erwartet einen Parameter vom Typ Func1<T,K>, wobei T der Typ der gruppierten Elemente und K der Typ des Gruppierungskriteriums ist. Ausgehend vom Element T ist die Instanz Func1<T,K> dafür zuständig, den Gruppierungsschlüssel K des Elements zu erzeugen;
  • Zeilen 48–51: Elemente vom Typ Integer werden nach Parität gruppiert. Die Instanz Func1<Integer,Boolean> erzeugt den Schlüssel true oder false, je nachdem, ob das Element in die eine oder die andere Gruppe eingeordnet werden soll. Am Ende erhält man zwei Gruppen: die Gruppe der geraden Elemente mit dem Schlüssel true und die Gruppe der ungeraden Elemente mit dem Schlüssel false;
  • Zeilen 53–59: Die Methode [concatMap] erwartet einen Parameter vom Typ Func1<T,Observable<R>> und erzeugt ein Observable mit Elementen vom Typ R. Der Typ T ist hier der vom Operator [groupBy] ausgegebene Typ, in diesem Fall ein Typ GroupedObservable<Boolean, Integer>;
  • Zeile 57: Aus dem Element vom Typ [GroupedObservable<Boolean, Integer>] wird ein Observable<Integer> erzeugt. Da der Operator [groupBy] zwei Gruppen erzeugt hat, erzeugt der Operator [concatMap] zwei Observables vom Typ [Observable<Integer>]. Wie [flatMap] wird er diese zu einem einzigen Observable zusammenfassen. Im Gegensatz zu [flatMap] vermischt er jedoch nicht die Elemente der zusammengefassten Observables. Man sollte daher zwei getrennte Gruppen beobachten: die geraden Zufallszahlen und die übrigen ungeraden.

Schritt 4

Wir führen die Anwendung aus:

Image

und erhält folgende Ergebnisse:

Image

  • in [1] die geraden Zufallszahlen, in [2] die ungeraden;

9.3.8.6. Weiter geht’s

Der Leser ist nun aufgefordert, eigene Beispiele zu erstellen und auch mit verschiedenen Werten für die Eingaben im Formular zu experimentieren, mit denen die Anfragen an den Zufallszahlengenerator konfiguriert werden.

9.3.9. Fazit

Wir haben in der Android-Umgebung die folgende Architektur erstellt:

Der Android-Client:

Die Schicht [DAO] kommuniziert mit dem Server, der die vom Android-Tablet angezeigten Zufallszahlen generiert. Dieser Server verfügt über die folgende zweischichtige Architektur:

Die Schicht [DAO] stellte n Anfragen (HTTP) an den Zufallszahlenserver, und die Schicht [swing] wartete asynchron auf die Ergebnisse dieser Anfragen, um sie anzuzeigen. Diese n Anfragen HTTP wurden an denselben Server gerichtet, der die gleichen Antworttypen lieferte. Dies ermöglichte es uns, die Antworten (mergeWith) in einem einzigen Observable zusammenzufassen.

In der Praxis wenden sich Android-Anwendungen an verschiedene Server, und deren Antworten werden wahrscheinlich nicht zusammengeführt. Die Anfragen HTTP an diese Server werden unabhängig voneinander verarbeitet, und ihre Ergebnisse werden durch separate Methoden beobachtet.