Skip to content

9. RxJava in de Android-omgeving

9.1. Introduction

We zullen hier een applicatie bespreken die al in verschillende documenten aan bod is gekomen:

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

Het gaat hier om een client/server-toepassing waarbij de server asynchroon willekeurige getallen levert die de Android-client weergeeft:

  • in document 1 maakt de Android-client gebruik van niet-standaardtechnologie;
  • in document 2 maakt de Android-client gebruik van de standaardtechnologie van Android voor asynchrone bewerkingen;
  • in document 3 gebruikt de Android-client dezelfde technologie als in document 2, maar vereenvoudigd door het gebruik van annotaties uit de Android Annotations-bibliotheek;

De Android-client ziet er als volgt uit:

De laag [DAO] communiceert met de server die de willekeurige getallen genereert die door de Android-tablet worden weergegeven. Deze server heeft de volgende tweelaagse architectuur:

De clients sturen verzoeken naar bepaalde URL-elementen van de [web / JSON]-laag en ontvangen een tekstantwoord in het JSON-formaat (JavaScript Object Notation).

We zullen de analyse van de applicatie in twee stappen opsplitsen:

De webserver / jSON

  • de bijbehorende laag [métier];
  • de [web / JSON]-service, geïmplementeerd met Spring MVC;

De Android-client

  • de [DAO]-laag;
  • zijn activiteit;
  • zijn weergaven;

9.2. De webservice / jSON

Opmerking: de webservice / jSON is geïmplementeerd met behulp van de Spring-technologie MVC. Lezers die hiermee niet bekend zijn, kunnen ofwel:

  • zich beperken tot het lezen van paragraaf 9.2.1, waarin wordt uitgelegd hoe de server moet worden gestart en hoe deze kan worden opgevraagd;
  • het document [Spring MVC et Thymeleaf par l'exemple] raadplegen, met name hoofdstuk 4, waarin de belangrijkste annotaties in de code worden toegelicht;

9.2.1. Het IntelliJ IDEA-project

De webservice / jSON heeft de volgende architectuur:

Deze architectuur wordt geïmplementeerd door het volgende IntelliJ IDEA-project [1]:

De server wordt gestart door [2-3]. Vervolgens worden er logberichten op de console weergegeven:

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
  • regel 12: geeft aan dat de service beschikbaar is op poort 8080;
  • regel 10: de enige URL van de webservice / jSON die beschikbaar is via een bewerking HTTP GET. De parameters zijn als volgt:
    • [a,b]: interval voor het genereren van willekeurige getallen;
    • [minCount, maxCount]: count willekeurige getallen worden gegenereerd, waarbij count een willekeurig getal is binnen het interval [minCount, maxCount];
    • [minDelay, maxDelay]: de dienst wacht delay milliseconden voordat de gevraagde getallen worden teruggestuurd, waarbij delay een willekeurig getal is in [minDelay, maxDelay];

Laten we in een browser deze URL opvragen:

 

We hebben het volgende opgevraagd:

  • willekeurige getallen in het interval [100, 200];
  • n willekeurige getallen met n in het interval [10, 20];
  • een wachttijd van x milliseconden met x in het interval [300, 400];

In het antwoord:

  • aleas: lijst met gegenereerde willekeurige getallen;
  • delay: de wachttijd in milliseconden die de server heeft vastgelegd;
  • fout: een foutcode – 0 als er geen fout is;
  • message: een foutmelding – null als er geen fout is;

9.2.2. De Gradle-afhankelijkheden van het project

  

Het project [serveur] is een Gradle-project dat is geconfigureerd door het volgende bestand: [build.gradle] [1]:


// gegenereerd door http://start.spring.io/ (mei 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')
}
  • regel 1: een opmerking die aangeeft hoe dit configuratiebestand is gegenereerd;
  • regels 4 en 10: een afhankelijkheid van het framework [Spring Boot], een tak van het Spring-ecosysteem. Dit framework [http://projects.spring.io/spring-boot/] maakt een minimale configuratie van Spring mogelijk. Op basis van de bestanden in het classpath van het project leidt [Spring Boot] een plausibele of waarschijnlijke configuratie voor het project af. Als Hibernate dus in het classpath van het project staat, zal [Spring Boot] afleiden dat de gebruikte implementatie JPA Hibernate zal zijn en Spring dienovereenkomstig configureren. De ontwikkelaar hoeft dit niet meer zelf te doen. Hij hoeft dan alleen nog de configuraties uit te voeren die [Spring Boot] niet standaard heeft uitgevoerd, of die [Spring Boot] wel standaard heeft uitgevoerd maar die moeten worden gespecificeerd. In alle gevallen heeft de door de ontwikkelaar gemaakte configuratie het laatste woord;
  • regels 14-15: twee Gradle-plugins die nodig zijn om de inhoud van dit Gradle-bestand te kunnen gebruiken;
  • regels 17-20: definiëren de kenmerken van het voor dit project gegenereerde archief;
  • regels 22-23: voor compatibiliteit met Java 8;
  • regels 25-27: de afhankelijkheden worden gezocht in de globale Maven-repository of in de lokale repository op de machine;
  • regel 30: definieert een afhankelijkheid van het artefact [spring-boot-starter-web]. Dit artefact bevat alle archieven die nodig zijn voor een Spring-project MVC. Hieronder bevindt zich het archief van een Tomcat-server. Deze zal worden gebruikt om de webapplicatie te implementeren. Merk op dat de versie van de afhankelijkheid niet is vermeld. De versie die wordt gebruikt, is de versie die is opgegeven in het geïmporteerde project [spring-boot];

Om het project bij te werken, moet het downloaden van de afhankelijkheden [1-3] worden geforceerd:

Laten we eens kijken naar [4], de afhankelijkheden die door dit bestand [build.gradle] worden meegebracht:

 

Dat zijn er heel veel. Spring Boot voor het web heeft de afhankelijkheden opgenomen die een Spring-webapplicatie MVC waarschijnlijk nodig zal hebben. Dit betekent dat sommige misschien overbodig zijn. Spring Boot is ideaal voor een tutorial:

  • het zorgt voor de afhankelijkheden die we waarschijnlijk nodig zullen hebben;
  • we zullen zien dat het de configuratie van het Spring-project MVC aanzienlijk vereenvoudigt;
  • het bevat een ingebouwde Tomcat-server [1], waardoor we de applicatie niet op een externe webserver hoeven te implementeren;
  • het maakt het mogelijk om een uitvoerbare JAR-bestand te genereren dat alle bovengenoemde afhankelijkheden bevat. Dit JAR-bestand kan zonder herconfiguratie van het ene platform naar het andere worden overgezet.

Op de website van het Spring-ecosysteem [http://spring.io/guides] zijn talrijke voorbeelden te vinden waarin Spring Boot wordt gebruikt. Nu we de afhankelijkheden van het project kennen, kunnen we verdergaan met de code.

9.2.3. De [métier]-laag

  

De laag [métier] krijgt de volgende interface [IMetier]:


package dvp.rxjava.server.metier;

public interface IMetier {
  // willekeurige getallen in het interval [a,b]
  // n getallen worden gegenereerd, waarbij n zelf een willekeurig getal is in het interval [minCount, maxCount]
  // de getallen worden gegenereerd na een wachttijd van delay milliseconden,
  // waarbij [delay] zelf een willekeurig getal is in het interval [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Deze interface is vrijwel identiek aan die welke in paragraaf 8.4 in de Swing-omgeving is besproken. Op regel 8 retourneert de methode [getAleas] het volgende type [AleasMetier]:


package dvp.rxjava.server.metier;

import java.util.List;

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

  // constructors
  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;
  }

  // getters en setters
...
}

De code van de klasse [Metier] die de interface [IMetier] implementeert, is als volgt:


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) {
    // willekeurige getallen in het interval [a,b]
    // n getallen worden gegenereerd, waarbij n zelf een willekeurig getal is in het interval [minCount, maxCount]
    // de getallen worden gegenereerd na een wachttijd van delay milliseconden,
    // waarbij [delay] zelf een willekeurig getal is in het interval [minDelay, maxDelay]

    // enkele controles
    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;
    }
    // fouten?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // willekeurige-getallengenerator
    Random random = new Random();
    // in afwachting?
    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);
      }
    }
    // resultaat genereren
    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));
    }
    // resultaat teruggeven
    return new AleasMetier(delay,nombres);
  }

}

We geven geen toelichting op de klasse: deze is vergelijkbaar met de klasse die in paragraaf 8.4 in de Swing-omgeving is behandeld. We wijzen alleen op de volgende punten:

  • regel 10: de Spring-annotatie [@Service] zorgt ervoor dat Spring één enkel exemplaar (singleton) van de klasse instantiëert en de referentie daarvan beschikbaar stelt voor andere Spring-componenten. Hier hadden ook andere Spring-annotaties kunnen worden gebruikt om hetzelfde effect te bereiken;
  • regels 13-14: er wordt een mapper jSON geïnjecteerd. Spring is een objectcontainer. Deze container wordt bij het opstarten van de webapplicatie geïnstantieerd en de objecten die in een configuratiebestand zijn gedefinieerd, worden vervolgens geïnstantieerd, standaard als één enkel exemplaar (singleton). Een Spring-singleton kan verwijzingen naar andere Spring-objecten bevatten. Dat is hier het geval: de singleton [metier] (regels 10-11) zal een verwijzing hebben naar de singleton [mapper] (regels 13-14). Dit wordt afhankelijkheidsinjectie genoemd. Er zijn twee manieren om een singleton in een andere singleton te injecteren:
    • op basis van het type: dit is mogelijk als het te injecteren singleton het enige Spring-object is met dit type. Dit is hier het geval voor de injectie in de regels 13-14 (type ObjectMapper);
    • op basis van de naam, als meerdere Spring-objecten hetzelfde type hebben. In dat geval moet de annotatie @Qualifier("nomDuSingleton") worden toegevoegd om de naam van het singleton te specificeren;

De klasse [Metier] genereert uitzonderingen van het type [AleaException]:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // foutcode
  private int code;

  // constructors
  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;
  }

  // getters en setters

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • regel 3: [AleasException] is een afgeleide van de klasse [RuntimeException]. Het gaat dus om een ongecontroleerde uitzondering (het is niet verplicht deze af te vangen met een try/catch);
  • regel 6: er wordt een foutcode toegevoegd aan de klasse [RuntimeException];

9.2.4. De webservice / JSON

  

De webservice / JSON wordt geïmplementeerd door Spring MVC. Spring MVC implementeert het zogenaamde MVC-architectuurmodel (Model – View – Controller) als volgt:

De verwerking van een verzoek van een klant verloopt als volgt:

  1. verzoek – de aangevraagde URL hebben de vorm http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... De [Dispatcher Servlet] is de Spring-klasse die de binnenkomende URL verwerkt. Deze „routeert“ de URL naar de actie die deze moet verwerken. Deze acties zijn methoden van specifieke klassen die [Contrôleurs] worden genoemd. De C van MVC is hier de tekenreeks [Dispatcher Servlet, Contrôleur, Action]. Als er geen actie is geconfigureerd om de binnenkomende URL te verwerken, zal de servlet [Dispatcher Servlet] antwoorden dat de gevraagde URL niet is gevonden (fout 404 NOT FOUND);
  1. verwerking
  • de gekozen actie kan gebruikmaken van de parameters parami die de servlet [Dispatcher Servlet] aan haar heeft doorgegeven. Deze kunnen uit verschillende bronnen afkomstig zijn:
    • het pad [/param1/param2/...] van de URL,
    • de parameters [p1=v1&p2=v2] van de URL,
    • uit parameters die door de browser bij het verzoek zijn meegestuurd;
  • bij de verwerking van het verzoek van de gebruiker kan de actie de lagen [metier] en [2b] nodig hebben. Zodra het verzoek van de klant is verwerkt, kan dit verschillende reacties oproepen. Een klassiek voorbeeld is:
    • een foutpagina als het verzoek niet correct kon worden verwerkt
    • een bevestigingspagina in het andere geval
  • de actie vraagt om een bepaalde weergave [3] weer te geven. Deze weergave toont gegevens die het weergavemodel worden genoemd. Dit is de M van MVC. De actie zal dit model M [2c] aanmaken en vragen om een weergave V weer te geven [3];
  1. antwoord – de gekozen weergave V gebruikt het door de actie opgebouwde model M om de dynamische delen van het antwoord HTML te initialiseren dat zij naar de client moet verzenden, en verstuurt vervolgens dit antwoord.

Voor een webservice / JSON is de voorgaande architectuur enigszins aangepast:

  • in [4a] wordt het model, dat een Java-klasse is, door een bibliotheek omgezet in de tekenreeks JSON;
  • in [4b] wordt deze tekenreeks JSON naar de browser verzonden;

Laten we teruggaan naar de laag [web] van onze applicatie:

In onze applicatie is er slechts één controller:

  

De webservice / JSON stuurt zijn clients het volgende antwoord van het type [AleasResponse]:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // foutcode
  private int erreur;
  // foutmelding
  private String message;

  // constructors
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getters en setters

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • regel 5: de klasse [AleasResponse] is een uitbreiding van de klasse [AleasMetier] en neemt dus alle attributen (aleas, delay) daarvan over;
  • regel 8: een foutcode (0 als er geen fout is);
  • regel 10: indien erreur!=0, een foutmelding; indien geen fout, null;

De controller [AleasController] is als volgt:


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 {

    // bedrijfslaag
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // willekeurige getallen in [a,b]
    // n getallen worden gegenereerd met n in het interval [minCount, maxCount]
    // de getallen worden gegenereerd na een wachttijd van delay milliseconden,
    // waarbij [delay] een willekeurig getal is in het interval [minDelay, maxDelay]
    @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 {

        // het antwoord wordt voorbereid
        AleasResponse response = new AleasResponse();
        // de bedrijfslaag wordt gebruikt om de willekeurige getallen te genereren
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // foutgeval (code en melding)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // het antwoord wordt teruggestuurd jSON
        return mapper.writeValueAsString(response);
    }
}
  • regel 16: de annotatie [@Controller] maakt van de klasse [AleasController] een Spring-singleton. Deze geeft bovendien aan dat de klasse methoden bevat die verzoeken voor bepaalde URL van de webapplicatie zullen verwerken. Hier is er slechts één op regel 29;
  • regels 20-21: de annotatie [@Autowired] vraagt Spring om een component van het type [IMetier] in het veld te injecteren. Dit is de voorgaande klasse [Metier]. Omdat we hier de annotatie [@Service] aan hebben toegevoegd, wordt deze behandeld als een Spring-component;
  • regels 22-23: de annotatie [@Autowired] vraagt Spring om in het veld een component van het type [ObjectMapper] te injecteren. Deze zullen we straks definiëren;
  • regel 31: de methode [getAleas] genereert de willekeurige getallen. De naam ervan doet er niet toe. Wanneer deze wordt uitgevoerd, zijn de parameters op de regels 31-33 geïnitialiseerd door Spring MVC. We zullen zien hoe dat gebeurt. Overigens wordt deze methode uitgevoerd omdat de webserver een verzoek HTTP GET heeft ontvangen voor de URL uit regel 29 (attribuut method);
  • regel 30: de annotatie [@ResponseBody] geeft aan dat het resultaat van de methode ongewijzigd naar de client moet worden verzonden. Hier sturen we een tekenreeks naar de client, namelijk de tekenreeks jSON van het type [AleasResponse];
  • regel 29: de verwerkte URL heeft de vorm /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, waarbij {x} een variabele vertegenwoordigt. Deze verschillende variabelen worden in de regels 32-33 toegewezen aan de parameters van de methode. Dit gebeurt via de annotatie @PathVariable("x"). Merk op dat de waarden van {x} componenten zijn van een URL en dus van het type String zijn. De conversie van String naar het type van de methodeparameters kan mislukken. Spring MVC genereert dan een uitzondering. Samenvattend: als ik met een browser de URL /100/200/10/20/300/400 opvraag, wordt de methode getAleas op regel 31 uitgevoerd met de parameters a=100 (regel 31), b=200 (regel 31), minCount=10 (regel 31), maxCount=20 (regel 32), minDelay=300 (regel 32), maxDelay=400 (regel 33);
  • regel 39: er wordt aan de laag [métier] een lijst met willekeurige getallen gevraagd. We herinneren ons dat de methode [metier].getAleas een uitzondering kan genereren;
  • regels 42-43: foutgeval;
  • regel 46: het antwoord van het type [AleasResponse] wordt weergegeven in de vorm van een tekenreeks jSON;

9.2.5. Configuratie van het Spring-project

  

Er zijn verschillende manieren om Spring te configureren:

  • met XML-bestanden;
  • met Java-code;
  • met een combinatie van beide;

Wij kiezen ervoor om onze webapplicatie met Java-code te configureren. De bovenstaande klasse [Config] zorgt voor deze configuratie:


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 {
  // -------------------------------- configuratie van de laag [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();
  }
}
  • regel 15: we vertellen Spring in welke pakketten het objecten zal vinden om te instantiëren. Het zal er twee vinden:
    • de klasse [Metier], geannoteerd met [@Service];
    • de klasse [AleasController], geannoteerd met [@Controller];
  • regel 16: de annotatie [@EnableWebMvc] leidt tot automatische configuraties voor het Spring-framework MVC;
  • regels 19-20: injectie van de Spring-context (container voor Spring-objecten). Deze injectie is nodig omdat het object in de regels 22-26 deze nodig heeft;
  • het Spring-configuratiebestand kan nieuwe Spring-objecten definiëren met behulp van methoden die zijn geannoteerd met [@Bean]. Het resultaat van de methode wordt dan een Spring-object;
  • regels 22-26: definitie van de servlet van het Spring-framework MVC, die de verzoeken HTTP naar de juiste controller en de juiste methode doorstuurt. [DispatcherServlet] is een Spring-klasse;
  • regels 28-31: hier wordt aangegeven dat deze servlet alle URL-verzoeken verwerkt;
  • regels 33-36: de aanwezigheid van deze bean zorgt ervoor dat de Tomcat-server in de projectarchieven wordt geactiveerd. Deze wacht op verzoeken op poort 8080;
  • regels 39-42: een mapper jSON. Deze is geïnjecteerd in de Spring-objecten [Metier] en [AleasController];

9.2.6. Uitvoering van de webserver

  

Het project wordt uitgevoerd vanuit de volgende uitvoerbare klasse [Application]:


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) {
    // toepassing uitvoeren
    SpringApplication.run(Config.class, args);
  }

}
  • regel 6: de klasse [Application] is een uitvoerbare klasse (regels 7-10);
  • regel 9: de statische methode [SpringApplication.run] is een methode van [spring Boot] (regel 4) die de toepassing zal starten. De eerste parameter is de Java-klasse die het project configureert. In dit geval is dat de klasse [Config] die we zojuist hebben beschreven. De tweede parameter is de array met argumenten die wordt doorgegeven aan de methode [main] (regel 7). In dit geval zijn er geen argumenten;

Voor de daadwerkelijke uitvoering wordt de lezer verwezen naar paragraaf 9.2.1.

9.3. De Android-client

Opmerking: het volgende Android-project is vrij complex. Het vereist een goede kennis van Android, die bijvoorbeeld te vinden is in [Introduction à la programmation de tablettes Android avec Android Studio ].

Activiteit

Weergaven

Laag

[DAO]

Gebruiker

Server

De client zal twee componenten hebben:

  1. een laag [Présentation] (weergaven + activiteit);
  2. een laag [DAO] die zich richt tot de service [web / JSON] die we eerder hebben besproken.

9.3.1. RxAndroid

Om asynchroon te communiceren met de server voor willekeurige getallen, zal de Android-client de bibliotheek RxAndroid gebruiken. Deze breidt RxJava uit naar de Android-omgeving. Net als bij de Swing-applicatie zullen we slechts één uitbreiding gebruiken die door RxAndroid wordt aangeboden, namelijk die van de scheduler [AndroidSchedulers.mainThread()]. Een Android-gebruikersinterface volgt dezelfde principes als een Swing-interface:

  • gebeurtenissen worden verwerkt in één enkele thread, de zogenaamde event loop of UI-thread;
  • wanneer een gebeurtenis asynchrone acties in gang zet, moeten de resultaten daarvan in de UI-thread worden opgehaald als ze nodig zijn om de UI bij te werken;

De Android-client:

  • zal meerdere asynchrone verzoeken naar de server voor willekeurige getallen verzenden. Deze verzoeken worden aan de clientzijde uitgevoerd met de threads van de scheduler [Schedulers.io()];
  • deze asynchrone verzoeken leveren observables op die tot één geheel worden samengevoegd (merge);
  • deze observable wordt aan de clientzijde geobserveerd in de scheduler [AndroidSchedulers.mainThread()], aangestuurd door RxAndroid;

9.3.2. Het IntelliJ IDEA-project

Het Android-project heet [client]:

Het wordt uitgevoerd via [2].

Opmerking: de uitvoering is sterk afhankelijk van de configuratie van de gebruikte IntelliJ Idea. Het is waarschijnlijk dat de bovenstaande uitvoering [2] niet meteen zal werken op een andere machine dan de mijne. Het correct configureren van IntelliJ IDEA om dit project uit te voeren, kan voor beginners een hele uitdaging zijn. Hier zijn enkele punten om op te letten:

  • in [3] de projectstructuur openen;
  • in [4-5], de JDK en de SDK Android-versies die op mijn computer staan. Let op: JDK 1.8 is niet noodzakelijk. Android ondersteunt bepaalde functies van Java 8 niet, waaronder lambda's. Om functionele interfaces te instantiëren, zullen we daarom anonieme klassen gebruiken. Een JDK 1.6 is dan voldoende. Het project is echter, zoals het wordt gedistribueerd, geconfigureerd met een JDK 1.8;

Het bestand [build.gradle] [6] dat het Android-project configureert, ziet er als volgt uit:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // vervang door de huidige versie van de Android-plugin
    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'
  }
}

Afhankelijk van de aanwezige Android-bestanden SDK moeten de versies op regel 8, 24-25 en 29 mogelijk worden aangepast.

Gebruik de SDK Manager als volgt om nieuwe SDK Android-bestanden te installeren:

Het project is geconfigureerd voor:

  • de SDK API 23 [2];
  • de SDK Build-tools 23.0.3 [3] ;
  • de SDK Tool 25.1.3 [4]

Controleer ten slotte het pad van de SDK Android in het bestand [local.properties] [4], regel 11 hieronder:


## Dit bestand wordt automatisch gegenereerd door Android Studio.
# Wijzig dit bestand niet -- YOUR CHANGES WILL BE ERASED!
#
# Dit bestand moet *NOT* worden ingecheckt in versiebeheersystemen,
# aangezien het informatie bevat die specifiek is voor uw lokale configuratie.
#
# Locatie van het bestand SDK. Dit wordt alleen door Gradle gebruikt.
# Lees voor aanpassingen bij het gebruik van een versiebeheersysteem de
# opmerking in de koptekst.
#do 7 apr 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Het IntelliJ IDEA-project uitvoeren

Zodra de juiste omgeving voor het project is aangemaakt, kan het als volgt worden uitgevoerd:

  • in [1] wordt de Android-emulator Genymotion gestart;
  • in [2] wordt de uitvoerconfiguratie [app] uitgevoerd;
  • in [3] wordt een uitvoeringsconfiguratie aangemaakt;
 
  • in [1, 3] is de configuratie [app] genoemd;
  • in [2], deze komt overeen met de uitvoering van de module met de naam [app];
  • in [4] wordt gevraagd dat IDE bij de uitvoering een uitvoeringsapparaat voorstelt. Hier zal dit altijd de Genymotion-emulator zijn;
  • in [5] wordt aangegeven dat dit apparaat voor alle uitvoeringen van de configuratie moet worden behouden;

De uitvoering van het project op de Genymotion-emulator begint met de volgende startopdracht:

Image

Om te weten wat je in [1] moet invoeren, open je een opdrachtvenster DOS en typ je de volgende opdracht [ipconfig]:


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


Voer in [1] een van de adressen IP van uw computer in (regels 20, 28, 32). Als u een Windows-firewall hebt, moet u deze waarschijnlijk uitschakelen zodat de Android-emulator de random number server kan bereiken.

Het uitvoeren van asynchrone verzoeken met bovenstaande gegevens levert de volgende resultaten op:

Image

Elk verzoek leidt tot een antwoord jSON met de volgende velden:

  • aleas: de door de server gegenereerde willekeurige getallen;
  • idClient: het verzoeknummer;
  • on: de uitvoeringsthread van de verzoek aan de clientzijde;
  • requestAt: tijdstip van de verzoek;
  • responseAt: tijdstip van ontvangst van het antwoord;
  • delay: de wachttijd die de server in acht heeft genomen alvorens zijn antwoord terug te sturen;
  • fout: een foutcode – 0 als er geen fout is;
  • message: een foutmelding – null indien geen fout;
  • observedAt: tijdstip waarop het antwoord is waargenomen;
  • observedOn: thread waarin het antwoord is waargenomen. Dit is hier altijd [main], wat verwijst naar de thread van de gebruikersinterface;

Aangezien de verzoeken asynchroon zijn en de wachttijden die aan de server worden opgelegd willekeurig zijn, komen de antwoorden in willekeurige volgorde binnen.

9.3.4. De Gradle-afhankelijkheden van het project

Het project heeft afhankelijkheden nodig die we in het bestand [app / build.gradle] vermelden:

  

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'
}
  • de afhankelijkheden op de regels 2-3 zijn standaardafhankelijkheden van een Android-project met SDK 23;
  • de afhankelijkheid van regel 5 verwijst naar het Spring-object [RestTemplate], dat de communicatie tussen de laag [DAO] en de server regelt;
  • de afhankelijkheid in regel 6 roept de bibliotheek JSON [Jackson] op die door de toepassing wordt gebruikt;
  • de afhankelijkheid op regel 7 zorgt ervoor dat de bibliotheek RxAndroid (en daarmee ook de bibliotheek RxJava) wordt geladen, die de Ui-laag gebruikt om te communiceren met de laag [DAO];

9.3.5. Het manifest van de Android-app

  

<?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>
  • regel 5: internettoegang moet worden toegestaan;

9.3.6. De laag [DAO]

 

9.3.6.1. De interface [IDao] van de laag [DAO]

De interface van de laag [DAO] zal als volgt zijn:


package android.aleas.dao;

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

public interface IDao {

  // willekeurige getallen in het interval [a,b]
  // n getallen worden gegenereerd, waarbij n zelf een willekeurig getal is in het interval [minCount, maxCount]
  // de getallen worden gegenereerd na een wachttijd van delay milliseconden,
  // waarbij [delay] zelf een willekeurig getal is in het interval [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL van de webservice
  public void setUrlServiceWebJson(String url);

  // maximale wachttijd (ms) voor het antwoord van de server op een verbindingsverzoek
  // maximale wachttijd (ms) voor het antwoord van de server op een verzoek
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • regel 12: de methode van de laag [DAO] die asynchroon willekeurige getallen levert;
  • regel 15: om de implementatie [DAO] te wijzen op de URL van de dienst voor het genereren van willekeurige getallen;
  • regel 19: om in de implementatie [DAO] maximale wachttijden in te stellen, om te voorkomen dat er te lang moet worden gewacht wanneer de server niet reageert;

De methode [getAleas] ontvangt al haar parameters in het volgende object [Request]:


package android.aleas.fragments;

public class Request {

  // verzoeknummer
  int id;
  // gebruikersinvoer
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructors
  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;
  }

  // getters en setters
...
}

Hierin zijn de meeste parameters herkenbaar van het URL-object van de server die moet worden opgevraagd.

De methode [getAleas] retourneert een type Observable<AleasDaoResponse>, waarbij de klasse [AleasDaoResponse] als volgt is gedefinieerd:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // foutcode
  private int erreur;
  // foutmelding
  private String message;
  // wachttijd van de server
  private int delay;
  // door de server gegenereerde willekeurige getallen
  private List<Integer> aleas;
  // clientstatus
  private ClientState clientState;

  // constructors

  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;
  }

  // getters en setters
...
}

Het type [ClientState] is als volgt:


package android.aleas.dao;

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

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

public class ClientState {

  // naam van de uitvoeringsthread
  private String on;
  // tijdstip van de aanvraag
  private String requestAt;
  // tijdstip van het antwoord
  private String responseAt;
  // ID van de klant
  private int idClient;

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

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

  // privé-methoden

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

  // getters en setters
...
}
  • regel 11: uitvoeringsthread van de laag [DAO];
  • regel 13: tijdstip van het verzoek;
  • regel 15: tijdstip van het antwoord;
  • regel 17: verzoeknummer;

De velden [on, requestAt, idClient] worden aan het begin van het verzoek door de client geïnitialiseerd. Het veld [responseAt] wordt geïnitialiseerd wanneer de client het antwoord van de server ontvangt.

9.3.6.2. Implementatie van de laag [DAO]

  

De interface [IDao] wordt geïmplementeerd met de volgende klasse [Dao]:


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 {

  // klant REST
  private RestTemplate restTemplate;
  // URL service
  private String urlServiceWebJson;

  // mapper jSON
  private ObjectMapper mapper;

  // bouwers
  public Dao() {
    // toewijzer jSON
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // we stellen de URL van de dienst REST vast
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • regel 22: het object [RestTemplate] dat de communicatie met de randomgetallenserver verzorgt;
  • regel 24: het URL-object van de generatieservice – wordt ingesteld door de methode [setUrlServiceWebJson] op regel 41;
  • regel 27: de mapper jSON die zal worden gebruikt om de door de randomgetallenserver verzonden string jSON te deserialiseren;
  • regels 30-33: de constructor van de klasse;
  • regel 32: de mapper jSON uit regel 27 wordt aangemaakt;

De methode [setClientTimeouts] is als volgt:


  // klant REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // de time-out voor verzoeken van de client REST wordt ingesteld
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • de communicatie tussen de client en de webserver / JSON wordt verzorgd door het object [RestTemplate] uit regel 2. Voorlopig hebben we dit nog niet geïnitialiseerd. Dit gebeurt door de methode [setClientTimeouts];
  • regel 8: de klasse [HttpComponentsClientHttpRequestFactory] wordt geleverd door de afhankelijkheid [spring-android-rest-template]. Hiermee kunnen we de maximale wachttijden voor het antwoord van de server instellen (regels 9-10);
  • regel 11: we maken het object van het type [RestTemplate] aan, dat als basis zal dienen voor de communicatie met de webservice. We geven het object [factory], dat zojuist is aangemaakt, als parameter door;
  • regel 12: de dialoog tussen client en server kan verschillende vormen aannemen. De uitwisselingen vinden plaats via tekstregels en we moeten het object van het type [RestTemplate] aangeven wat het met deze tekstregel moet doen. Hiervoor voorzien we het van converters, klassen die tekstregels kunnen verwerken. De keuze van de converter gebeurt doorgaans via de headers HTTP die bij de tekstregel horen. Op basis van deze headers kiest het object [RestTemplate] uit zijn converters degene die het meest geschikt is voor de situatie. In dit geval hebben we slechts één converter, namelijk een String --> String-converter, waardoor het type String dat van de server wordt ontvangen, geen enkele transformatie ondergaat.

De methode [getAleas] is de meest complexe methode:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // uitvoering van de service
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL van de service: /{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());
          // klantgegevens
          ClientState clientState = new ClientState(request.getId());
          // synchroon HTTP-verzoek
          String response = executeRestService("get", urlService, null);
          // deserialisatie van het antwoord jSON van de server
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // fout?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // de uitzondering wordt doorgestuurd
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // de ontvangsttijd wordt geregistreerd
            clientState.setResponseAt();
            // het resultaat wordt doorgestuurd naar de abonnee
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // de uitzondering wordt doorgestuurd naar de abonnee
          subscriber.onError(ex);
        } finally {
          // het einde van de observable wordt gemeld
          // tijdens de uitvoering blijkt dat deze methode geen effect heeft als de methode [onError] eerder is aangeroepen     in overeenstemming met de theorie - deze instructie zou dus alleen in de try-blok geplaatst kunnen worden
          subscriber.onCompleted();
        }
      }
    });
  }
  • regel 2: on moet niet vergeten dat we een type [Observable<AleasResponse>] moeten genereren;
  • regel 3: een logregel op de Android-console;
  • regel 5: het object [RestTemplate] zorgt voor een synchrone communicatie met de server. Dit betekent dat de uitvoeringsthread die het verzoek doet, wordt geblokkeerd totdat het antwoord is ontvangen. In het Swing-voorbeeld hebben we gezien hoe we een synchrone actie kunnen omzetten in een asynchrone actie met behulp van de methode [Observable.create]. Diezelfde aanpak volgen we hier;
  • regel 7: de methode [call] van de interface [Observable.OnSubscribe<AleasDaoResponse>] uit regel 5. Deze methode wordt aangeroepen wanneer een observer zich abonneert op de observable;
  • regels 10-12: aanmaken van het URL-object van de dienst voor willekeurige getallen;
  • regel 14: initialisatie van het object [ClientState]. Hier wordt het tijdstip van het verzoek genoteerd;
  • regel 16: synchroon verzoek HTTP. We krijgen een antwoord jSON. De methode [executeRestService] verwacht drie parameters:
      1. de methode HTTP die moet worden gebruikt om de service te raadplegen;
      2. de URL van de service;
      3. het te verzenden object van het type Object, of null als de methode HTTP niet POST is;
  • 18-21: deserialisatie van de ontvangen tekenreeks jSON naar een type [AleasServerResponse]. Dit type is als volgt:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // foutcode
  private int erreur;
  // foutmelding
  private String message;
  // wachttijd server
  private int delay;
  // willekeurige getallen
  private List<Integer> aleas;

  // getters en setters
...
}
  • regel 23: de door de server verzonden foutcode wordt opgehaald;
  • regels 24-26: in geval van een fout wordt een uitzondering doorgegeven aan de abonnee;
  • regel 29: [clientState] wordt bijgewerkt; dit zal deel uitmaken van het antwoord dat naar de abonnee wordt verzonden;
  • regels 31-33: verzending van het antwoord naar de abonnee. Het antwoord heeft het formaat [AleasDaoResponse];
  • regels 35-37: behandelen alle foutgevallen op dezelfde manier. De meest waarschijnlijke fout is een netwerkfout;
  • regel 41: melding dat de verzending is voltooid;

9.3.7. De weergaven van de applicatie

  

De applicatie bevat de volgende twee weergaven:

Het verzoekscherm

Image

Het antwoordscherm

Image

9.3.7.1. De klasse [MyFragment]

Er zijn twee fragmenten:

  • [RequestFragment] voor de aanvraag;
  • [ResponseFragment] voor het antwoord;

Beide fragmenten vormen een uitbreiding van de volgende 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 {

  // ------------- gegevens die voor alle fragmenten gelden
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • regel 7: de klasse [MyFragment] is een uitbreiding van de Android-klasse [Fragment];
  • regels 10-11: de gegevens die alle fragmenten gemeen hebben;
  • regel 10: elk fragment kent de enige activiteit van de applicatie;
  • regel 11: om onderling te communiceren, maken de fragmenten gebruik van een sessie;
  • regel 13: voordat een fragment wordt weergegeven, wordt het gevraagd om zich te vernieuwen met de inhoud van de sessie. Deze methode is als abstract gedeclareerd omdat deze door de afgeleide klassen wordt geïmplementeerd. Om deze reden is de klasse zelf als abstract gedeclareerd (regel 7);

De klasse [Session] bevat de gegevens die de verschillende fragmenten van de applicatie met elkaar delen. De code ervan is als volgt:

  

package android.aleas.activity;

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

public class Session {

  // activiteit van de applicatie
  private MainActivity activity;
  // aantal verzoeken
  private int nbRequests;
  // kenmerken van de verzoeken
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL webservice / jSON
  private String urlWebJson;
  // bewerking gestart
  private boolean onAir;
  // idem, maar iets later
  private boolean operationStarted;
  // de naam van het voorbeeld dat de gebruiker uit de lijst met voorbeelden heeft gekozen
  private String exampleName;
  // het nummer in de lijst met fragmenten
  private int examplePosition;
  // de spinner-adapter van de voorbeelden in de queryweergave
  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);
  }

  // getters en setters
...
}

Met de methode op regel 46 kan het object [Request] worden aangemaakt, waarin alle door de gebruiker in de queryweergave ingevoerde informatie is ingekapseld:

  

package android.aleas.fragments;

public class Request {

  // query-nummer
  int id;
  // gebruikersinvoer
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructors
  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;
  }

  // getters en setters
....
}

9.3.7.2. Het fragment [RequestFragment] van de query

Het fragment van de query bestaat uit de volgende componenten:

Image

De applicatie heeft één weergave die bestaat uit twee tabbladen:

  • [1]: het tabblad van de aanvraag;
  • [2]: het tabblad met het antwoord;

De onderdelen van het fragment [RequestFragment] zijn als volgt:

nr.
Type
Naam
Functie
3
EditText
edtNbRequests
aantal verzoeken aan de dienst voor het genereren van willekeurige getallen
4
EditText
edtA, edtB
de grenzen [a,b] van het interval voor het genereren van getallen;
5
EditText
edtMinCount, edtMaxCount
de dienst genereert getallen count, waarbij count een willekeurig getal is binnen het interval [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
de dienst wacht delay milliseconden voordat hij de getallen genereert, waarbij delay een willekeurig getal is in het interval [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
URL van de dienst voor het genereren van willekeurige getallen;
8
Spinner
spinnerExemples
de vervolgkeuzelijst met voorbeelden. Elk voorbeeld illustreert een specifieke methode van de klasse [Observable];
8
Button
btnExecuter
de knop die de oproepen naar de dienst voor het genereren van getallen initieert;

Invoerfouten worden gemeld:

Image

De componenten 1 tot en met 6 zijn [TextView]-componenten met de volgende namen (in volgorde): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. Het fragment [ResponseFragment] uit het antwoord

Het fragment van het antwoord bestaat uit de volgende onderdelen:

Image

nr.
Type
Naam
Functie
1
TextView
infoReponses
aantal ontvangen reacties
2
ListView
listReponses
lijst met jSON-kanalen ontvangen van de server
3
Knop
btnAnnuler
om verzoeken aan de server te annuleren

9.3.7.4. De Android-activiteit [MainActivity]

  

De klasse [MainActivity] geeft de volgende weergave [] weer:


<?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">

  <!-- toepassingsbalk -->
  <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">

    <!-- werkbalk -->
    <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">

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

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

  <!-- weergavecontainer -->
  <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>

De componenten van deze weergave zijn als volgt:

regels
Type
Naam
Rol
20-34
Werkbalk
werkbalk
werkbalk van de applicatie
29-34
ProgressBar
loadingPanel
laadbeeld dat wordt weergegeven terwijl het verzoek van de gebruiker wordt verwerkt
37-40
TabLayout
tabbladen
de tabbalk van de applicatie
44-51
MyPager
container
de container waarin de verschillende fragmenten van de applicatie worden weergegeven

De klasse [MyPager] is als volgt:


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 {

  // veegbediening
  private boolean isSwipeEnabled;

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

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

  // herdefinitie van methoden
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe toegestaan?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

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

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

}
  • de klasse [MyPager] is een uitbreiding van de standaard Android-klasse [ViewPager]. We gebruiken de klasse [MyPager] in plaats van de klasse [ViewPager] uitsluitend omdat we het vegen willen uitschakelen: standaard kun je met de klasse [ViewPager] van het ene tabblad naar het andere gaan door te vegen (een veegbeweging naar links of rechts). Hier willen we dit gedrag niet;
  • regel 11: de booleaanse variabele die de veegbeweging regelt (regels 26 en 36);
  • regels 44-46: de methode waarmee het veld op regel 11 wordt geïnitialiseerd;

Het raamwerk van de Android-activiteit [MainActivity] is als volgt:


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 {

  // laag [DAO]
  private IDao dao;
  // de sessie
  private Session session;

  // constructor
  public MainActivity() {
    // ouder
    super();
    // sessie
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getters

  public Session getSession() {
    return session;
  }

  // implementatie 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);
  }

}
  • regel 21: de klasse [MainActivity] is een uitbreiding van de standaard Android-klasse [AppCompatActivity]. Het is dus een standaard Android-activiteit;
  • regel 21: de klasse [MainActivity] implementeert de interface [IDao];

Terugkomend op de architectuur van de applicatie:

doordat de activiteit de interface van de laag [DAO] implementeert, hoeven de weergaven geen kennis te hebben van de laag [DAO]: hun gebeurtenishandlers zullen zich tot de laag [activité] richten wanneer ze met de server willen communiceren.

  • regel 24: een verwijzing naar de laag [DAO], geïnitialiseerd door de constructor op regel 35;
  • regel 26: een verwijzing naar de door de fragmenten gedeelde sessie, geïnitialiseerd door de constructor op regel 33;
  • regels 46-59: implementatie van de interface [IDao];

De klasse [MainActivity] initialiseert de componenten van de bijbehorende weergave als volgt:


  // werkbalk
  private Toolbar toolbar;
  // fragmentbeheerder
  private MyPager mViewPager;
  // tabbladcontainer
  private TabLayout tabLayout;
  // laadbeeld
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // klassiek
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // sessie
    session.setActivity(this);
    // configuratie van time-outs van de laag [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

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

    // werkbalk
    setSupportActionBar(toolbar);

    // in het begin is er slechts één tabblad
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gebeurtenisbeheerder
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // er is een tabblad geselecteerd – het weergegeven fragment wordt gewijzigd door de fragmentcontainer
        int position = tab.getPosition();
        if (position == 0) {
          // tabblad ‘verzoek’
          showView(0);
        } else {
          // tabblad ‘antwoord’ – afhankelijk van het gekozen voorbeeld
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // aanmaken van antwoordfragmenten
    createResponseFragments();

    // beheer van het laadbeeld
    loadingPanel.setVisibility(View.INVISIBLE);
}

Deze code is vrij standaard in een activiteit. Laten we enkele punten toelichten:

  • regel 19 verwijst naar de volgende 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";
}
  • regels 31-33: hier wordt het eerste tabblad aangemaakt met de titel [Request]. Op een gegeven moment hebben we in het geheugen:
    • het fragment [Request];
    • n fragmenten van het type [ExampleXXFragment];

Het eerste tabblad toont altijd het fragment [Request]. Het tweede tabblad toont het fragment [ExampleXXFragment] dat overeenkomt met het door de gebruiker gekozen voorbeeld. Het fragment dat door het tweede tabblad wordt weergegeven, verandert dus in de loop van de tijd;

  • regels 37-48: de code die wordt uitgevoerd wanneer de gebruiker op een van de tabbladen klikt;
  • regel 43: fragment nr. 0 wordt weergegeven;
  • regel 46: het fragment dat momenteel in gebruik is (weergegeven), wordt weergegeven. Het nummer ervan wordt in de sessie opgehaald;
  • regel 62: de fragmenten van alle voorbeelden in de voorbeeldspinner in de weergave [RequestFragment] (eerste tabblad) worden aangemaakt;
  • regel 65: de laadafbeelding is voorlopig verborgen;

Om de methode [showView] (regels 43, 46) en de methode [createResponseFragments] te begrijpen, moeten we eerst de fragmentbeheerder in het geheugen introduceren (klasse opgenomen in het Java-bestand van MainActivity):


  // de fragmentbeheerder – moet de methoden getItem en getCount definiëren
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // de beheerde fragmenten
    private MyFragment[] fragments;

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

    // moet fragment nr. positie weergeven
    @Override
    public MyFragment getItem(int position) {
      // het fragment
      return fragments[position];
    }

    // geeft het aantal te beheren fragmenten weer
    @Override
    public int getCount() {
      // aantal fragmenten
      return fragments.length;
    }
  }
}
  • De klasse [SectionsPagerAdapter] is een uitbreiding van de Android-klasse [FragmentPagerAdapter]. Deze herdefinieert twee methoden van de bovenliggende klasse:
    • de methode [getItem], regel 15;
    • de methode [getCount], regel 22;
  • de klasse [SectionsPagerAdapter] bevat alle fragmenten van de toepassing. Deze worden opgeslagen in regel 5. Merk op dat ze van het type [MyFragment] zijn, zoals beschreven in paragraaf 9.3.7.1;
  • regel 8: om te worden opgebouwd, ontvangt de klasse [SectionsPagerAdapter] de fragmenten die zij moet beheren;
  • regels 14-18: de methode [getItem] retourneert het fragment op positie [position];
  • regels 21-25: de methode [getCount] retourneert het totale aantal fragmenten;

De methode [createResponseFragments] maakt alle fragmenten aan die de applicatie nodig heeft:


private void createResponseFragments() {
    // spinner met voorbeelden
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Geef de lay-out op die moet worden gebruikt wanneer de keuzelijst verschijnt
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // de adapter wordt in de sessie geplaatst zodat de weergave [Request] deze kan ophalen
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • regel 3: er wordt een adapter aangemaakt voor de spinner van de voorbeelden, in dit geval een lijst van String met de namen van de voorbeelden. Deze namen staan in het bestand [layout/exemples.xml]:
  

Het bestand [exemples.xml] bevat de volgende code:


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

Regel 1: dit bestand is de tweede parameter van de methode [createFromResource]. In [R.array.exemples] is [exemples] de naam van de tabel (zie regel 3 hierboven), niet de naam van het bestand.

  • regel 5: er wordt een layout (weergavebeheerder) aan de adapter gekoppeld. Nu beschikt de adapter zowel over de gegevens als over de weergavemodus;
  • regel 7: de adapter wordt in een sessie geplaatst. Daar wordt hij opgehaald door het fragment [RequestFragment] dat hem nodig heeft;

Laten we verdergaan met de code van de methode [createResponseFragments]:


private void createResponseFragments() {
    // voorbeelden van spinners
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Geef de lay-out op die moet worden gebruikt wanneer de lijst met keuzemogelijkheden verschijnt
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // we plaatsen de adapter in de sessie zodat de weergave [Request] deze kan ophalen
    session.setSpinnerExemplesAdapter(adapter);
    // aanmaken van de tabel met fragmenten (1 query, n antwoorden)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // fragment van de aanvraag
    tFragments[0] = new RequestFragment();
    // fragmenten van de antwoorden
    for (int i = 1; i < tFragments.length; i++) {
      // de naam van het te instantiëren fragment wordt samengesteld op basis van het door de gebruiker gekozen voorbeeld
      // deze naam moet de volledige naam met het bijbehorende pakket zijn – hier wordt deze direct gekoppeld aan het nummer van het voorbeeld in de spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // het fragment dat bij het voorbeeld hoort, wordt geïnstantieerd
      MyFragment fragment;
      try {
        // instantie van de klasse
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // het fragment is aangemaakt – we voegen het toe aan de array
      tFragments[i] = fragment;
    }
    // het fragmentbeheer wordt geïnstantieerd met deze nieuwe fragmenten
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Configureer de ViewPager met de sectie-adapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navigatie tussen pagina’s – deze instructie is belangrijk
    // hier wordt aangegeven dat aan weerszijden van de weergegeven weergave de geïnitialiseerde weergaven [tFragments.length] behouden moeten blijven
    // dit betekent hier dat  alle door de applicatie gebruikte fragmenten in het geheugen staan en geïnitialiseerd zijn
    // als dit niet gebeurt, is de standaardwaarde van [OffscreenPageLimit] 1
    // dus als fragment nr. 3 wordt weergegeven, worden alleen de fragmenten 2 en 4 geïnitialiseerd
    // dit gebeurt door de methode [onCreateView] van deze twee fragmenten aan te roepen – dit betekent dat in deze methode moet worden voorzien in
    // de visuele weergave van het fragment te herstellen zoals die was toen het voor het laatst werd gebruikt – bovendien mag er in deze methode
    // geen code mag staan die niet tegen twee keer uitvoeren kan – dat zorgt voor een enorme puinhoop en is moeilijk te beheren
    // hier hebben we ervoor gekozen deze moeilijkheden te vermijden – in de logbestanden is te zien dat bij het opstarten van de applicatie alle fragmenten worden aangemaakt
    // en dat hun methode [onCreateView] wordt uitgevoerd – daarna gebeurt dat nooit meer –
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // we schakelen het swipen tussen fragmenten uit
    mViewPager.setSwipeEnabled(false);
  }
  • regel 9: aanmaken van de array die alle fragmenten van de applicatie zal bevatten;
  • regel 11: het eerste fragment is dat van de aanvraag;
  • regels 13-28: we gaan evenveel fragmenten aanmaken als er voorbeelden zijn. Deze fragmenten zijn allemaal afgeleid van het antwoordfragment [ResponseFragment] en implementeren alleen wat specifiek is voor het voorbeeld: het genereren van de waargenomen waarden. Deze verschillen namelijk van voorbeeld tot voorbeeld;
  • regel 16: het fragment van een voorbeeld heeft een standaardnaam: ExampleXXFragment, waarbij XX de positie in de voorbeeldspinner is, vermeerderd met 1. XX is tevens het nummer van het fragment van het voorbeeld in de fragmentbeheerder;
  • regel 21: instantiëren van het fragment van voorbeeld nr. i uit de spinner:
    • Class.forName(exampleName): laadt het fragment in het geheugen;
    • Class.forName(exampleName).getConstructors()[0]: haalt de referentie op van de eerste constructor van de klasse. De klasse ExampleXXFragment heeft slechts één constructor. Er wordt dus een verwijzing naar deze constructor opgehaald;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) maakt een object van het type ExampleXXFragment aan met behulp van de constructor uit de vorige stap. new Object[]{} vertegenwoordigt de parameters die aan deze constructor worden doorgegeven. Aangezien de constructor van de klasse ExampleXXFragment geen parameters verwacht, wordt een lege objectenarray doorgegeven;
  • regel 27: dit fragment wordt toegevoegd aan de fragmentenarray;
  • regel 30: we hebben gezien dat de constructor van de fragmentbeheerder [SectionsPagerAdapter] in zijn parameters de array met fragmenten verwachtte die hij moest beheren. Deze wordt nu aan hem doorgegeven;
  • regel 22: de fragmentcontainer [mViewPager] van de weergave die gekoppeld is aan de activiteit [MainActivity] wordt hier gekoppeld aan de fragmentbeheerder: de fragmentcontainer [mViewPager] geeft de fragmenten van de fragmentbeheerder weer;
  • regel 43: we lezen de opmerkingen – de instructie komt erop neer dat alle fragmenten in de toestand moeten blijven waarin de code ze plaatst, ongeacht welk fragment momenteel wordt weergegeven. Zodat wanneer we ernaar terugkeren, we het terugvinden in de toestand waarin we het hebben achtergelaten;
  • regel 45: de fragmentcontainer [mViewPager] is van het type [MyPager], waarmee het vegen kan worden uitgeschakeld;

De methode [MainActivity.showView] is als volgt:


  // weergaveweergave nr. [position]
  private void showView(int position) {
    // het fragment wordt vernieuwd voordat het wordt weergegeven
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // de gevraagde weergave wordt weergegeven – er wordt direct naar de weergave gegaan (tweede parameter op false)
    // zonder deze parameter gaat men standaard naar de gewenste weergave door de tussenliggende weergaven snel weer te geven – ongewenst gedrag
    mViewPager.setCurrentItem(position, false);
}
  • regel 3: we willen fragment nr. positie weergeven;
  • regel 4: dit fragment wordt opgevraagd bij de fragmentbeheerder en vervolgens vernieuwd. Sinds de laatste keer dat het werd weergegeven, kan de sessie namelijk zijn gewijzigd. Het fragment moet deze dan controleren om te zien of het moet worden bijgewerkt;
  • regel 7: het fragment wordt weergegeven door [ViewPager]. Aangezien dit is gekoppeld aan de fragmentbeheerder, wordt fragment nr. [position] weergegeven, het fragment dat zojuist in regel 4 is vernieuwd;

Laten we afsluiten met de twee methoden voor het beheer van de wachtrij:


  public void beginWaiting() {
    // beheer van de laadafbeelding
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // beheer van de laadafbeelding
    loadingPanel.setVisibility(View.INVISIBLE);
    // einde uitvoering
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. Het fragment [RequestFragment]

De klasse [RequestFragment] ziet er als volgt uit:


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 van de webservice
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // aantal verzoeken
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // generatie-interval
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // vertraging
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // aantal gegenereerde waarden
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // knop
  private Button btnExecuter;
  // lijst met antwoorden
  private ListView listReponses;
  private TextView infoReponses;
  // spinner met voorbeelden
  private Spinner spinnerExemples;

  // de invoer
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // de activiteit en de sessie worden opgehaald
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // het fragment wordt aangemaakt op basis van de definitie XML
    View view = inflater.inflate(R.layout.request, container, false);
    // componenten
    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);

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

    // in eerste instantie geen foutmeldingen
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner uit de voorbeelden
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // resultaat
    return view;
  }
...
}
  • regel 16: de klasse [RequestFragment] is een uitbreiding van de klasse [MyFragment] (zie paragraaf 9.3.7.1);
  • regels 18-42: de visuele componenten van het fragment (zie paragraaf 9.3.7.2);
  • regels 45-52: de invoer die de gebruiker in het formulier heeft gedaan;
  • de constructor (regels 55-58) en de methode [onCreateView] worden uitgevoerd wanneer de activiteit [MainActivity] alle fragmenten van de applicatie aanmaakt. Dit gebeurt slechts één keer;
  • regel 61: de code van de methode [onCreateView] is standaard. Let op regel 102: de adapter van de spinner uit de voorbeelden wordt in de sessie opgenomen. Merk ook op in regel 91 dat het klikken op de knop [Exécuter] wordt afgehandeld door de methode [doExecuter];
  • regels 64-65: de velden [activity] en [session] behoren tot de bovenliggende klasse [MyFragment];

De methode [doExecuter] is als volgt:


  // de invoer
  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() {
    // geldige invoer?
    if (isPageValid()) {
      // de gegevens worden in de sessie opgeslagen
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // de URL van de webservice wordt opgeslagen
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // actie bezig
      session.setOnAir(true);
      // maar nog niet gestart
      session.setOperationStarted(false);
      // het fragment van het antwoord wordt weergegeven
      activity.selectTab(Constants.VUE_RESPONSE);
      // het wachten begint
      beginWaiting();
    }
}
  • regel 15: we zullen de methode [ispageValid] niet toelichten. Deze controleert de geldigheid van de invoer en retourneert alleen ‘true’ als alle gegevens geldig zijn. In dat geval worden ze gebruikt om de velden op de regels 2-9 te initialiseren;
  • regel 17: de verschillende invoergegevens worden in de sessie opgeslagen:
    • [spinnerExemples.getSelectedItem().toString()] is de naam van het door de gebruiker geselecteerde voorbeeld en wordt opgeslagen in [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] is het nummer van het fragment dat aan het voorbeeld is gekoppeld en dat (het fragment) door de fragmentbeheerder is opgeslagen. Dit nummer wordt opgeslagen in [session.examplePosition];
  • regel 19: de URL van de webservice / jSON wordt doorgegeven aan de activiteit, die deze op haar beurt doorgeeft aan de laag [DAO];
  • regels 21-24: er wordt aangegeven dat een bewerking gaat starten;
  • regel 26: het tabblad met het antwoord wordt weergegeven. Om te begrijpen wat er gaat gebeuren, moet men de code [MainActivity.selectTab] in gedachten houden:

  // selectie van een tabblad
  public void selectTab(int position) {
    // er zijn maximaal 2 tabbladen
    // in eerste instantie is er slechts één, namelijk die van de aanvraag
    // als het gevraagde tabblad nr. 1 is en dit nog niet bestaat, dan moet het worden aangemaakt
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 tabblad erbij
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // het tabblad wordt programmatisch geselecteerd, wat de gebeurtenis [onTabSelected] activeert
    // waardoor de juiste weergave aan dit tabblad wordt gekoppeld
    tabLayout.getTabAt(position).select();
}
  • aanvankelijk had de activiteit alleen het tabblad van de aanvraag (tabblad nr. 0) aangemaakt;
  • regels 6-11: het tabblad van het antwoord (tabblad nr. 1) wordt aangemaakt als dit nog niet was gebeurd;
  • regel 14: tabblad nr. position (0 of 1) wordt geselecteerd. Hierdoor wordt de gebeurtenis [onTabSelected] in de wachtrij van de event loop van de Android-applicatie geplaatst;

De gebeurtenishandler voor [onTabSelected] in [MainActivity] is als volgt:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // er is een tabblad geselecteerd – het weergegeven fragment wordt gewijzigd door de fragmentcontainer
        int position = tab.getPosition();
        if (position == 0) {
          // tabblad ‘verzoek’
          showView(0);
        } else {
          // tabblad ‘antwoord’ – afhankelijk van het gekozen voorbeeld
          showView(session.getExamplePosition());
        }
}

In het geval van het tabblad [Response] wordt regel 9 uitgevoerd. Fragment nr. [session.getExamplePosition()] wordt weergegeven. Bijvoorbeeld, voor het voorbeeld [exemple-03] is het nummer dat is opgeslagen in [session.examplePosition] 3. Regel 10 geeft dan fragment nr. 3 weer. De tabel met fragmenten die aanvankelijk door de activiteit is opgebouwd, is [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Het is dus inderdaad het fragment [Exemple03Fragment] dat wordt weergegeven. Dit gebeurt met de volgende code:


  // weergave van weergave nr. [position]
  private void showView(int position) {
    // het fragment wordt vernieuwd voordat het wordt weergegeven
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // de gevraagde weergave wordt weergegeven – men gaat direct naar de weergave (tweede parameter op false)
    // zonder deze parameter gaat men standaard naar de gewenste weergave door de tussenliggende weergaven snel weer te geven – ongewenst gedrag
    mViewPager.setCurrentItem(position, false);
}

We zien dat het fragment wordt vernieuwd (regel 4) voordat het wordt weergegeven (regel 7).

9.3.7.6. Het fragment [ResponseFragment]

De klasse [ResponseFragment] geeft de antwoorden van de server weer. De code ervan is als volgt:


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 {

  // lijst met antwoorden
  private ListView listReponses;
  private TextView infoReponses;
  // knop
  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) {
    // de activiteit en de sessie worden opgehaald
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // het fragment wordt op basis van de definitie weergegeven XML
    View view = inflater.inflate(R.layout.response, container, false);
    // componenten
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // knop [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // resultaat
    return view;
  }
...
  // uit te voeren methode (via expliciete code) vóór elke weergave van het fragment
  public void onRefresh() {
...
  }
}
  • regel 21: de klasse [ResponseFragment] is een uitbreiding van de klasse [MyFragment];
  • regels 23-27: de componenten van het fragment;
  • regels 32-36: de constructor wordt slechts één keer uitgevoerd, bij de eerste aanmaak van de voorbeeldfragmenten door de activiteit. Alle voorbeeldfragmenten zijn namelijk afgeleid van het fragment [ResponseFragment]. Bij het instantiëren ervan wordt de constructor van hun bovenliggende klasse [ResponseFragment] aangeroepen;
  • regel 35: initialiseert de mapper jSON uit regel 30 die wordt gebruikt om de tekenreeks jSON van een uitzonderingsstapel weer te geven;
  • regels 38-59: de methode [onCreateView] wordt slechts één keer uitgevoerd, bij de eerste aanmaak van de fragmenten van de voorbeelden door de activiteit. Hierin staat klassieke code voor een Android-applicatie;
  • regels 52-56: de methode die wordt uitgevoerd wanneer op de knop [Annuler] wordt geklikt, is de methode [doAnnuler];
  • regels 62-64: de methode [onRefresh] wordt telkens uitgevoerd wanneer het tabblad [Response] wordt weergegeven;

Dankzij de verschillende logboekvermeldingen in de belangrijke methoden kunnen we zien wat er gebeurt bij het opstarten van de applicatie:

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
  • regel 1: opbouw van het fragment [RequestFragment];
  • regels 2-9: opbouw van de fragmenten van de 4 voorbeelden van de applicatie;
  • regel 10: initialisatie van het fragment [RequestFragment];
  • regels 11-14: initialisatie van de fragmenten van de vier voorbeelden van de applicatie;

Daarna komen deze methoden niet meer voor.

De methode [ResponseFragment.onRefresh] is als volgt:


  // methode die (via expliciete code) vóór elke weergave van het fragment moet worden uitgevoerd
  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()));
    // wordt momenteel uitgevoerd?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // verzoek wordt uitgevoerd
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • regel 5: er wordt gecontroleerd of het fragment [RequestFragment] een verzoek heeft gedaan (session.isOnAir) en of dit is gestart (isOperationStarted). Als het fragment [RequestFragment] een verzoek heeft gedaan en dit verzoek nog niet wordt uitgevoerd, wordt de bewerking gestart (regels 7-8);
  • zodra de bewerking is gestart, kan de gebruiker, aangezien deze asynchroon is, tussen de twee tabbladen schakelen. Als hij teruggaat naar het tabblad [Response] en er is een bewerking bezig, dan worden de regels 7-8 niet uitgevoerd;

De methode [doExecuter] in regel 8 voert de door de gebruiker gevraagde bewerking uit:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // begin van de wachttijd
    beginWaiting();
    // voorbereiding uitvoering
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // de observables van het gekozen voorbeeld worden aangemaakt en uitgevoerd
    createAndExecuteObservables();
}

// methode geïmplementeerd door onderliggende klassen
protected abstract void createAndExecuteObservables();
  • regel 10: maakt observables aan, voert ze uit en observeert ze. Deze zijn voor elk voorbeeld verschillend. Daarom is de methode [createAndExecuteObservables] abstract (regel 14). Deze wordt geïmplementeerd door de fragmenten [ExampleXXFragment] die de klasse [ResponseFragment] uitbreiden;
  • regel 6: de lijst met abonnementen wordt leeggemaakt;
  • regel 7: de lijst met antwoorden wordt leeggemaakt;
  • regel 8: telt het aantal ontvangen antwoorden;

De dochterklassen [ExampleXXFragment] delegeren de taak om de elementen die zij observeren weer te geven aan de volgende methode [showAlea]:


  protected void showAlea(String data) {
    // nog wat extra informatie
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 extra antwoord
    reponses.add(0, data);
    Log.d("rxjava", data);
    // update van de UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • regel 1: we zien dat het waargenomen element in de vorm van een tekenreeks binnenkomt. Dit is in feite de tekenreeks jSON van het waargenomen element. Hierdoor kunnen we het waargenomen element op één enkele manier weergeven, ongeacht het exacte Java-type ervan;
  • regel 6: het waargenomen element [data] wordt als eerste toegevoegd aan de lijst met antwoorden. De gebruiker ziet dus bovenaan de lijst de meest recente antwoorden;

Het wachten wordt afgehandeld door de volgende methoden [beginWaiting] en [cancelWaiting]:


  private void beginWaiting() {
    // het zandloperpictogram wordt weergegeven
    activity.beginWaiting();
    // de knop [Annuler] wordt weergegeven
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // het wachten is voorbij
    activity.cancelWaiting();
    // de knop [Annuler] is verborgen
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Deze maken gebruik van de gelijknamige methoden van de activiteit en zorgen vervolgens alleen voor het weergeven of verbergen van de knop [Annuler].

Het klikken op de knop [Annuler] wordt afgehandeld door de volgende code:


  protected void doAnnuler() {
    // alle abonnementen worden opgezegd
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // einde van het wachten
    cancelWaiting();
}
  • regels 3-7: alle abonnementen worden één voor één opgezegd;

9.3.8. Voorbeelden van observables

9.3.8.1. Exemple-01

De klassen [ExampleXXFragment] hebben als functie het aanmaken, uitvoeren en observeren van observables. De weergave van de waargenomen waarden gebeurt door de bovenliggende klasse [ResponseFragment].

De klasse [Example01Fragment] ziet er als volgt uit:

  

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 {

    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;

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

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // er worden willekeurige getallen gevraagd
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // waarneembare configuratie nr. i
            // verzoek aan de server
            Request request = session.getRequest();
            request.setId(i);
            // observable uitgevoerd op de berekeningsthread
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observatie op de thread van de event loop;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // al deze observables worden uitgevoerd
        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) {
        // de weer te geven informatie wordt opgehaald
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • regel 36: de enige observable die wordt gegenereerd;
  • regels 37-44: generatie en configuratie van de verschillende observables die (in regel 43) worden samengevoegd tot de observable van regel 36;
  • regel 43: de observable wordt uitgevoerd in een thread van de scheduler [Schedulers.io()]. De aanroep HTTP naar de server wordt in deze thread uitgevoerd;
  • regel 46: de uiteindelijke observable wordt geobserveerd in de thread van de event loop;
  • regels 48-57: uitvoering van de observables, dus van de verzoeken aan de server voor willekeurige getallen. Android ondersteunt Java 8 en de bijbehorende lambda's nog niet. Daarom worden hier anonieme klassen gebruikt om de functionele interfaces van RxJava te instantiëren;
  • regels 49-52: actie die wordt uitgevoerd wanneer de waarnemer een nieuw element van het type [AleasDaoResponse] ontvangt van de waarneembare (zie paragraaf 9.3.6.1);
  • regel 51: aanroep van de methode [showAlea] van de bovenliggende klasse. We herinneren ons dat deze methode een tekenreeks verwacht. Deze wordt geleverd door de methode [getDataFrom] in de regels 59-68;
  • regel 63: de tekenreeks jSON van het type [AleasUiResponse] wordt als volgt geretourneerd:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // antwoord [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // observatiethread
  private String observedOn;
  // observatietijdstip
  private String observedAt;

  // constructors
  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;
  }
// getters en setters
...
}
  • aan het antwoord van de laag [DAO] (regel 11) worden twee gegevens toegevoegd:
    • regel 13: de observatiethread;
    • regel 15: het tijdstip van waarneming;

Laten we teruggaan naar de abonnementscode:


    @Override
    public void createAndExecuteObservables() {
...
        // we voeren al deze observables uit
        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) {
                // de uitzondering wordt weergegeven
                showAlea(getMessagesFromThrowable(th));
                // na het ontvangen van een uitzondering ontvangt de observable noch onNext, noch onCompleted
                // moet het abonnement handmatig opzeggen
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // einde wachttijd
                cancelWaiting();
            }
        }));
}
  • regels 11-18: het geval waarin de waarnemer een uitzondering ontvangt;
  • regel 14: we gebruiken opnieuw de methode [showAlea] van de bovenliggende klasse om de uitzondering weer te geven. De methode [getMessagesFromThrowable] is een methode van de bovenliggende klasse [ResponseFragment] die op basis van een uitzondering een tekenreeks genereert:

  // uitzonderingsberichten
  protected String getMessagesFromThrowable(Throwable ex) {
    // er wordt een lijst aangemaakt met de foutmeldingen uit de uitzonderingsstack
    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();
    }
}
  • regel 11: de tekenreeks jSON wordt geretourneerd uit een lijst met foutmeldingen (regel 4);

Laten we teruggaan naar de code voor het abonneren op de observable:

  • regels 19-25: de code die wordt uitgevoerd wanneer de observer de melding ontvangt dat de verzending is beëindigd. Vervolgens wordt het wachten geannuleerd (regel 23), waardoor de grafische interface wordt bijgewerkt;

Een uitvoerresultaat van voorbeeld 01 levert een resultaat op dat vergelijkbaar is met het volgende:

Image

Elk element in de lijst is de tekenreeks jSON van een waargenomen waarde. De velden van de tekenreeks jSON zijn als volgt:

  • aleas: de lijst met willekeurige getallen die door de server wordt geleverd;
  • idClient: het nummer van de aanvraag (te zien is dat de antwoorden in willekeurige volgorde zijn teruggekomen);
  • on: de uitvoeringsthread van de observable die deze waarde heeft gegenereerd;
  • requestAt: tijdstip van het verzoek van de client;
  • responseAt: tijdstip van het antwoord van de server;
  • delay: door de server waargenomen wachttijd;
  • fout: door de server geretourneerde foutcode (0 = geen fout);
  • message: door de server teruggestuurd foutbericht (null = geen fout);
  • observedAt: tijdstip waarop de waargenomen waarde is geregistreerd;
  • observedOn: thread waarin de waargenomen waarde is geregistreerd;

9.3.8.2. Exemple-02

De klasse [Example02Fragment] is als volgt:


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 {

    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;

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

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // willekeurige getallen opvragen
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // verzoek voorbereiden
            Request request = session.getRequest();
            request.setId(i);
            // alleen observables met een even klantnummer worden behouden
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // uitvoering op I/O-thread
                            .subscribeOn(Schedulers.io()));
        }
        // waarneming op de thread van de event loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // deze observables worden uitgevoerd
        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() {
                // einde wachten
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // de weer te geven informatie wordt opgehaald
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Dit voorbeeld is vergelijkbaar met het vorige (regel 38). Maar van de waarnemingen uit het vorige voorbeeld worden alleen de waarnemingen met een even klantnummer (regels 42-46) behouden, dankzij de methode [filter] (regel 41).

De verkregen resultaten zijn als volgt (voor 10 verzoeken):

Image

9.3.8.3. Exemple-03

De klasse [Example03Fragment] is als volgt:


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 {

  // mappers jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // er worden willekeurige getallen opgevraagd
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // verzoek voorbereiden
      Request request = session.getRequest();
      request.setId(i);
      // configuratie van de observabel
      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();
        }
      })
        // uitvoering op I/O-thread
        .subscribeOn(Schedulers.io()));
    }
    // waarneming op de thread van de event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // deze observables worden uitgevoerd
    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() {
            // einde wachten
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // de weer te geven informatie wordt opgehaald
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Dit voorbeeld is vergelijkbaar met Voorbeeld-02:

  • regel 40: we definiëren dezelfde observables als in Voorbeeld-02;
  • regel 45: elke waarde die door de voorgaande observables wordt uitgezonden, wordt via de methode [map] omgezet in een List<Integer>-type, wat de lijst is met willekeurige getallen die door de server worden gegenereerd;
  • regel 58: de waargenomen waarde is nu van het type List<Integer>;

Het resultaat voor 10 verzoeken is als volgt:

Image

9.3.8.4. Exemple-04

De klasse [Example04Fragment] is als volgt:


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 {

  // mappers jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // er worden willekeurige getallen opgevraagd
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // verzoek voorbereiden
      Request request = session.getRequest();
      request.setId(i);
      // configuratie van 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;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // uitvoering op een I/O-thread
        .subscribeOn(Schedulers.io()));
    }
    // waarneming op de thread van de event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // deze observables worden uitgevoerd
    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() {
            // einde van het wachten
            cancelWaiting();
          }
        }
      ));

  }
}

Dit voorbeeld is vergelijkbaar met Voorbeeld-03, behalve dat in regel 42 in plaats van de methode [map] de methode [flatMap] wordt gebruikt.

  • regel 55: merk op dat het type van de waargenomen waarde nu Integer is;

Voor 10 verzoeken krijgt men de volgende resultaten:

Image

Deze keer zijn er meer waargenomen waarden dan zoekopdrachten.

9.3.8.5. Exemple-05

We leggen nu de procedure uit die moet worden gevolgd om een nieuw voorbeeld van waarnemingen aan de applicatie toe te voegen.

Stel dat we het voorbeeld [Exemple22h] uit paragraaf 7.6.4 willen namaken:


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 {
        // proces
        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()));
        // abonnementen
        ProcessUtils.subscribe(1, process);
    }
}
  • de waarden van de observable [Observable.range(1, 10)] worden eerst gegroepeerd in even en oneven waarden door de methode [groupBy] (regel 11) en vervolgens samengevoegd tot één enkele observable door de methode [concatMap] (regel 12);

stap 1

We maken een nieuw voorbeeld aan in het bestand [exemples.xml]:

  

<!-- voorbeelden -->
<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>

Hierboven is regel 8 toegevoegd. De naam van het voorbeeld mag willekeurig zijn.

stap 2

We dupliceren de klasse [Example04Fragment] naar [Example05Fragment]. Hier is de naam vastgelegd.

stap 3

We wijzigen de code van [Example05Fragment] als volgt:


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 {

  // mappers jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instanties van functionele interfaces
    // 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();
      }
    };
    // er worden willekeurige getallen gevraagd
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // verzoek wordt voorbereid
      Request request = session.getRequest();
      request.setId(i);
      // configuratie van de observable
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // uitvoering op een I/O-thread
        .subscribeOn(Schedulers.io());
    }
    // waarneming op een thread van de event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // deze observables worden uitgevoerd
    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() {
            // einde van het wachten
            cancelWaiting();
          }
        }
      ));

  }
}
  • regel 67: vertegenwoordigt de observable uit voorbeeld 04: een stroom van gehele getallen;
  • regel 68: we groeperen deze stroom van gehele getallen op basis van een booleaanse voorwaarde die we gaan definiëren. We krijgen een observable van het type Observable<GroupedObservable<Boolean, Integer>> die dus elementen van het type GroupedObservable<Boolean, Integer> produceert;
  • regel 68: de methode [concatMap] zal elementen van het type Integer genereren op basis van de elementen van het type GroupedObservable<Boolean, Integer>;
  • regels 32-59: om het aanmaken van de observable in regels 67-69 leesbaarder te maken, hebben we de instanties van functionele interfaces geïsoleerd die de verschillende [filter, flatMap, groupBy, concatMap]-operatoren nodig hebben;
  • regels 47-52: de methode [groupBy] verwacht een parameter van het type Func1<T,K>, waarbij T het type is van de gegroepeerde elementen en K het type van het groeperingscriterium. Op basis van het element T is de instantie Func1<T,K> verantwoordelijk voor het genereren van de groeperingssleutel K van het element;
  • regels 48-51: de elementen van het type Integer worden gegroepeerd op pariteit. De instantie Func1<Integer,Boolean> genereert de sleutel true of false, afhankelijk van of het element in de ene of de andere groep moet worden geplaatst. Als resultaat zijn er twee groepen: de groep met even elementen met sleutel true en de groep met oneven elementen met sleutel false;
  • regels 53-59: de methode [concatMap] verwacht een parameter van het type Func1<T,Observable<R>> en produceert een observable van elementen van het type R. Het type T is hier het type dat wordt uitgegeven door de operator [groupBy], in dit geval een type GroupedObservable<Boolean, Integer>;
  • regel 57: uit het element van het type [GroupedObservable<Boolean, Integer>] wordt een type Observable<Integer> gegenereerd. Aangezien de operator [groupBy] twee groepen heeft geproduceerd, zal de operator [concatMap] twee observables van het type [Observable<Integer>] produceren. Net als [flatMap] zal deze ze samenvoegen tot één enkele observable. Maar in tegenstelling tot [flatMap] mengt deze de elementen van de samengevoegde observables niet. We moeten dus twee afzonderlijke groepen waarnemen: de even willekeurige getallen en de overige oneven getallen.

stap 4

We voeren de toepassing uit:

Image

en we krijgen de volgende resultaten:

Image

  • in [1] de even willekeurige getallen, in [2] de oneven;

9.3.8.6. Om verder te gaan

De lezer wordt nu uitgenodigd om zijn eigen voorbeelden te maken en ook te experimenteren met verschillende waarden voor de invoer in het formulier waarmee de verzoeken aan de willekeurige-getallenserver worden geconfigureerd.

9.3.9. Conclusie

We hebben in de Android-omgeving de volgende architectuur gecreëerd:

De Android-client:

De laag [DAO] communiceert met de server die de willekeurige getallen genereert die op de Android-tablet worden weergegeven. Deze server heeft de volgende tweelaagse architectuur:

De laag [DAO] stuurde n verzoeken HTTP naar de server voor willekeurige getallen en de laag [swing] wachtte asynchroon op de resultaten daarvan om ze weer te geven. Deze n verzoeken HTTP werden naar dezelfde server gestuurd, die dezelfde soorten antwoorden leverde. Hierdoor konden we de antwoorden samenvoegen (mergeWith) tot één enkele observable.

In de praktijk richten Android-apps zich tot verschillende servers en zullen hun antwoorden waarschijnlijk niet worden samengevoegd. De verzoeken HTTP aan deze servers worden onafhankelijk van elkaar afgehandeld en de resultaten ervan worden via afzonderlijke methoden geobserveerd.