Skip to content

9. RxJava dans l'environnement Android

9.1. Introduction

Nous allons ici reprendre une application déjà traitée dans plusieurs documents :

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

On y traite d'une application client / serveur où le serveur délivre de façon asynchrone des nombres aléatoires que le client Android affiche :

  • dans le document 1, le client Android utilise une technologie non standard ;
  • dans le document 2, le client Android utilise la technologie standard d'Android pour les opérations asynchrones ;
  • dans le document 3, le client Android utilise la même technologie que dans le document 2 mais simplifiée par l'usage des annotations de la bibliothèque Android Annotations ;

Le client Android est le suivant :

La couche [DAO] communique avec le serveur qui génère les nombres aléatoires affichés par la tablette Android. Ce serveur a une architecture à deux couches suivante :

Les clients interrogent certaines URL de la couche [web / JSON] et reçoivent une réponse texte au format JSON (JavaScript Object Notation).

Nous allons décomposer l'étude de l'application en deux étapes :

Le serveur web / jSON

  • sa couche [métier] ;
  • son service [web / JSON] implémenté avec Spring MVC ;

Le client Android

  • sa couche [DAO] ;
  • son activité ;
  • ses vues ;

9.2. Le service web / jSON

Note : le service web / jSON est implémenté par la technologie Spring MVC. Le lecteur ne connaissant pas celle-ci peut soit :

  • se contenter de lire le paragraphe 9.2.1 qui explique comment lancer le serveur et comment l'interroger ;
  • consulter le document [Spring MVC et Thymeleaf par l'exemple], notamment le chapitre 4 qui présente les principales annotations utilisées dans le code ;

9.2.1. Le projet Intellij Idea

Le service web / jSON a l'architecture suivante :

Cette architecture est implémenté par le projet Intellij Idea suivant [1] :

Le serveur est lancé par [2-3]. Des logs sur la console sont alors affichés :

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
  • ligne 12 : indique que le service est disponible sur le port 8080 ;
  • ligne 10 : l'unique URL du service web / jSON disponible via une opération HTTP GET. Ses paramètres sont les suivants :
    • [a,b] : intervalle de génération des nombres aléatoires ;
    • [minCount, maxCount] : count nombres aléatoires sont générés, où count est un nombre aléatoire dans l'intervalle [minCount, maxCount] ;
    • [minDelay, maxDelay] : le service attend delay millisecondes avant de renvoyer les nombres demandés où delay est un nombre aléatoire dans [minDelay, maxDelay] ;

Dans un navigateur, demandons cette URL :

 

On a demandé :

  • des nombres aléatoires dans l'intervalle [100, 200] ;
  • n nombres aléatoires avec n dans l'intervalle [10, 20] ;
  • un délai d'attente de x millisecondes avec x dans l'intervalle [300, 400] ;

Dans la réponse :

  • aleas : liste des nombres aléatoires générés ;
  • delay : le temps d'attente en millisecondes que le serveur a retenu ;
  • erreur : un code d'erreur - 0 si pas d'erreur ;
  • message : un message d'erreur - null si pas d'erreur ;

9.2.2. Les dépendances Gradle du projet

  

Le projet [serveur] est un projet Gradle configuré par le fichier [build.gradle] [1] suivant :


// généré par http://start.spring.io/ (mai 2016)
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

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

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

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
}
  • ligne 1 : un commentaire pour indiquer comment a été généré ce fichier de configuration ;
  • lignes 4 et 10 : une dépendance sur le framework [Spring Boot], une branche de l'écosystème Spring. Ce framework [http://projects.spring.io/spring-boot/] permet une configuration minimale de Spring. Selon les archives présentes dans le Classpath du projet, [Spring Boot] infère une configuration plausible ou probable pour celui-ci. Ainsi si Hibernate est dans le Classpath du projet, alors [Spring Boot] infèrera que l'implémentation JPA utilisée va être Hibernate et configurera Spring dans ce sens. Le développeur n'a plus à le faire. Il ne lui reste alors à faire que les configurations que [Spring Boot] n'a pas faites par défaut ou celles que [Spring Boot] a faites par défaut mais qui doivent être précisées. Dans tous les cas c'est la configuration faite par le développeur qui a le dernier mot ;
  • lignes 14-15 : deux plugins Gradle nécessaires pour exploiter le contenu de ce fichier Gradle ;
  • lignes 17-20 : définissent les caractéristiques de l'archive générée pour ce projet ;
  • lignes 22-23 : pour une compatibilité avec Java 8 ;
  • lignes 25-27 : les dépendances seront cherchées dans de dépôt global Maven ou dans le dépôt local à la machine ;
  • ligne 30 : définissent une dépendance sur l'artifact [spring-boot-starter-web]. Cet artifact amène avec lui toutes les archives nécessaires à un projet Spring MVC. Parmi celles-ci on trouve l'archive d'un serveur Tomcat. C'est lui qui sera utilisé pour déployer l'application web. On notera que la version de la dépendance n'a pas été mentionnée. C'est celle mentionnée dans le projet importé [spring-boot] qui sera utilisée ;

Pour mettre à jour le projet, il faut forcer le téléchargement des dépendances [1-3] :

Regardons [4] les dépendances amenées par ce fichier [build.gradle] :

 

Elles sont très nombreuses. Spring Boot pour le web a inclus les dépendances dont une application web Spring MVC aura probablement besoin. Cela veut dire que certaines sont peut être inutiles. Spring Boot est idéal pour un tutoriel :

  • il amène les dépendances dont nous aurons probablement besoin ;
  • nous allons voir qu'il simplifie considérablement la configuration du projet Spring MVC ;
  • il amène un serveur Tomcat embarqué [1] ce qui nous évite le déploiement de l'application sur un serveur web externe ;
  • il permet de générer un jar exécutable incluant toutes les dépendances ci-dessus. Ce jar peut être transporté d'une plate-forme à une autre sans reconfiguration.

On trouvera de nombreux exemples utilisant Spring Boot sur le site de l'écosystème Spring [http://spring.io/guides]. Maintenant que nous connaissons les dépendances du projet, nous pouvons passer au code.

9.2.3. La couche [métier]

  

La couche [métier] aura l'interface [IMetier] suivante :


package dvp.rxjava.server.metier;

public interface IMetier {
  // nombres aléatoires dans l'intervalle [a,b]
  // n nombres sont générés avec n lui-même nombre aléatoire dans l'intervalle [minCount, maxCount]
  // les nombres sont générés après une attente de delay millisecondes,
  // où [delay] est lui-même un nombre aléatoire dans l'intervalle [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Cette interface est quasi identique à celle étudiée dans l'environnement Swing au paragraphe 8.4. Ligne 8, la méthode [getAleas] rend le type [AleasMetier] suivant :


package dvp.rxjava.server.metier;

import java.util.List;

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

  // constructeurs
  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 et setters
...
}

Le code de la classe [Metier] implémentant l'interface [IMetier] est le suivant :


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) {
    // nombres aléatoires dans l'intervalle [a,b]
    // n nombres sont générés avec n lui-même nombre aléatoire dans l'intervalle [minCount, maxCount]
    // les nombres sont générés après une attente de delay millisecondes,
    // où [delay] est lui-même un nombre aléatoire dans l'intervalle [minDelay, maxDelay]

    // qqs vérifications
    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;
    }
    // erreurs ?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // générateur de nombres aléatoires
    Random random = new Random();
    // attente ?
    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);
      }
    }
    // génération résultat
    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));
    }
    // retour résultat
    return new AleasMetier(delay,nombres);
  }

}

Nous ne commentons pas la classe : elle est analogue à celle rencontrée dans l'environnement Swing au paragraphe 8.4. On notera simplement les points suivants :

  • ligne 10 : l'annotation Spring [@Service] qui va faire que Spring va instancier la classe en un unique exemplaire (singleton) et rendre sa référence disponible pour d'autres composants Spring. D'autres annotations Spring auraient pu être utilisées ici pour le même effet ;
  • lignes 13-14 : on injecte un mappeur jSON. Spring est un conteneur d'objets. Ce conteneur est instancié au démarrage de l'application web et des objets définis par un fichier de configuration sont alors instanciés, par défaut en un seul exemplaire (singleton). Un singleton Spring peut contenir des références sur d'autres objets Spring. C'est le cas ici : le singleton [metier] (lignes 10-11) aura une référence sur le singleton [mapper] (lignes 13-14). On appelle cela une injection de dépendance. Il y a deux façons d'injecter un singleton dans un autre singleton :
    • par son type : cela est possible si le singleton à injecter est le seul objet Spring à avoir ce type. C'est le cas ici pour l'injection des lignes 13-14 (type ObjectMapper) ;
    • par son nom si plusieurs objets Spring ont le même type. Il faut alors ajouter l'annotation @Qualifier(« nomDuSingleton ») pour préciser le nom du singleton ;

La classe [Metier] lance des exceptions de type [AleaException] :


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // code d'erreur
  private int code;

  // constructeurs
  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 et setters

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • ligne 3 : [AleasException] étend la classe [RuntimeException]. C'est donc une exception non contrôlée (pas d'obligation de la gérer avec un try / catch) ;
  • ligne 6 : on ajoute à la classe [RuntimeException] un code d'erreur ;

9.2.4. Le service web / JSON

  

Le service web / JSON est implémenté par Spring MVC. Spring MVC implémente le modèle d'architecture dit MVC (Modèle – Vue – Contrôleur) de la façon suivante :

Le traitement d'une demande d'un client se déroule de la façon suivante :

  1. demande - les URL demandées sont de la forme http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] est la classe de Spring qui traite les URL entrantes. Elle "route" l'URL vers l'action qui doit la traiter. Ces actions sont des méthodes de classes particulières appelées [Contrôleurs]. Le C de MVC est ici la chaîne [Dispatcher Servlet, Contrôleur, Action]. Si aucune action n'a été configurée pour traiter l'URL entrante, la servlet [Dispatcher Servlet] répondra que l'URL demandée n'a pas été trouvée (erreur 404 NOT FOUND) ;
  1. traitement
  • l'action choisie peut exploiter les paramètres parami que la servlet [Dispatcher Servlet] lui a transmis. Ceux-ci peuvent provenir de plusieurs sources :
    • du chemin [/param1/param2/...] de l'URL,
    • des paramètres [p1=v1&p2=v2] de l'URL,
    • de paramètres postés par le navigateur avec sa demande ;
  • dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [metier] [2b]. Une fois la demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
    • une page d'erreur si la demande n'a pu être traitée correctement
    • une page de confirmation sinon
  • l'action demande à une certaine vue de s'afficher [3]. Cette vue va afficher des données qu'on appelle le modèle de la vue. C'est le M de MVC. L'action va créer ce modèle M [2c] et demander à une vue V de s'afficher [3] ;
  1. réponse - la vue V choisie utilise le modèle M construit par l'action pour initialiser les parties dynamiques de la réponse HTML qu'elle doit envoyer au client puis envoie cette réponse.

Pour un service web / JSON, l'architecture précédente est légèrement modifiée :

  • en [4a], le modèle qui est une classe Java est transformé en chaîne JSON par une bibliothèque JSON ;
  • en [4b], cette chaîne JSON est envoyée au navigateur ;

Revenons à la couche [web] de notre application :

Dans notre application, il n'y a qu'un contrôleur :

  

Le service web / JSON enverra à ses clients une réponse de type [AleasResponse] suivant :


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // code d'erreur
  private int erreur;
  // message d'erreur
  private String message;

  // constructeurs
  public AleasResponse() {

  }

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

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • ligne 5 : la classe [AleasResponse] étend la classe [AleasMetier] et en reprend donc tous les attributs (aleas, delay) ;
  • ligne 8 : un code d'erreur (0 si pas d'erreur) ;
  • ligne 10 : si erreur!=0, un message d'erreur, null si pas d'erreur ;

Le contrôleur [AleasController] est le suivant :


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 {

    // couche métier
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // nombres aléatoires dans [a,b]
    // n nombres sont générés avec n dans l'intervalle [minCount, maxCount]
    // les nombres sont générés après une attente de delay millisecondes,
    // où [delay] est un nombre aléatoire dans l'intervalle [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 {

        // on prépare la réponse
        AleasResponse response = new AleasResponse();
        // on utilise la couche métier pour générer les nombres aléatoires
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // cas d'erreur (code et message)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // on rend la réponse jSON
        return mapper.writeValueAsString(response);
    }
}
  • ligne 16 : l'annotation [@Controller] fait de la classe [AleasController] un singleton Spring. Elle indique de plus que la classe contient des méthodes qui vont traiter des requêtes pour certaines URL de l'application web. Ici, il n'y en a qu'une à la ligne 29 ;
  • lignes 20-21 : l'annotation [@Autowired] demande à Spring d'injecter dans le champ, un composant de type [IMetier]. Ce sera la classe [Metier] précédente. C'est parce que nous avons mis à celle-ci l'annotation [@Service] qu'elle est gérée comme un composant Spring ;
  • lignes 22-23 : l'annotation [@Autowired] demande à Spring d'injecter dans le champ un composant de type [ObjectMapper]. Nous allons prochainement définir celui-ci ;
  • ligne 31 : la méthode [getAleas] génère les nombres aléatoires. Son nom n'a pas d'importance. Lorsqu'elle s'exécute, les paramètres des lignes 31-33 ont été initialisés par Spring MVC. Nous verrons comment. Par ailleurs, si elle s'exécute, c'est parce que le serveur web a reçu une requête HTTP GET pour l'URL de la ligne 29 (attribut method) ;
  • ligne 30 : l'annotation [@ResponseBody] indique que le résultat de la méthode doit être envoyé tel quel au client. Ici, nous allons lui envoyer une chaîne de caractères qui sera la chaîne jSON d'un type [AleasResponse] ;
  • ligne 29 : l'URL traitée est de la forme /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay} où {x} représente une variable. Ces différentes variables sont affectées aux paramètres de la méthode aux lignes 32-33. Cela se fait via l'annotation @PathVariable("x"). On notera que les valeurs {x} sont des composantes d'une URL et sont donc de type String. La conversion de String vers le type des paramètres de la méthode peut échouer. Spring MVC lance alors une exception. Résumons : si avec un navigateur je demande l'URL /100/200/10/20/300/400, la méthode getAleas de la ligne 31 s'exécutera avec les paramètres a=100 (ligne 31), b=200 (ligne 31), minCount=10 (ligne 31), maxCount=20 (ligne 32), minDelay=300 (ligne 32), maxDelay=400 (ligne 33) ;
  • ligne 39 : on demande à la couche [métier] une liste de nombres aléatoires. On se souvient que la méthode [metier].getAleas peut lancer une exception ;
  • lignes 42-43 : cas d'erreur ;
  • ligne 46 : la réponse de type [AleasResponse] est rendue sous la forme d'une chaîne jSON ;

9.2.5. Configuration du projet Spring

  

Il existe diverses façons de configurer Spring :

  • avec des fichiers XML ;
  • avec du code Java ;
  • avec un mix des deux ;

Nous choisissons de configurer notre application web avec du code Java. C'est la classe [Config] ci-dessus qui assure cette configuration :


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 {
  // -------------------------------- configuration couche [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);
  }

  // mappeur jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • ligne 15 : on dit à Spring dans quels packages il va trouver des objets à instancier. Il en trouvera deux :
    • la classe [Metier] annotée par [@Service] ;
    • la classe [AleasController] annotée par [@Controller] ;
  • ligne 16 : l'annotation [@EnableWebMvc] induit des configurations automatiques pour le framework Spring MVC ;
  • lignes 19-20 : injection du contexte Spring (conteneur des objets Spring). Cette injection est nécessaire car l'objet des lignes 22-26 en a besoin ;
  • le fichier de configuration Spring peut définir de nouveaux objets Spring à l'aide de méthodes annotées [@Bean]. Le résulat de la méthode devient alors un objet Spring ;
  • lignes 22-26 : définition de la servlet du framework Spring MVC, celle qui route les requêtes HTTP vers le bon contrôleur et la bonne méthode. [DispatcherServlet] est une classe de Spring ;
  • lignes 28-31 : on indique que cette servlet traite toutes les URL ;
  • lignes 33-36 : c'est la présence de ce bean qui va activer le serveur Tomcat présent dans les archives du projet. Il attendra les requêtes sur le port 8080 ;
  • lignes 39-42 : un mappeur jSON. C'est celui-ci qui a été injecté dans les objets Spring [Metier] et [AleasController] ;

9.2.6. Exécution du serveur web

  

Le projet s'exécute à partir de la classe exécutable [Application] suivante :


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) {
    // exécution application
    SpringApplication.run(Config.class, args);
  }

}
  • ligne 6 : la classe [Application] est une classe exécutable (lignes 7-10) ;
  • ligne 9 : la méthode statique [SpringApplication.run] est une méthode de [spring Boot] (ligne 4) qui va lancer l'application. Son premier paramètre est la classe Java qui configure le projet. Ici la classe [Config] que nous venons de décrire. Le second paramètre est le tableau d'arguments passé à la méthode [main] (ligne 7). Ici, il n'y aura pas d'arguments ;

Pour l'exécution proprement dite, le lecteur est invité à revenir sur le paragraphe 9.2.1.

9.3. Le client Android

Note : le projet Android qui suit est assez complexe. Il nécessite de bonnes connaissances d'Android qu'on pourra trouver par exemple dans [Introduction à la programmation de tablettes Android avec Android Studio ].

ActivitéVuesCouche[DAO]UtilisateurServeur

Le client aura deux composantes :

  1. une couche [Présentation] (vues+activité) ;
  2. une couche [DAO] qui s'adresse au service [web / JSON] que nous avons étudié précédemment.

9.3.1. RxAndroid

Pour communiquer de façon asynchrone avec le serveur de nombres aléatoires, le client Android va utiliser la bibliothèque RxAndroid. Celle-ci étend RxJava au monde Android. Comme il a été fait pour l'application Swing, nous n'utiliserons qu'une unique extension amenée par RxAndroid, celle du schéduler [AndroidSchedulers.mainThread()]. Une interface graphique Android obéit aux mêmes lois qu'une interface Swing :

  • les événements sont traités dans un unique thread appelé event loop ou thread de l'Ui ;
  • lorsqu'un événement lance des actions asynchrones, les résultats de ceux-ci doivent être récupérés dans le thread de l'Ui s'ils doivent servir à mettre à jour l'Ui ;

Le client Android :

  • lancera plusieurs requêtes asynchrones vers le serveur de nombres aléatoires. Ces requêtes seront exécutées côté client avec les threads du schéduler [Schedulers.io()] ;
  • ces requêtes asynchrones rendront des observables qui seront fusionnés en un seul (merge) ;
  • cet observable sera observé côté client dans le schéduler [AndroidSchedulers.mainThread()] amené par RxAndroid ;

9.3.2. Le projet Intellij Idea

Le projet Android s'appelle [client] :

On l'exécutera par [2].

Note : l'exécution est très dépendante de la configuration de l'IDE Intellij Idea utilisé. Il est probable que l'exécution [2] ci-dessus ne marchera pas du premier coup sur une autre machine que la mienne. Configurer correctement l'IDE Intellij Idea pour exécuter ce projet peut être une tâche redoutable pour les débutants. Voici quelques points à regarder :

  • en [3], accéder à la structure du projet ;
  • en [4-5], le JDK et les SDK Android présents sur ma machine. A noter que le JDK 1.8 n'est pas indispensable. Android ne supporte pas certaines fonctionnalités de Java 8 dont les lambdas. Donc pour instancier des interfaces fonctionnelles, nous utiliserons des classes anonymes. Un JDK 1.6 est alors suffisant. Cependant, le projet tel qu'il est distribué a été configuré avec un JDK 1.8 ;

Le fichier [build.gradle] [6] qui configure le projet Android est le suivant :


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // replace with the current version of the 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'
  }
}

Selon les SDK Android présents, les versions des lignes 8, 24-25, 29 peuvent devoir être modifiées.

Pour installer de nouveaux SDK Android, utilisez le SDK Manager comme suit [1] :

Le projet a été configuré pour :

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

Enfin, vérifiez le chemin du SDK Android dans le fichier [local.properties] [4], ligne 11 ci-dessous :


## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Apr 07 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Exécution du projet Intellij Idea

Lorsqu'un environnement correct pour le projet a été créé, celui-ci peut être exécuté comme suit :

  • en [1], on lance l'émulateur Android Genymotion ;
  • en [2], on exécute la configuration d'exécution [app] ;
  • en [3], pour créer une configuration d'exécution ;
 
  • en [1, 3], la configuration a été nommée [app] ;
  • en [2], elle correspond à l'exécution du module appelé [app] ;
  • en [4], on demande qu'à l'exécution, l'IDE nous propose un périphérique d'exécution. Ici ce sera toujours l'émulateur Genymotion ;
  • en [5], on indique de garder ce périphérique pour toutes les exécutions de la configuration ;

L'exécution du projet sur l'émulateur Genymotion commence avec la bue initiale suivante :

Image

Pour savoir quoi mettre en [1], ouvrez une fenêtre de commandes DOS et tapez la commande [ipconfig] suivante :


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


Tapez en [1], l'une des adresses IP de votre machine (lignes 20, 28, 32). Si vous avez un pare-feu windows, vous aurez probablement à le désactiver pour que l'émulateur Android puisse atteindre le serveur de nombres aléatoires.

L'exécution des requêtes asynchrones avec les informations ci-dessus, donne les résultats suivants :

Image

Chaque requête donne naissance à une réponse jSON ayant les champs suivants :

  • aleas : les nombres aléatoires générés par le serveur ;
  • idClient : le n° de la requête ;
  • on : le thread d'exécution de la requête côté client ;
  • requestAt : heure de la requête ;
  • responseAt : heure de réception de la réponse ;
  • delay : le délai d'attente que le serveur a observé avant de renvoyer sa réponse ;
  • erreur : un code d'erreur - 0 si pas d'erreur ;
  • message : un message d'erreur - null si pas d'erreur ;
  • observedAt : heure d'observation de la réponse ;
  • observedOn : thread d'observation de la réponse. Ce sera ici toujours [main] qui désigne le thread de l'Ui ;

Comme les requêtes sont asynchrones et que les temps d'attente imposés au serveur sont aléatoires, les réponses reviennent en ordre dispersé.

9.3.4. Les dépendances Gradle du projet

Le projet a besoin de dépendances que nous inscrivons dans le fichier [app / build.gradle] :

  

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'
}
  • les dépendances des lignes 2-3 sont des dépendances standard d'un projet Android avec le SDK 23 ;
  • la dépendance de la ligne 5 amène l'objet Spring [RestTemplate] qui gère le dialogue de la couche [DAO] avec le serveur ;
  • la dépendance de la ligne 6 amène la bibliothèque JSON [Jackson] utilisée par l'application ;
  • la dépendance de la ligne 7 amène la bibliothèque RxAndroid (et avec elle la bibliothèque RxJava) que la couche Ui utilise pour dialoguer avec la couche [DAO] ;

9.3.5. Le manifeste de l'application Android

  

<?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>
  • ligne 5 : les accès internet doivent être autorisés ;

9.3.6. La couche [DAO]

 

9.3.6.1. L'interface [IDao] de la couche [DAO]

L'interface de la couche [DAO] sera la suivante :


package android.aleas.dao;

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

public interface IDao {

  // nombres aléatoires dans l'intervalle [a,b]
  // n nombres sont générés avec n lui-même nombre aléatoire dans l'intervalle [minCount, maxCount]
  // les nombres sont générés après une attente de delay millisecondes,
  // où [delay] est lui-même un nombre aléatoire dans l'intervalle [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL du service web
  public void setUrlServiceWebJson(String url);

  // délai d'attente (ms) max de la réponse du serveur à une demande de connexion
  // délai d'attente (ms) max de la réponse du serveur à une requête
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • ligne 12 : la méthode de la couche [DAO] qui délivre les nombres aléatoires de façon asynchrone ;
  • ligne 15 : pour indiquer à l'implémentation [DAO] l'URL du service de génération des nombres aléatoires ;
  • ligne 19 : pour fixer à l'implémentation [DAO] des temps d'attente maximaux, ceci pour éviter un trop long temps d'attente lorsque le serveur ne répond pas ;

La méthode [getAleas] reçoit tous ses paramètres dans l'objet [Request] suivant :


package android.aleas.fragments;

public class Request {

  // n° requête
  int id;
  // saisies utilisateur
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructeurs
  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 et setters
...
}

On reconnait là, la plupart des paramètres de l'URL du serveur qu'il faut interroger.

La méthode [getAleas] rend un type Observable<AleasDaoResponse> où la classe [AleasDaoResponse] est la suivante :


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // code d'erreur
  private int erreur;
  // message d'erreur
  private String message;
  // temps d'attente du serveur
  private int delay;
  // nombres aléatoires délivrés par le serveur
  private List<Integer> aleas;
  // état client
  private ClientState clientState;

  // constructeurs

  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 et setters
...
}

Le type [ClientState] est le suivant :


package android.aleas.dao;

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

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

public class ClientState {

  // nom du thread d'exécution
  private String on;
  // heure de la requête
  private String requestAt;
  // heure de la réponse
  private String responseAt;
  // id du client
  private int idClient;

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

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

  // méthodes privées

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

  // getters et setters
...
}
  • ligne 11 : thread d'exécution de la couche [DAO] ;
  • ligne 13 : heure de la requête ;
  • ligne 15 : heure de la réponse ;
  • ligne 17 : n° de la requête ;

Les champs [on, requestAt, idClient] sont initialisés par le client au début de la requête. Le champ [responseAt] est initialisé lorsque le client reçoit la réponse du serveur.

9.3.6.2. Implémentation de la couche [DAO]

  

L'interface [IDao] est implémentée avec la classe [Dao] suivante :


package android.aleas.dao;

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

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

public class Dao implements IDao {

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

  // mappeur jSON
  private ObjectMapper mapper;

  // constructeurs
  public Dao() {
    // mappeur jSON
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // on fixe l'URL du service REST
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • ligne 22 : l'objet [RestTemplate] qui va assurer le dialogue avec le serveur de nombres aléatoires ;
  • ligne 24 : l'URL du service de génération - est fixée par la méthode [setUrlServiceWebJson] de la ligne 41 ;
  • ligne 27 : le mappeur jSON qui va servir à désérialiser la chaîne jSON envoyée par le serveur de nombres aléatoires ;
  • lignes 30-33 : le constructeur de la classe ;
  • ligne 32 : le mappeur jSON de la ligne 27 est créé ;

La méthode [setClientTimeouts] est la suivante :


  // client REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // on fixe le timeout des requêtes du client REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • le dialogue du client avec le serveur web / JSON est assuré par l'objet [RestTemplate] de la ligne 2. Pour l'instant nous ne l'avons pas initialisé. C'est la méthode [setClientTimeouts] qui le fait ;
  • ligne 8 : la classe [HttpComponentsClientHttpRequestFactory] est fournie par la dépendance [spring-android-rest-template]. Elle va nous permettre de fixer les délais d'attente maximum de la réponse du serveur (lignes 9-10) ;
  • ligne 11 : nous construisons l'objet de type [RestTemplate] qui va être le support de la communication avec le service web. Nous lui passons comme paramètre l'objet [factory] qui vient d'être construit ;
  • ligne 12 : le dialogue client / serveur peut prendre diverses formes. Les échanges se font par lignes de texte et nous devons indiquer à l'objet de type [RestTemplate] ce qu'il doit faire avec cette ligne de texte. Pour cela, nous lui fournissons des convertisseurs, des classes capables de traiter les lignes de texte. Le choix du convertisseur se fait en général via les entêtes HTTP qui accompagnent la ligne de texte. Selon ces entêtes, l'objet [RestTemplate] va choisir, parmi ses convertisseurs, celui qui est le mieux adapté à la situation. Ici, nous n'aurons qu'un unique convertisseur, un convertisseur String --> String, qui fait que le type String reçu du serveur ne subira aucune transformation.

La méthode [getAleas] est la méthode la plus complexe :


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // exécution service
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL du 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());
          // informations client
          ClientState clientState = new ClientState(request.getId());
          // requête http synchrone
          String response = executeRestService("get", urlService, null);
          // désérialisation de la réponse jSON du serveur
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // erreur ?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // on fait suivre l'exception
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // on inscrit l'heure de réception
            clientState.setResponseAt();
            // on fait suivre le résultat à l'abonné
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // on fait suivre l'exception à l'abonné
          subscriber.onError(ex);
        } finally {
          // on signale la fin de l'observable
          // à l'exécution, on remarque que cette méthode n'a aucun effet si la méthode [onError] a été appelée     précédemment - conforme à la théorie - on pourrait donc placer cette instruction uniquement dans le try
          subscriber.onCompleted();
        }
      }
    });
  }
  • ligne 2 : il faut se rappeler qu'on doit produire un type [Observable<AleasResponse>] ;
  • ligne 3 : une ligne de log sur la console Android ;
  • ligne 5 : l'objet [RestTemplate] assure un dialogue synchrone avec le serveur. Cela signifie que le thread d'exécution qui fait la requête est bloqué jusqu'à réception de la réponse. Dans l'exemple Swing, nous avons vu comment transformer une action synchrone en action asynchrone grâce à la méthode [Observable.create]. C'est cette même voie que nous suivons ici ;
  • ligne 7 : la méthode [call] de l'interface [Observable.OnSubscribe<AleasDaoResponse>] de la ligne 5. C'est cette méthode qui est appelée lorsqu'un observateur s'abonne à l'observable ;
  • lignes 10-12 : construction de l'URL du service des nombes aléatoires ;
  • ligne 14 : initialisation de l'objet [ClientState]. Il s'agit ici de noter l'heure de la requête ;
  • ligne 16 : requête HTTP synchrone. On obtient une réponse jSON. La méthode [executeRestService] attend trois paramètres :
      1. la méthode HTTP à utiliser pour interroger le service ;
      2. l'URL du service ;
      3. l'objet à poster de type Object, null si la méthode HTTP n'est pas POST ;
  • 18-21 : désérialisation de la chaîne jSON reçue en un type [AleasServerResponse]. Ce type est le suivant :

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // code d'erreur
  private int erreur;
  // message d'erreur
  private String message;
  // temps d'attente serveur
  private int delay;
  // nombres aléatoires
  private List<Integer> aleas;

  // getters et setters
...
}
  • ligne 23 : on récupère le code d'erreur envoyé par le serveur ;
  • lignes 24-26 : en cas d'erreur, on fait suivre une exception à l'abonné ;
  • ligne 29 : on met à jour [clientState] qui fera partie de la réponse envoyée à l'abonné ;
  • lignes 31-33 : envoi de la réponse à l'abonné. Elle est de type [AleasDaoResponse] ;
  • lignes 35-37 : traitent tous les cas d'erreur de façon non différenciée. L'erreur la plus probable est une erreur de réseau ;
  • ligne 41 : notification de fin d'émission ;

9.3.7. Les vues de l'application

  

L'application présente les deux vues suivantes :

La vue de la requête

Image

La vue de la réponse

Image

9.3.7.1. La classe [MyFragment]

Il y a deux fragments :

  • [RequestFragment] pour la requête ;
  • [ResponseFragment] pour la réponse ;

Les deux fragments étendent la classe [MyFragment] suivante :


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 {

  // ------------- données communes aux fragments
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • ligne 7 : la classe [MyFragment] étend la classe Android [Fragment] ;
  • lignes 10-11 : les données communes à tous les fragments ;
  • ligne 10 : chaque fragment connaît l'unique activité de l'application ;
  • ligne 11 : pour communiquer entre-eux, les fragments utilisent une session ;
  • ligne 13 : avant d'afficher un fragment, on lui demandera de se rafraîchir avec le contenu de la session. Cette méthode est déclarée abstraite car elle est implémentée par les classes filles. Pour cette raison, la classe elle-même est déclarée abstraite (ligne 7) ;

La classe [Session] contient les données que les différents fragments de l'application se partagent. Son code est le suivant :

  

package android.aleas.activity;

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

public class Session {

  // activité de l'application
  private MainActivity activity;
  // nombre de requêtes
  private int nbRequests;
  // caractéristiques des requêtes
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL service web / jSON
  private String urlWebJson;
  // opération a débuté
  private boolean onAir;
  // idem mais un peu plus tard dans le temps
  private boolean operationStarted;
  // le nom de l'exemple choisi par l'utilisateur dans la liste des exemples
  private String exampleName;
  // son n° dans la liste des fragments
  private int examplePosition;
  // l'adaptateur du spinner des exemples dans la vue de la requête
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // méthodes
  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 et setters
...
}

La méthode de la ligne 46 permet de créer l'objet [Request] encapsulant toutes les informations données par l'utilisateur dans la vue de requête :

  

package android.aleas.fragments;

public class Request {

  // n° requête
  int id;
  // saisies utilisateur
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructeurs
  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 et setters
....
}

9.3.7.2. Le fragment [RequestFragment] de la requête

Le fragment de la requête a les composants suivants :

Image

L'application a une vue unique qui est une vue avec deux onglets :

  • [1] : l'onglet de la requête ;
  • [2] : l'onglet de la réponse ;

Les composants du fragment [RequestFragment] sont les suivants :

Type
Nom
Rôle
3
EditText
edtNbRequests
nombre de requêtes à faire au service de génération des nombres aléatoires
4
EditText
edtA, edtB
les bornes [a,b] de l'intervalle de génération des nombres ;
5
EditText
edtMinCount, edtMaxCount
le service génère count nombres où count est un nombre aléatoire dans l'intervalle [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
le service attend delay millisecondes avant de générer les nombres où delay est un nombre aléatoire dans l'intervalle [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
l'URL du service de génération des nombres aléatoires ;
8
Spinner
spinnerExemples
la liste déroulante des exemples. Chaque exemple illustre une méthode particulière de la classe [Observable] ;
8
Button
btnExecuter
le bouton qui lance les appels au service de génération des nombres ;

Les erreurs de saisie sont signalées :

Image

Les composants 1 à 6 sont des composants [TextView] ayant les noms suivants (dans l'ordre) : txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. Le fragment [ResponseFragment] de la réponse

Le fragment de la réponse a les composants suivants :

Image

Type
Nom
Rôle
1
TextView
infoReponses
nombre de réponses reçues
2
ListView
listReponses
liste des chaînes jSON reçues du serveur
3
Button
btnAnnuler
pour annuler les requêtes au serveur

9.3.7.4. L'activité Android [MainActivity]

  

La classe [MainActivity] affiche la vue [] suivante :


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

  <!-- barre d'application -->
  <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">

    <!-- barre d'outils -->
    <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">

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

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

  <!-- conteneur de vues -->
  <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>

Les composants de cette vue sont les suivants :

lignes
Type
Nom
Rôle
20-34
Toolbar
toolbar
barre d'outils de l'application
29-34
ProgressBar
loadingPanel
image d'attente affichée tant que la demande de l'utilisateur est en cours d'exécution
37-40
TabLayout
tabs
le barre des onglets de l'application
44-51
MyPager
container
le conteneur dans lequel sont affichés les différents fragments de l'application

La classe [MyPager] est la suivante :


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 {

  // contrôle le swipe
  private boolean isSwipeEnabled;

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

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

  // redéfinition de méthodes
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe autorisé ?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

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

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

}
  • la classe [MyPager] étend la classe standard Android [ViewPager]. On utilise la classe [MyPager] en lieu et place de la classe [ViewPager] uniquement parce qu'on veut inhiber le swipe : par défaut avec la classe [ViewPager], on peut passer d'un onglet à l'autre par un swipe (un tirer à gauche ou à droite). Ici, on ne veut pas ce comportement ;
  • ligne 11 : le booléen qui va contrôler le swipe (lignes 26 et 36) ;
  • lignes 44-46 : la méthode qui permet d'initialiser le champ de la ligne 11 ;

Le squelette de l'activité Android [MainActivity] est le suivant :


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 {

  // couche [DAO]
  private IDao dao;
  // la session
  private Session session;

  // constructeur
  public MainActivity() {
    // parent
    super();
    // session
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getters

  public Session getSession() {
    return session;
  }

  // implémentation 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);
  }

}
  • ligne 21 : la classe [MainActivity] étend la classe standard Android [AppCompatActivity]. C'est donc une activité Android standard ;
  • ligne 21 : la classe [MainActivity] implémente l'interface [IDao] ;

Si on revient à l'architecture de l'application :

le fait que l'activité implémente l'interface de la couche [DAO] permet aux vues de ne pas avoir connaissance de la couche [DAO] : leurs gestionnaires d'événements s'adresseront à la couche [activité] lorsqu'elles veulent échanger avec le serveur.

  • ligne 24 : une référence sur la couche [DAO] initialisée par le constructeur ligne 35 ;
  • ligne 26 : une référence sur la session partagée par les fragments initialisée par le constructeur ligne 33 ;
  • lignes 46-59 : implémentation de l'interface [IDao] ;

La classe [MainActivity] initialise les composants de la vue qui lui est associée de la façon suivante :


  // barre d'outils
  private Toolbar toolbar;
  // gestionnaire de fragments
  private MyPager mViewPager;
  // conteneur d'onglets
  private TabLayout tabLayout;
  // image d'attente
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // classique
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // session
    session.setActivity(this);
    // configuration timeouts de la couche [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

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

    // toolbar
    setSupportActionBar(toolbar);

    // au départ on n'a qu'un seul onglet
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gestionnaire d'évt
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
        int position = tab.getPosition();
        if (position == 0) {
          // onglet requête
          showView(0);
        } else {
          // onglet réponse - dépend de l'exemple choisi
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // création des fragments des réponses
    createResponseFragments();

    // gestion image d'attente
    loadingPanel.setVisibility(View.INVISIBLE);
}

Ce code est assez classique dans une activité. Explicitons quelques points :

  • la ligne 19 référence la classe [Constants] suivante :

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";
}
  • lignes 31-33 : on crée le 1er onglet avec le titre [Request]. A un moment donné, nous aurons en mémoire :
    • le fragment [Request] ;
    • n fragments de type [ExampleXXFragment] ;

Le 1er onglet affichera toujours le fragment [Request]. Le 2ième onglet affichera le fragment [ExampleXXFragment] correspondant à l'exemple choisi par l'utilisateur. Le fragment affiché par le second onglet change donc au fil du temps ;

  • lignes 37-48 : le code exécuté lorsque l'utilisateur clique sur l'un des onglets ;
  • ligne 43 : on affiche le fragment n° 0 ;
  • ligne 46 : on affiche le fragment actuellement en cours d'utilisation (visualisé). Son n° est trouvé dans la session ;
  • ligne 62 : on crée les fragments de tous les exemples présents dans le spinner des exemples dans la vue [RequestFragment] (1er onglet) ;
  • ligne 65 : l'image d'attente est pour l'instant cachée ;

Pour comprendre la méthode [showView] (lignes 43, 46) et la méthode [createResponseFragments], il nous faut tout d'abord présenter le gestionnaire des fragments en mémoire (classe incluse dans le fichier Java de MainActivity) :


  // le gestionnaire de fragments - doit définir les méthodes getItem, getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // les fragments gérés
    private MyFragment[] fragments;

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

    // doit rendre le fragment n° position
    @Override
    public MyFragment getItem(int position) {
      // le fragment
      return fragments[position];
    }

    // rend le nombre de fragments à gérer
    @Override
    public int getCount() {
      // nb de fragments
      return fragments.length;
    }
  }
}
  • la classe [SectionsPagerAdapter] étend la classe Android [FragmentPagerAdapter]. Elle redéfinit deux méthodes de sa classe parent :
    • la méthode [getItem], ligne 15 ;
    • la méthode [getCount], ligne 22 ;
  • la classe [SectionsPagerAdapter] contient tous les fragments de l'application. Ceux-ci sont mémorisés ligne 5. On notera qu'ils sont de type [MyFragment] présenté au paragraphe 9.3.7.1 ;
  • ligne 8 : pour se construire, la classe [SectionsPagerAdapter] reçoit les fragments qu'elle doit gérer ;
  • lignes 14-18 : la méthode [getItem] rend le fragment en position [position] ;
  • lignes 21-25 : la méthode [getCount] rend le nombre total de fragments ;

La méthode [createResponseFragments] crée tous les fragments dont a besoin l'application :


private void createResponseFragments() {
    // spinner des exemples
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // on met l'adaptateur dans la session pour que la vue [Request] le récupère
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • ligne 3 : on crée un adaptateur pour le spinner des exemples, ici une liste de String représentant les noms des exemples. Ces noms sont présents dans le fichier [layout/exemples.xml] :
  

Le fichier [exemples.xml] contient le code suivant :


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

Ligne 1, ce fichier est le second paramètre de la méthode [createFromResource]. Dans [R.array.exemples], [exemples] est le nom du tableau, ligne 3 ci-dessus, pas le nom du fichier.

  • ligne 5 : on associe un layout (gestionnaire d'affichage) à l'adaptateur. Maintenant l'adaptateur a à la fois les données et leur mode d'affichage ;
  • ligne 7 : on met l'adaptateur en session. C'est là que le récupèrera le fragment [RequestFragment] qui en a besoin ;

Continuons le code de la méthode [createResponseFragments] :


private void createResponseFragments() {
    // spinner des exemples
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // on met l'adaptateur dans la session pour que la vue [Request] le récupère
    session.setSpinnerExemplesAdapter(adapter);
    // création du tableau des fragments (1 requête, n réponses)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // fragment de la requête
    tFragments[0] = new RequestFragment();
    // fragments des réponses
    for (int i = 1; i < tFragments.length; i++) {
      // on construit le nom du fragment à instancier correspondant à l'exemple choisi par l'utilisateur
      // ce nom doit être le nom complet avec son package - ici il est directement associé au n° de l'exemple dans le spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // on instancie le fragment associé à l'exemple
      MyFragment fragment;
      try {
        // instanciation de la classe
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // le fragment a été créé - on le met dans le tableau
      tFragments[i] = fragment;
    }
    // instanciation du gestionnaire de fragments avec ces nouveaux fragments
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Set up the ViewPager with the sections adapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navigation entre pages - cette instruction est importante
    // ici on dit que de part et d'autre de la vue affichée, on doit conserver [tFragments.length] vues initialisées
    // cela entraîne ici que  tous les fragments utilisés par l'application sont en mémoire et initialisés
    // si on ne fait pas ça alors par défaut le [OffscreenPageLimit] est de 1
    // ainsi si le fragment visualisé est le n° 3, seuls les fragments 2 et 4 seront initialisés
    // cela se passe par l'appel de la méthode [onCreateView] de ces 2 fragments - cela signifie que dans cette méthode, il faut prévoir de
    // régénérer l'aspect visuel qu'avait le fragment la dernière fois qu'il a été utilisé - par ailleurs, il ne faut pas que dans cette méthode
    // il y ait du code qui ne supporterait pas d'être exécuté 2 fois - cela crée une pagaille monstre et est complexe à gérer
    // ici on a préféré éviter ces difficultés - dans les logs, on voit qu'au démarrage de l'application, tous les fragments sont créés
    // et leur méthode [onCreateView] exécutée - elle ne l'est plus jamais ensuite -
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // on inhibe le swipe entre fragments
    mViewPager.setSwipeEnabled(false);
  }
  • ligne 9 : création du tableau qui va contenir tous les fragments de l'application ;
  • ligne 11 : le 1er fragment est celui de la requête ;
  • lignes 13-28 : on va créer autant de fragments qu'il y a d'exemples. Ces fragments étendent tous le fragment de la réponse [ResponseFragment] et n'implémente que ce qui est spécifique à l'exemple : la création des valeurs observées. Celles-ci difffèrent en effet d'un exemple à l'autre ;
  • ligne 16 : le fragment d'un exemple porte un nom standard : ExampleXXFragment où XX est sa position dans le spinner des exemples augmentée de 1. XX est également le n° du fragment de l'exemple dans le gestionnaire de fragments ;
  • ligne 21 : instanciation du fragment de l'exemple n° i du spinner :
    • Class.forName(exampleName) : charge le fragment en mémoire ;
    • Class.forName(exampleName).getConstructors()[0] : obtient la référence du 1er constructeur de la classe. La classe ExampleXXFragment n'a qu'un constructeur. C'est donc une référence sur celui-ci qui sera obtenue ;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instancie un objet de type ExampleXXFragment en utilisant le constructeur de l'étape précédente. new Object[]{} représente les paramètres passé à ce constructeur. Le constructeur de la classe ExampleXXFragment n'attendant pas de paramètres, on passe un tableau d'objets vide ;
  • ligne 27 : ce fragment est ajouté au tableau des fragments ;
  • ligne 30 : nous avons vu que le constructeur du gestionnaire de fragments [SectionsPagerAdapter] attendait dans ses paramètres le tableau des fragments qu'il devait gérer. C'est maintenant qu'on le lui passe ;
  • ligne 22 : le conteneur de fragments [mViewPager] de la vue associée à l'activité [MainActivity] est ici associé au gestionnaire de fragments : le conteneur de fragments [mViewPager] affiche les fragments du gestionnaire de fragments ;
  • ligne 43 : on lira les commentaires - l'instruction revient à dire que tous les fragments doivent rester dans l'état où le code les met, quelque soit le fragment actuellement affiché. Si bien que lorsqu'on revient à lui, on le retrouve dans l'état où on l'a laissé ;
  • ligne 45 : le conteneur de fragments [mViewPager] est de type [MyPager] qui permet d'inhiber le swipe ;

La méthode [MainActivity.showView] est la suivante :


  // afichage vue n° [position]
  private void showView(int position) {
    // on rafraîchit le fragment avant son affichage
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // on affiche la vue demandée - on va directement sur la vue (second paramètre à false)
    // sans ce paramètre, on va par défaut à la vue désirée en affichant rapidement les vues intermédiaires - comportement indésirable
    mViewPager.setCurrentItem(position, false);
}
  • ligne 3 : on veut afficher le fragment n° position ;
  • ligne 4 : ce fragment est demandé au gestionnaire de fragments puis rafraîchi. En effet, depuis la dernière fois qu'il a été affiché, la session a pu changer. Le fragment doit alors inspecter celle-ci pour voir s'il doit se mettre à jour ;
  • ligne 7 : le fragment est affiché par le [ViewPager]. Comme celui-ci a été associé au gestionnaire de fragments, c'est le fragment n ° [position] qui va être affiché, celui qu'on vient de rafraîchir ligne 4 ;

Terminons par les deux méthodes de gestion de l'attente :


  public void beginWaiting() {
    // gestion image d'attente
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // gestion image d'attente
    loadingPanel.setVisibility(View.INVISIBLE);
    // fin exécution
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. Le fragment [RequestFragment]

La classe [RequestFragment] est la suivante :


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 du service web
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // nombre de requêtes
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // intervalle de génération
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // delay
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // nombre de valeurs générées
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // bouton
  private Button btnExecuter;
  // liste des réponses
  private ListView listReponses;
  private TextView infoReponses;
  // spinner des exemples
  private Spinner spinnerExemples;

  // les saisies
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // on récupère l'activité et la session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // on crée la vue du fragment à partir de sa définition XML
    View view = inflater.inflate(R.layout.request, container, false);
    // composants
    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);

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

    // au départ pas de messages d'erreur
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner des exemples
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // résultat
    return view;
  }
...
}
  • ligne 16 : la classe [RequestFragment] étend la classe [MyFragment] (cf paragraphe 9.3.7.1) ;
  • lignes 18-42 : les composants visuels du fragment (cf paragraphe 9.3.7.2) ;
  • lignes 45-52 : les saisies faites par l'utilisateur dans le formulaire ;
  • le constructeur (lignes 55-58) et la méthode [onCreateView] sont exécutés lorsque l'activité [MainActivity] crée tous les fragments de l'application. C'est l'unique fois ;
  • ligne 61 : le code de la méthode [onCreateView] est classique. On notera ligne 102, que l'adaptateur du spinner des exemples est pris dans la session. On notera également ligne 91, que le clic sur le bouton [Exécuter] est géré par la méthode [doExecuter] ;
  • lignes 64-65 : les champs [activity] et [session] appartiennent à la classe parent [MyFragment] ;

La méthode [doExecuter] est la suivante :


  // les saisies
  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() {
    // saisies valides ?
    if (isPageValid()) {
      // on met les infos en session
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // on mémorise l'URL du service web
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // action en cours
      session.setOnAir(true);
      // mais pas commencée
      session.setOperationStarted(false);
      // on affiche le fragment de la réponse
      activity.selectTab(Constants.VUE_RESPONSE);
      // on commence l'attente
      beginWaiting();
    }
}
  • ligne 15 : nous ne commenterons pas la méthode [ispageValid]. Elle vérifie la validité des saisies et rend true uniquement si elles sont toutes valides. Dans ce cas, elles sont utilisées pour initialiser les champs des lignes 2-9 ;
  • lignes 17 : les différentes saisies sont mises en session :
    • [spinnerExemples.getSelectedItem().toString()] est le nom de l'exemple sélectionné par l'utilisateur et est mémorisé dans [session.exampleName] ;
    • [spinnerExemples.getSelectedItemPosition() + 1] est le n° du fragment associé à l'exemple et qui a été mémorisé (le fragment) par le gestionnaire de fragments. Ce n° est mémorisé dans [session.examplePosition] ;
  • ligne 19 : l'URL du service web / jSON est transmise à l'activité qui la transmet à son tour à la couche [DAO] ;
  • lignes 21-24 : on note qu'une opération va démarrer ;
  • ligne 26 : l'onglet de la réponse va être affiché. Pour comprendre ce qui va se passer, il faut se rappeler le code [MainActivity.selectTab] :

  // sélection d'un onglet
  public void selectTab(int position) {
    // il y a au plus 2 onglets
    // au départ il n'y en a qu'un, celui de la requête
    // si l'onglet demandé est le n° 1 et que celui-ci n'existe pas encore, alors il faut le créer
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 onglet de +
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // on sélectionne par programme l'onglet, ce qui va déclencher l'événement [onTabSelected]
    // qui va associer la bonne vue à cet onglet
    tabLayout.getTabAt(position).select();
}
  • initialement, l'activité n'avait créé que l'onglet de la requête (onglet n° 0) ;
  • lignes 6-11 : on crée l'onglet de la réponse (onglet n° 1) s'il n'avait pas été créé ;
  • ligne 14 : on sélectionne l'onglet n° position (0 ou 1). Cela met l'événement [onTabSelected] dans la file d'attente de l'event loop de l'application Android ;

Le gestionnaire de l'événement [onTabSelected] dans [MainActivity] est le suivant :


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
        int position = tab.getPosition();
        if (position == 0) {
          // onglet requête
          showView(0);
        } else {
          // onglet réponse - dépend de l'exemple choisi
          showView(session.getExamplePosition());
        }
}

Dans le cas de l'onglet [Response], c'est la ligne 9 qui est exécutée. Le fragment n° [session.getExamplePosition()] va être affiché. Par exemple, pour l'exemple [exemple-03], le n° qui a été enregistré dans [session.examplePosition] est 3. La ligne 10 affiche alors le fragment n° 3. Le tableau des fragments construits initialement par l'activité est [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. C'est donc bien le fragment [Exemple03Fragment] qui va être affiché. Il l'est par le code suivant :


  // afichage vue n° [position]
  private void showView(int position) {
    // on rafraîchit le fragment avant son affichage
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // on affiche la vue demandée - on va directement sur la vue (second paramètre à false)
    // sans ce paramètre, on va par défaut à la vue désirée en affichant rapidement les vues intermédiaires - comportement indésirable
    mViewPager.setCurrentItem(position, false);
}

On voit que le fragment va être rafraîchi (ligne 4) avant d'être affiché (ligne 7).

9.3.7.6. Le fragment [ResponseFragment]

La classe [ResponseFragment] affiche les réponses du serveur. Son code est le suivant :


package android.aleas.fragments;

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

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

public abstract class ResponseFragment extends MyFragment {

  // liste des réponses
  private ListView listReponses;
  private TextView infoReponses;
  // bouton
  private Button btnAnnuler;

  // mappeur 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) {
    // on récupère l'activité et la session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // on crée la vue du fragment à partir de sa définition XML
    View view = inflater.inflate(R.layout.response, container, false);
    // composants
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // bouton [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // résultat
    return view;
  }
...
  // méthode à exécuter (par code explicite) avant chaque visualisation du fragment
  public void onRefresh() {
...
  }
}
  • ligne 21 : la classe [ResponseFragment] étend la classe [MyFragment] ;
  • lignes 23-27 : les composants du fragment ;
  • lignes 32-36 : le constructeur n'est exécuté qu'une fois, lors de la création initiale des fragments des exemples par l'activité. En effet, tous les fragments des exemples étendent le fragment [ResponseFragment]. Lors de leur instanciation, le constructeur de leur classe parent [ResponseFragment] est appelé ;
  • ligne 35 : initialise le mappeur jSON de la ligne 30 utilisé pour afficher la chaîne jSON d'une pile d'exceptions ;
  • lignes 38-59 : la méthode [onCreateView] n'est exécutée qu'une fois, lors de la création initiale des fragments des exemples par l'activité. On y trouve du code classique dans une application Android ;
  • lignes 52-56 : la méthode exécutée lors d'un clic sur le bouton [Annuler] est la méthode [doAnnuler] ;
  • lignes 62-64 : la méthode [onRefresh] est exécutée à chaque fois que l'onglet [Response] est affiché ;

Grâce aux différents logs placés dans les méthodes importantes, on est capable de voir ce qui se passe au démarrage de l'application :

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
  • ligne 1 : construction du fragment [RequestFragment] ;
  • lignes 2-9 : construction des fragments des 4 exemples de l'application ;
  • ligne 10 : initialisation du fragment [RequestFragment] ;
  • lignes 11-14 : initialisation des fragments des 4 exemples de l'application ;

Ensuite, on ne revoit plus jamais d'appels à ces méthodes.

La méthode [ResponseFragment.onRefresh] est la suivante :


  // méthode à exécuter (par code explicite) avant chaque visualisation du fragment
  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()));
    // exécution en cours ?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // exécution requête
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • ligne 5 : on regarde si le fragment [RequestFragment] a fait une requête (session.isOnAir) et si celle-ci a démarré (isOperationStarted). Si le fragment [RequestFragment] a fait une requête et que celle-ci n'est pas déjà en cours d'exécution, l'opération est lancée (lignes 7-8) ;
  • une fois l'opération lancée, comme celle-ci est asynchrone, l'utilisateur peut naviguer entre les deux onglets. S'il navigue de nouveau vers l'onglet [Response] et qu'une opération est en cours, alors les lignes 7-8 ne sont pas exécutées ;

La méthode [doExecuter] de la ligne 8, exécute l'opération demandée par l'utilisateur :


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // début attente
    beginWaiting();
    // préparation exécution
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // on crée et exécute les observables de l'exemple choisi
    createAndExecuteObservables();
}

// méthode implémentée par classes filles
protected abstract void createAndExecuteObservables();
  • ligne 10 : crée, exécute et observe des observables. Ceux-ci sont différents pour chaque exemple. C'est pourquoi, la méthode [createAndExecuteObservables] est abstraite (ligne 14). Elle va être implémentée par les fragments [ExampleXXFragment] qui étendent la classe [ResponseFragment] ;
  • ligne 6 : la liste des abonnements est vidée ;
  • ligne 7 : la liste qui affiche les réponses est vidée ;
  • ligne 8 : compte le nombre de réponses reçues ;

Les classes filles [ExampleXXFragment] confient à la méthode [showAlea] suivante, la mission d'afficher les élements qu'elles observent :


  protected void showAlea(String data) {
    // une info de plus
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 réponse de +
    reponses.add(0, data);
    Log.d("rxjava", data);
    // maj de l'UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • ligne 1 : on voit que l'élément observé arrive sous la forme d'une chaîne. Ce sera en fait la chaîne jSON de l'élément observé. Cela nous permet d'avoir une unique méthode d'affichage de l'élément observé quelque soit son type Java exact ;
  • ligne 6 : l'élément [data] observé est ajouté en première position de la liste des réponses. L'utilisateur voit donc en début de liste, les réponses les plus récentes ;

L'attente est gérée par les méthodes [beginWaiting] et [cancelWaiting] suivantes :


  private void beginWaiting() {
    // on met le sablier
    activity.beginWaiting();
    // le bouton [Annuler] est affiché
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // fin de l'attente
    activity.cancelWaiting();
    // le bouton [Annuler] est caché
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Elle font appel aux méthode de mêmes noms de l'activité puis se contentent de montrer / cacher le bouton [Annuler].

Le clic sur le bouton [Annuler] est géré par le code suivant :


  protected void doAnnuler() {
    // on annule tous les abonnements
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fin de l'attente
    cancelWaiting();
}
  • lignes 3-7: on annule un à un tous les abonnements ;

9.3.8. Les exemples d'observables

9.3.8.1. Exemple-01

Les classes [ExampleXXFragment] ont pour fonctionnalité de créer, exécuter et observer des observables. L'affichage des valeurs observées est fait par la classe parent [ResponseFragment].

La classe [Example01Fragment] est la suivante :

  

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 {

    // mappeurs jSON
    private ObjectMapper mapperAleasUiResponse;

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

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // on demande les nombres aléatoires
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // configuration observable n° i
            // requête à faire au serveur
            Request request = session.getRequest();
            request.setId(i);
            // observable exécuté sur thread de calcul
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observation sur thread de l'event loop;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // on exécute tous ces observables
        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) {
        // on extrait l'info à afficher
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • ligne 36 : l'unique observable qui va être généré ;
  • lignes 37-44 : génération et configuration des différents observables qui sont fusionnés (ligne 43) dans l'observable de la ligne 36 ;
  • ligne 43 : l'observable est exécuté dans un thread du schéduler [Schedulers.io()]. L'appel HTTP au serveur sera exécuté dans ce thread ;
  • ligne 46 : l'observable final est observé sur le thread de l'event loop ;
  • lignes 48-57 : exécution des observables donc des requêtes vers le serveur de nombres aléatoires. Android ne supporte pas encore Java 8 et ses lambdas. On utilise donc ici des classes anonymes pour instancier les interfaces fonctionnelles de RxJava ;
  • lignes 49-52 : action exécutée lorsque l'observateur reçoit un nouvel élément de type [AleasDaoResponse] de l'observable (cf paragraphe 9.3.6.1) ;
  • ligne 51 : appel de la méthode [showAlea] de la classe parent. On se rappelle qu'elle attend une chaîne de caractères. Celle-ci est fournie par la méthode [getDataFrom] des lignes 59-68 ;
  • ligne 63 : on rend la chaîne jSON du type [AleasUiResponse] suivant :

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // réponse [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // thread d'observation
  private String observedOn;
  // heure d'observation
  private String observedAt;

  // constructeurs
  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 et setters
...
}
  • à la réponse de la couche [DAO] (ligne 11), on ajoute deux informations :
    • ligne 13 : le thread d'observation ;
    • ligne 15 : l'heure d'observation ;

Revenons au code de souscription :


    @Override
    public void createAndExecuteObservables() {
...
        // on exécute tous ces observables
        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) {
                // on affiche l'exception
                showAlea(getMessagesFromThrowable(th));
                // après avoir reçu une exception, l'observable ne reçoit ni onNext, ni onCompleted
                // obligé d'annuler la souscription à la main
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fin attente
                cancelWaiting();
            }
        }));
}
  • lignes 11-18 : cas où l'observateur reçoit une exception ;
  • ligne 14 : on utilise de nouveau la méthode [showAlea] de la classe parent pour afficher l'exception. La méthode [getMessagesFromThrowable] est une méthode de la classe parent [ResponseFragment] qui à partir d'une exception produit une chaîne de caractères :

  // messages d'une exception
  protected String getMessagesFromThrowable(Throwable ex) {
    // on crée une liste avec les msg d'erreur de la pile d'exceptions
    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();
    }
}
  • ligne 11 : on rend la chaîne jSON d'une liste de messages d'erreur (ligne 4) ;

Revenons au code de souscription à l'observable :

  • lignes 19-25 : le code exécuté lorsque l'observateur reçoit la notification de fin d'émission. On annule alors l'attente (ligne 23) ce qui met à jour l'interface graphique ;

Un résultat d'exécution de l'exemple 01 produit un résultat analogue au suivant :

Image

Chaque élément de la liste est la chaîne jSON d'une valeur observée. Les champs de la chaîne jSON sont les suivants :

  • aleas : la liste de nombres aléatoires délivrée par le serveur ;
  • idClient : le n° de la requête (on peut voir que les réponses sont revenues en ordre dispersé) ;
  • on : le thread d'exécution de l'observable qui a émis cette valeur ;
  • requestAt : heure de la requête du client ;
  • responseAt : heure de la réponse du serveur ;
  • delay : attente observée par le serveur ;
  • erreur : code d'erreur renvoyé par le serveur (0=pas d'erreur) ;
  • message : message d'erreur renvoyé par le serveur (null=pas d'erreur) ;
  • observedAt : heure d'observation de la valeur observée ;
  • observedOn : thread d'observation de la valeur observée ;

9.3.8.2. Exemple-02

La classe [Example02Fragment] est la suivante :


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 {

    // mappeurs jSON
    private ObjectMapper mapperAleasUiResponse;

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

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // on demande les nombres aléatoires
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // préparation requête
            Request request = session.getRequest();
            request.setId(i);
            // on ne garde que les observables ayant un n° de client pair
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // exécution sur thread d'E/S
                            .subscribeOn(Schedulers.io()));
        }
        // observation sur thread de l'event loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // on exécute ces observables
        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() {
                // fin attente
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // on extrait l'info à afficher
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Cet exemple est analogue au précédent (ligne 38). Mais des observables obtenus dans l'exemple précédent, on ne garde que les observables ayant un n° de client pair (lignes 42-46), grâce à la méthode [filter] (ligne 41).

Les résultats obtenus sont les suivants (pour 10 requêtes) :

Image

9.3.8.3. Exemple-03

La classe [Example03Fragment] est la suivante :


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 {

  // mappeurs jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // on demande les nombres aléatoires
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // préparation requête
      Request request = session.getRequest();
      request.setId(i);
      // configuration observable
      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();
        }
      })
        // exécution sur thread d'E/S
        .subscribeOn(Schedulers.io()));
    }
    // observation sur thread de l'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // on exécute ces observables
    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() {
            // fin attente
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // on extrait l'info à afficher
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Cet exemple est analogue à Exemple-02 :

  • ligne 40 : on définit les mêmes observables que dans Exemple-02 ;
  • ligne 45 : chacune des valeurs émises par les observables précédents est transformée, par la méthode [map], en un type List<Integer> qui est la liste des nombres aléatoires générés par le serveur ;
  • ligne 58 : désormais la valeur observée est de type List<Integer> ;

Le résultat obtenu pour 10 requêtes est le suivant :

Image

9.3.8.4. Exemple-04

La classe [Example04Fragment] est la suivante :


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 {

  // mappeurs jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // on demande les nombres aléatoires
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // préparation requête
      Request request = session.getRequest();
      request.setId(i);
      // configuration 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());
        }
      })
        // exécution sur un thread d'E/S
        .subscribeOn(Schedulers.io()));
    }
    // observation sur thread de l'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // on exécute ces observables
    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() {
            // fin attente
            cancelWaiting();
          }
        }
      ));

  }
}

Cet exemple est analogue à Exemple-03, si ce n'est qu'au lieu d'utiliser, ligne 42, la méthode [map], on utilise la méthode [flatMap].

  • ligne 55 : on notera que désormais le type de la valeur observée est Integer ;

Pour 10 requêtes, on obtient les résultats suivants :

Image

On a cette fois-ci, plus de valeurs observées que de requêtes.

9.3.8.5. Exemple-05

Nous exposons maintenant la procédure à suivre pour ajouter un nouvel exemple d'observables à l'application.

Supposons qu'on veuille reproduire l'exemple [Exemple22h] du paragraphe 7.6.4 :


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 {
        // processus
        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()));
        // souscriptions
        ProcessUtils.subscribe(1, process);
    }
}
  • les valeurs de l'observable [Observable.range(1, 10)] sont d'abord regroupées en valeurs paires et impaires par la méthode [groupBy] (ligne 11) puis rassemblées en un seul observable par la méthode [concatMap] (ligne 12) ;

étape 1

On crée un nouvel exemple dans le fichier [exemples.xml] :

  

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

Ci-dessus, la ligne 8 a été rajoutée. Le nom donné à l'exemple peut être quelconque.

étape 2

On duplique la classe [Example04Fragment] dans [Example05Fragment]. Là le nom est imposé.

étape 3

On modifie le code de [Example05Fragment] de la façon suivante :


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 {

  // mappeurs jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instanciations des interfaces fonctionnelles
    // 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();
      }
    };
    // on demande les nombres aléatoires
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // préparation requête
      Request request = session.getRequest();
      request.setId(i);
      // configuration observable
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // exécution sur un thread d'E/S
        .subscribeOn(Schedulers.io());
    }
    // observation sur thread de l'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // on exécute ces observables
    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() {
            // fin attente
            cancelWaiting();
          }
        }
      ));

  }
}
  • ligne 67 : représente l'observable de l'exemple 04 : un flux d'entiers ;
  • ligne 68 : nous regroupons ce flux d'entiers selon un critère booléen que nous allons définir. Nous allons obtenir un observable de type Observable<GroupedObservable<Boolean, Integer>> qui émet donc des éléments de type GroupedObservable<Boolean, Integer> ;
  • ligne 68 : la méthode [concatMap] va produire des éléments de type Integer à partir des éléments de type GroupedObservable<Boolean, Integer> ;
  • lignes 32-59 : pour rendre plus lisible la création de l'observable lignes 67-69, nous avons isolé les instances d'interfaces fonctionnelles dont ont besoin les différents opérateurs [filter, flatMap, groupBy, concatMap] ;
  • lignes 47-52 : la méthode [groupBy] attend un paramètre de type Func1<T,K>T est le type des éléments regroupés et K le type du critère de regroupement. A partir de l'élément T, l'instance Func1<T,K> est chargée de produire la clé K de regroupement de l'élément ;
  • lignes 48-51 : les éléments de type Integer seront regroupés par parité. L'instance Func1<Integer,Boolean> produit la clé true ou false selon que l'élément doit être mis dans un groupe où l'autre. A la sortie, on a deux groupes : le groupe des éléments pairs de clé true et le groupe des éléments impairs de clé false ;
  • lignes 53-59 : la méthode [concatMap] attend un paramètre de type Func1<T,Observable<R>> et produit un observable d'éléments de type R. Le type T sera ici le type émis par l'opérateur [groupBy], ici un type GroupedObservable<Boolean, Integer> ;
  • ligne 57 : de l'élément de type [GroupedObservable<Boolean, Integer>], on produit un type Observable<Integer>. Comme l'opérateur [groupBy] a produit deux groupes, l'opérateur [concatMap] va produire deux observables de type [Observable<Integer>]. Comme [flatMap], il va les aplatir en un unique observable. Mais à la différence de [flatMap], il ne mélange pas les éléments des observables aplatis. On doit donc observer deux groupes isolés : les nombres aléatoires pairs et les autres impairs.

étape 4

On exécute l'application :

Image

et on obtient les résultats suivants :

Image

  • en [1], les nombres aléatoires pairs, en [2] les impairs ;

9.3.8.6. Pour continuer

Le lecteur est maintenant invité à créer ses propres exemples et également à expérimenter diverses valeurs pour les saisies dans le formulaire configurant les requêtes faites au serveur de nombres aléatoires.

9.3.9. Conclusion

Nous avons créé dans l'environnement Android l'architecture suivante :

Le client Android :

La couche [DAO] communique avec le serveur qui génère les nombres aléatoires affichés par la tablette Android. Ce serveur a une architecture à deux couches suivante :

La couche [DAO] faisait n requêtes HTTP au serveur de nombres aléatoires et la couche [swing] attendait de façon asynchrone les résultats de celles-ci pour les afficher. Ces n requêtes HTTP étaient faites au même serveur qui délivraient les mêmes types de réponses. Ceci nous a permis de fusionner (mergeWith) les réponses dans un unique observable.

Dans la réalité, les applications Android s'adressent à des serveurs différents et on ne fusionnera probablement pas leurs réponses. Les requêtes HTTP à ces serveurs seront gérées indépendamment les unes des autres et leurs résultats observés par des méthodes séparées.