Skip to content

9. RxJava nell'ambiente Android

9.1. Introduzione

Qui riprenderemo un'applicazione già discussa in diversi documenti:

  1. [Android per sviluppatori JEE: un modello asincrono per i client Android] (Capitolo 4);
  2. [Introduzione alla programmazione per tablet Android attraverso esempi] (Capitolo 9);
  3. [Introduzione alla programmazione per tablet Android attraverso esempi - Versione 2] (Sezione 1.11);

Si tratta di un'applicazione client/server in cui il server fornisce in modo asincrono numeri casuali che il client Android visualizza:

  • Nel documento 1, il client Android utilizza una tecnologia non standard;
  • nel documento 2, il client Android utilizza la tecnologia standard di Android per le operazioni asincrone;
  • Nel Documento 3, il client Android utilizza la stessa tecnologia del Documento 2, ma semplificata grazie all'uso delle annotazioni della libreria Android Annotations;

Il client Android è il seguente:

Il livello [DAO] comunica con il server che genera i numeri casuali visualizzati dal tablet Android. Questo server presenta la seguente architettura a due livelli:

I client interrogano determinati URL nel livello [web / JSON] e ricevono una risposta testuale in formato JSON (JavaScript Object Notation).

Suddivideremo l'analisi dell'applicazione in due fasi:

Il server web/JSON

  • il suo livello [business];
  • il suo servizio [web / JSON] implementato con Spring MVC;

Il client Android

  • il suo livello [DAO];
  • la sua attività;
  • le sue viste;

9.2. Il servizio web / JSON

Nota: il servizio web / JSON è implementato utilizzando la tecnologia Spring MVC. I lettori che non hanno familiarità con questa tecnologia possono:

  • leggere semplicemente la sezione 9.2.1, che spiega come avviare il server e come interrogarlo;
  • consultare il documento [Spring MVC and Thymeleaf by Example], in particolare il Capitolo 4, che presenta le principali annotazioni utilizzate nel codice;

9.2.1. Il progetto IntelliJ IDEA

Il servizio web / JSON presenta la seguente architettura:

Questa architettura è implementata dal seguente progetto IntelliJ IDEA [1]:

Il server viene avviato tramite [2-3]. Vengono quindi visualizzati i log della console:

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
  • Riga 12: indica che il servizio è disponibile sulla porta 8080;
  • riga 10: l'URL univoco del servizio web / JSON disponibile tramite un'operazione HTTP GET. I suoi parametri sono i seguenti:
    • [a,b]: intervallo per la generazione di numeri casuali;
    • [minCount, maxCount]: numero di numeri casuali generati, dove count è un numero casuale nell'intervallo [minCount, maxCount];
    • [minDelay, maxDelay]: il servizio attende un ritardo di delay millisecondi prima di restituire i numeri richiesti, dove delay è un numero casuale compreso tra [minDelay, maxDelay];

In un browser, richiediamo questo URL:

 

Abbiamo richiesto:

  • numeri casuali nell'intervallo [100, 200];
  • n numeri casuali dove n è nell'intervallo [10, 20];
  • un tempo di attesa di x millisecondi, dove x è compreso nell'intervallo [300, 400];

Nella risposta:

  • aleas: elenco dei numeri casuali generati;
  • delay: il tempo di attesa in millisecondi impostato dal server;
  • error: un codice di errore — 0 se non ci sono errori;
  • message: un messaggio di errore - null se non ci sono errori;

9.2.2. Le dipendenze Gradle del progetto

  

Il progetto [server] è un progetto Gradle configurato dal seguente file [build.gradle] [1]:


// 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')
}
  • riga 1: un commento che spiega come è stato generato questo file di configurazione;
  • righe 4 e 10: una dipendenza dal framework [Spring Boot], un ramo dell'ecosistema Spring. Questo framework [http://projects.spring.io/spring-boot/] consente una configurazione Spring minimale. Sulla base delle librerie presenti nel classpath del progetto, [Spring Boot] deduce una configurazione plausibile o probabile per il progetto. Pertanto, se Hibernate è presente nel classpath del progetto, [Spring Boot] dedurrà che l'implementazione JPA utilizzata è Hibernate e configurerà Spring di conseguenza. Lo sviluppatore non deve più occuparsene. Tutto ciò che gli resta da fare è configurare le impostazioni che [Spring Boot] non ha configurato di default, o quelle che [Spring Boot] ha configurato di default ma che devono essere specificate. In ogni caso, la configurazione effettuata dallo sviluppatore ha la precedenza;
  • righe 14–15: due plugin Gradle necessari per utilizzare il contenuto di questo file Gradle;
  • righe 17–20: definiscono le caratteristiche dell'archivio generato per questo progetto;
  • righe 22–23: per la compatibilità con Java 8;
  • righe 25–27: le dipendenze saranno cercate nel repository globale di Maven o nel repository locale sulla macchina;
  • riga 30: definisce una dipendenza dall'artefatto [spring-boot-starter-web]. Questo artefatto include tutti gli archivi necessari per un progetto Spring MVC. Tra questi c'è l'archivio del server Tomcat. Questo è quello che verrà utilizzato per distribuire l'applicazione web. Si noti che la versione della dipendenza non è stata specificata. Verrà utilizzata la versione specificata nel progetto [spring-boot] importato;

Per aggiornare il progetto, è necessario forzare il download delle dipendenze [1-3]:

Diamo un'occhiata [4] alle dipendenze incluse in questo file [build.gradle]:

 

Ce ne sono molte. Spring Boot per il web include le dipendenze di cui un'applicazione web Spring MVC potrebbe aver bisogno. Ciò significa che alcune potrebbero non essere necessarie. Spring Boot è l'ideale per un tutorial:

  • include le dipendenze di cui probabilmente avremo bisogno;
  • vedremo che semplifica notevolmente la configurazione del progetto Spring MVC;
  • include un server Tomcat incorporato [1], che ci evita di dover distribuire l'applicazione su un server web esterno;
  • ci permette di generare un file JAR eseguibile che include tutte le dipendenze di cui sopra. Questo file JAR può essere spostato da una piattaforma all'altra senza bisogno di riconfigurarlo.

È possibile trovare molti esempi che utilizzano Spring Boot sul sito web dell'ecosistema Spring [http://spring.io/guides]. Ora che conosciamo le dipendenze del progetto, possiamo passare al codice.

9.2.3. Il livello [business]

  

Il livello [business] avrà la seguente interfaccia [IMetier]:


package dvp.rxjava.server.metier;
 
public interface IMetier {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Questa interfaccia è quasi identica a quella discussa nell'ambiente Swing nella Sezione 8.4. Alla riga 8, il metodo [getAleas] restituisce il seguente tipo [AleasMetier]:


package dvp.rxjava.server.metier;
 
import java.util.List;
 
public class AleasMetier {
  // fields
  private int delay;
  private List<Integer> aleas;
 
  // manufacturers
  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 and setters
...
}

Il codice della classe [Metier] che implementa l'interfaccia [IMetier] è il seguente:


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) {
    // random numbers in the [a,b] interval
    // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
    // numbers are generated after a delay of milliseconds,
    // where [delay] is itself a random number in the interval [minDelay, maxDelay]
 
    // some checks
    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;
    }
    // mistakes?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // random number generator
    Random random = new Random();
    // waiting?
    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);
      }
    }
    // result generation
    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));
    }
    // return result
    return new AleasMetier(delay,nombres);
  }
 
}

Non commenteremo la classe: è simile a quella incontrata nell'ambiente Swing nella Sezione 8.4. Ci limiteremo a sottolineare i seguenti punti:

  • riga 10: l'annotazione Spring [@Service], che fa sì che Spring istanzi la classe come singola istanza (singleton) e renda il suo riferimento disponibile ad altri componenti Spring. Qui si sarebbero potute usare altre annotazioni Spring per ottenere lo stesso effetto;
  • righe 13–14: viene iniettato un mappatore JSON. Spring è un contenitore di oggetti. Questo contenitore viene istanziato all'avvio dell'applicazione web, dopodiché vengono istanziati gli oggetti definiti da un file di configurazione, per impostazione predefinita come istanza singola (singleton). Un singleton Spring può contenere riferimenti ad altri oggetti Spring. È il caso qui: il singleton [business] (righe 10–11) avrà un riferimento al singleton [mapper] (righe 13–14). Questo si chiama iniezione di dipendenze. Esistono due modi per iniettare un singleton in un altro singleton:
    • in base al tipo: ciò è possibile se il singleton da iniettare è l'unico oggetto Spring di quel tipo. È il caso dell'iniezione alle righe 13–14 (tipo ObjectMapper);
    • in base al suo nome, se più oggetti Spring hanno lo stesso tipo. In questo caso, è necessario aggiungere l'annotazione @Qualifier("singletonName") per specificare il nome del singleton;

La classe [Metier] genera eccezioni di tipo [AleaException]:


package android.exemples.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  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 and setters
 
  public int getCode() {
    return code;
  }
 
  public void setCode(int code) {
    this.code = code;
  }
}
  • riga 3: [AleasException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non gestita (non è necessario gestirla con un try/catch);
  • riga 6: viene aggiunto un codice di errore alla classe [RuntimeException];

9.2.4. Il servizio Web / JSON

  

Il servizio web / JSON è implementato da Spring MVC. Spring MVC implementa il modello architettonico MVC (Model–View–Controller) come segue:

L'elaborazione di una richiesta del client procede come segue:

  1. richiesta – gli URL richiesti hanno il formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Dispatcher Servlet] è la classe Spring che gestisce gli URL in entrata. Esso "instradano" l'URL all'azione che deve elaborarlo. Queste azioni sono metodi di classi specifiche chiamate [Controller]. La C in MVC in questo caso è la catena [Dispatcher Servlet, Controller, Action]. Se non è stata configurata alcuna azione per gestire l'URL in entrata, il [Dispatcher Servlet] risponderà che l'URL richiesto non è stato trovato (errore 404 NOT FOUND);
  1. l'elaborazione
  • l'azione selezionata può utilizzare i parametri che il [Dispatcher Servlet] le ha passato. Questi possono provenire da diverse fonti:
    • il percorso [/param1/param2/...] dell'URL,
    • i parametri dell'URL [p1=v1&p2=v2],
    • dai parametri inviati dal browser con la sua richiesta;
  • durante l'elaborazione della richiesta dell'utente, l'azione potrebbe aver bisogno del livello [business] [2b]. Una volta elaborata la richiesta del client, questa può innescare varie risposte. Un esempio classico è:
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  • l'azione indica di visualizzare una vista specifica [3]. Questa vista mostrerà i dati noti come modello di vista. Questa è la M in MVC. L'azione creerà questo modello M [2c] e indicherà di visualizzare una vista V [3];
  1. risposta - la vista V selezionata utilizza il modello M costruito dall'azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia questa risposta.

Per un servizio web / JSON, l'architettura precedente viene leggermente modificata:

  • in [4a], il modello, che è una classe Java, viene convertito in una stringa JSON da una libreria JSON;
  • in [4b], questa stringa JSON viene inviata al browser;

Torniamo al livello [web] della nostra applicazione:

Nella nostra applicazione c'è un solo controller:

  

Il servizio web / JSON invierà ai propri clienti una risposta di tipo [AleasResponse] come segue:


package dvp.rxjava.server.web;
 
import dvp.rxjava.server.metier.AleasMetier;
 
public class AleasResponse extends AleasMetier {
 
  // error code
  private int erreur;
  // error message
  private String message;
 
  // manufacturers
  public AleasResponse() {
 
  }
 
  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getters and setters
 
  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • riga 5: la classe [AleasResponse] estende la classe [AleasMetier] e quindi eredita tutti i suoi attributi (aleas, delay);
  • riga 8: un codice di errore (0 se non c'è errore);
  • riga 10: se error != 0, un messaggio di errore; null se non c'è errore;

Il controller [AleasController] è il seguente:


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 {

    // business layer
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;
 
    // random numbers in [a,b]
    // n numbers are generated with n in the range [minCount, maxCount]
    // numbers are generated after a delay of milliseconds,
    // where [delay] is a random number in the range [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 {
 
        // we prepare the answer
        AleasResponse response = new AleasResponse();
        // the business layer is used to generate random numbers
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // case of error (code and message)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // we return the answer jSON
        return mapper.writeValueAsString(response);
    }
}
  • riga 16: l'annotazione [@Controller] rende la classe [AleasController] un singleton Spring. Indica inoltre che la classe contiene metodi che gestiranno le richieste per determinati URL nell'applicazione web. Qui, ce n'è solo uno alla riga 29;
  • righe 20–21: l'annotazione [@Autowired] indica a Spring di iniettare un componente di tipo [IMetier] nel campo. Si tratterà della precedente classe [Metier]. Poiché vi abbiamo aggiunto l'annotazione [@Service], viene trattata come un componente Spring;
  • righe 22–23: l'annotazione [@Autowired] indica a Spring di iniettare un componente di tipo [ObjectMapper] nel campo. Lo definiremo tra poco;
  • riga 31: il metodo [getAleas] genera numeri casuali. Il suo nome è irrilevante. Quando viene eseguito, i parametri alle righe 31–33 sono stati inizializzati da Spring MVC. Vedremo come. Inoltre, viene eseguito perché il server web ha ricevuto una richiesta HTTP GET per l’URL alla riga 29 (attributo method);
  • riga 30: l'annotazione [@ResponseBody] indica che il risultato del metodo deve essere inviato così com'è al client. Qui, gli invieremo una stringa che sarà la stringa JSON di tipo [AleasResponse];
  • Riga 29: l'URL elaborato è della forma /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, dove {x} rappresenta una variabile. Queste varie variabili vengono assegnate ai parametri del metodo alle righe 32–33. Ciò avviene tramite l'annotazione @PathVariable("x"). Si noti che i valori {x} sono componenti di un URL e sono quindi di tipo String. La conversione da String al tipo di parametro del metodo potrebbe non andare a buon fine. Spring MVC genera quindi un'eccezione. Riassumendo: se richiedo l'URL /100/200/10/20/300/400 in un browser, il metodo getAleas alla riga 31 verrà eseguito con i parametri a=100 (riga 31), b=200 (riga 31), minCount=10 (riga 31), maxCount=20 (riga 32), minDelay=300 (riga 32), maxDelay=400 (riga 33);
  • riga 39: richiediamo un elenco di numeri casuali dal livello [business]. Ricordiamo che il metodo [business].getRandom può generare un'eccezione;
  • righe 42–43: gestione degli errori;
  • riga 46: la risposta [AleasResponse] viene restituita come stringa JSON;

9.2.5. Configurazione del progetto Spring

  

Esistono vari modi per configurare Spring:

  • utilizzando file XML;
  • con codice Java;
  • utilizzando una combinazione di entrambi;

Abbiamo scelto di configurare la nostra applicazione web utilizzando il codice Java. La classe [Config] sopra riportata gestisce questa configurazione:


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 {
  // -------------------------------- layer configuration [web]
  @Autowired
  private ApplicationContext context;
 
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }
 
  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }
 
  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }
 
  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • Riga 15: Indichiamo a Spring in quali pacchetti troverà gli oggetti da istanziare. Ne troverà due:
    • la classe [Metier] annotata con [@Service];
    • la classe [AleasController] annotata con [@Controller];
  • riga 16: l'annotazione [@EnableWebMvc] attiva le configurazioni automatiche per il framework Spring MVC;
  • righe 19–20: iniezione del contesto Spring (contenitore per gli oggetti Spring). Questa iniezione è necessaria perché l'oggetto nelle righe 22–26 la richiede;
  • il file di configurazione Spring può definire nuovi oggetti Spring utilizzando metodi annotati con [@Bean]. Il risultato del metodo diventa quindi un oggetto Spring;
  • righe 22–26: definizione del servlet del framework Spring MVC, che instrada le richieste HTTP al controller e al metodo corretti. [DispatcherServlet] è una classe Spring;
  • righe 28–31: questo specifica che questo servlet gestisce tutti gli URL;
  • righe 33–36: la presenza di questo bean attiverà il server Tomcat incluso negli archivi del progetto. Ascolterà le richieste sulla porta 8080;
  • righe 39–42: un mappatore JSON. È quello che è stato integrato negli oggetti Spring [Metier] e [AleasController];

9.2.6. Esecuzione del server web

  

Il progetto viene eseguito dalla seguente classe eseguibile [Application]:


package android.exemples.server.boot;
 
import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;
 
public class Application {
  public static void main(String[] args) {
    // application execution
    SpringApplication.run(Config.class, args);
  }
 
}
  • riga 6: la classe [Application] è una classe eseguibile (righe 7–10);
  • riga 9: il metodo statico [SpringApplication.run] è un metodo di [Spring Boot] (riga 4) che avvierà l'applicazione. Il suo primo parametro è la classe Java che configura il progetto. In questo caso, si tratta della classe [Config] appena descritta. Il secondo parametro è l'array di argomenti passati al metodo [main] (riga 7). In questo caso, non ci saranno argomenti;

Per l'esecuzione effettiva, si invita il lettore a fare riferimento alla sezione 9.2.1.

9.3. Il client Android

Nota: il seguente progetto Android è piuttosto complesso. Richiede una solida conoscenza di Android, che è possibile acquisire, ad esempio, in [Introduzione alla programmazione per tablet Android con Android Studio].

ActivityViewsLayer[DAO]UserServer

Il client avrà due componenti:

  1. un livello [Presentazione] (viste + attività);
  2. un livello [DAO] che comunica con il servizio [web / JSON] che abbiamo studiato in precedenza.

9.3.1. RxAndroid

Per comunicare in modo asincrono con il server dei numeri casuali, il client Android utilizzerà la libreria RxAndroid. Questa libreria estende RxJava all'ecosistema Android. Come abbiamo fatto per l'applicazione Swing, useremo solo una singola estensione fornita da RxAndroid: lo scheduler [AndroidSchedulers.mainThread()]. Una GUI Android segue le stesse regole di un'interfaccia Swing:

  • gli eventi vengono elaborati in un unico thread chiamato event loop o thread UI;
  • quando un evento innesca azioni asincrone, i risultati di tali azioni devono essere recuperati nel thread UI se devono essere utilizzati per aggiornare l'interfaccia utente;

Il client Android:

  • invierà più richieste asincrone al server dei numeri casuali. Queste richieste saranno eseguite sul lato client utilizzando i thread dello scheduler [Schedulers.io()];
  • Queste richieste asincrone restituiranno osservabili che verranno uniti in un unico osservabile;
  • Questo osservabile verrà osservato sul lato client nello scheduler [AndroidSchedulers.mainThread()] fornito da RxAndroid;

9.3.2. Il progetto IntelliJ IDEA

Il progetto Android si chiama [client]:

Verrà eseguito tramite [2].

Nota: l'esecuzione dipende in larga misura dalla configurazione dell'IDE IntelliJ IDEA in uso. È probabile che l'esecuzione [2] sopra indicata non funzioni al primo tentativo su un computer diverso dal mio. Configurare correttamente l'IDE IntelliJ IDEA per eseguire questo progetto può essere un compito arduo per i principianti. Ecco alcuni punti da tenere in considerazione:

  • in [3], accedere alla struttura del progetto;
  • in [4-5], il JDK e gli SDK Android installati sul mio computer. Si noti che il JDK 1.8 non è indispensabile. Android non supporta alcune funzionalità di Java 8, tra cui le lambda. Pertanto, per istanziare le interfacce funzionali, useremo classi anonime. È quindi sufficiente un JDK 1.6. Tuttavia, il progetto così come distribuito è stato configurato con un JDK 1.8;

Il file [build.gradle] [6] che configura il progetto Android è il seguente:


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

A seconda degli SDK Android disponibili, potrebbe essere necessario modificare le versioni indicate alle righe 8, 24–25 e 29.

Per installare nuovi SDK Android, utilizzare l'SDK Manager come segue [1]:

Il progetto è stato configurato per:

  • SDK API 23 [2];
  • Strumenti di compilazione SDK 23.0.3 [3];
  • Strumento SDK 25.1.3 [4]

Infine, verifica il percorso dell'SDK Android nel file [local.properties] [4], alla riga 11 qui sotto:


## 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. Esecuzione del progetto IntelliJ IDEA

Una volta creato un ambiente adeguato per il progetto, è possibile eseguirlo come segue:

  • In [1], avviare l'emulatore Android Genymotion;
  • in [2], eseguire la configurazione di esecuzione [app];
  • in [3], per creare una configurazione di esecuzione;
 
  • In [1, 3], la configurazione è stata denominata [app];
  • in [2], corrisponde all'esecuzione del modulo denominato [app];
  • in [4], specifichiamo che durante l'esecuzione l'IDE dovrebbe offrirci un dispositivo di esecuzione. In questo caso, sarà sempre l'emulatore Genymotion;
  • in [5], specifichiamo che questo dispositivo debba essere utilizzato per tutte le esecuzioni della configurazione;

L'esecuzione del progetto sull'emulatore Genymotion inizia con il seguente comando iniziale:

Image

Per scoprire cosa inserire in [1], apri una finestra di comando DOS e digita il seguente comando [ipconfig]:


C:\Program Files\Console2>ipconfig
 
Configuration IP de Windows
 
 
Carte Ethernet Ethernet :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
 
Carte réseau sans fil Connexion au réseau local* 3 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet VirtualBox Host-Only Network :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte Ethernet Ethernet 2 :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte réseau sans fil Wi-Fi :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1
 
 

Inserisci [1] per uno degli indirizzi IP del tuo computer (righe 20, 28, 32). Se disponi di un firewall di Windows, probabilmente dovrai disabilitarlo affinché l'emulatore Android possa raggiungere il server dei numeri casuali.

L'esecuzione di richieste asincrone con le informazioni di cui sopra produce i seguenti risultati:

Image

Ogni richiesta restituisce una risposta JSON con i seguenti campi:

  • aleas: i numeri casuali generati dal server;
  • idClient: l'ID della richiesta;
  • on: il thread lato client che esegue la richiesta;
  • requestAt: ora della richiesta;
  • responseAt: ora in cui è stata ricevuta la risposta;
  • delay: il tempo di attesa osservato dal server prima di restituire la risposta;
  • error: un codice di errore — 0 se non c'è alcun errore;
  • message: un messaggio di errore - null se non c'è errore;
  • observedAt: ora in cui è stata osservata la risposta;
  • observedOn: thread che osserva la risposta. In questo caso, sarà sempre [main], che si riferisce al thread dell'interfaccia utente;

Poiché le richieste sono asincrone e i tempi di attesa imposti al server sono casuali, le risposte vengono restituite in ordine sparso.

9.3.4. Le dipendenze Gradle del progetto

Il progetto richiede delle dipendenze, che specifichiamo nel file [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'
}
  • Le dipendenze alle righe 2–3 sono dipendenze standard per un progetto Android che utilizza l'SDK 23;
  • La dipendenza alla riga 5 include l'oggetto Spring [RestTemplate], che gestisce la comunicazione tra il livello [DAO] e il server;
  • la dipendenza alla riga 6 introduce la libreria JSON [Jackson] utilizzata dall'applicazione;
  • La dipendenza alla riga 7 include la libreria RxAndroid (e con essa la libreria RxJava) che il livello UI utilizza per comunicare con il livello [DAO];

9.3.5. Il manifesto dell'applicazione 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>
  • Riga 5: l'accesso a Internet deve essere consentito;

9.3.6. Il livello [DAO]

 

9.3.6.1. L'interfaccia [IDao] del livello [DAO]

L'interfaccia del livello [DAO] sarà la seguente:


package android.aleas.dao;
 
import android.aleas.fragments.Request;
import rx.Observable;
 
public interface IDao {
 
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);
 
  // URL of the web service
  public void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response to connection request
  // max wait time (ms) for server response to a request
  public void setClientTimeouts(int connectTimeout, int readTimeOut);
 
}
  • riga 12: il metodo del livello [DAO] che genera numeri casuali in modo asincrono;
  • riga 15: per fornire all'implementazione [DAO] l'URL del servizio di generazione di numeri casuali;
  • riga 19: per impostare i timeout massimi per l'implementazione [DAO], al fine di evitare attese eccessivamente lunghe quando il server non risponde;

Il metodo [getAleas] riceve tutti i suoi parametri nel seguente oggetto [Request]:


package android.aleas.fragments;
 
public class Request {
 
  // request no
  int id;
  // user input
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
 
  // manufacturers
  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 and setters
...
}

Qui possiamo vedere la maggior parte dei parametri dell'URL del server che devono essere interrogati.

Il metodo [getAleas] restituisce un tipo Observable<AleasDaoResponse>, dove la classe [AleasDaoResponse] è la seguente:


package android.aleas.dao;
 
import java.util.List;
 
public class AleasDaoResponse {
 
  // error code
  private int erreur;
  // error message
  private String message;
  // server waiting time
  private int delay;
  // random numbers delivered by the server
  private List<Integer> aleas;
  // customer status
  private ClientState clientState;
 
  // manufacturers
 
  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 and setters
...
}

Il tipo [ClientState] è il seguente:


package android.aleas.dao;
 
import org.codehaus.jackson.map.annotate.JsonFilter;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
public class ClientState {
 
  // name of execution thread
  private String on;
  // query time
  private String requestAt;
  // response time
  private String responseAt;
  // customer id
  private int idClient;
 
  // manufacturer
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }
 
  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }
 
  // private methods
 
  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }
 
  // getters and setters
...
}
  • riga 11: thread di esecuzione del livello [DAO];
  • riga 13: tempo di richiesta;
  • riga 15: tempo di risposta;
  • riga 17: numero della richiesta;

I campi [on, requestAt, idClient] vengono inizializzati dal client all'inizio della richiesta. Il campo [responseAt] viene inizializzato quando il client riceve la risposta dal server.

9.3.6.2. Implementazione del livello [DAO]

  

L'interfaccia [IDao] è implementata dalla seguente classe [Dao]:


package android.aleas.dao;
 
import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;
 
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
 
public class Dao implements IDao {
 
  // customer REST
  private RestTemplate restTemplate;
  // URL service
  private String urlServiceWebJson;
 
  // mapper jSON
  private ObjectMapper mapper;
 
  // manufacturers
  public Dao() {
    // mapper jSON
    mapper = new ObjectMapper();
  }
 
  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }
 
  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // set the URL of the REST service
    this.urlServiceWebJson = urlServiceWebJson;
  }
 
  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • riga 22: l'oggetto [RestTemplate] che gestirà la comunicazione con il server dei numeri casuali;
  • riga 24: l'URL del servizio di generazione, impostato dal metodo [setUrlServiceWebJson] alla riga 41;
  • riga 27: il mappatore JSON utilizzato per deserializzare la stringa JSON inviata dal server dei numeri casuali;
  • righe 30–33: il costruttore della classe;
  • riga 32: viene creato il mappatore JSON della riga 27;

Il metodo [setClientTimeouts] è il seguente:


  // 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());
}
  • La comunicazione del client con il server web / JSON è gestita dall'oggetto [RestTemplate] alla riga 2. Non l'abbiamo ancora inizializzato. Il metodo [setClientTimeouts] lo fa;
  • Riga 8: la classe [HttpComponentsClientHttpRequestFactory] è fornita dalla dipendenza [spring-android-rest-template]. Ci consentirà di impostare i tempi massimi di attesa per la risposta del server (righe 9–10);
  • riga 11: costruiamo l'oggetto [RestTemplate], che fungerà da canale di comunicazione con il servizio web. Passiamo come parametro l'oggetto [factory] appena costruito;
  • riga 12: il dialogo client/server può assumere varie forme. Gli scambi avvengono tramite righe di testo e dobbiamo indicare all'oggetto [RestTemplate] cosa fare con questa riga di testo. Per farlo, gli forniamo dei convertitori, ovvero classi in grado di elaborare righe di testo. La scelta del convertitore avviene generalmente tramite le intestazioni HTTP che accompagnano la riga di testo. Sulla base di queste intestazioni, l'oggetto [RestTemplate] selezionerà, tra i suoi convertitori, quello più adatto alla situazione. In questo caso, avremo un solo convertitore, un convertitore String --> String, il che significa che il tipo String ricevuto dal server non subirà alcuna trasformazione.

Il metodo [getAleas] è il più complesso:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // service execution
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL of the 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());
          // customer information
          ClientState clientState = new ClientState(request.getId());
          // synchronous http request
          String response = executeRestService("get", urlService, null);
          // deserialization of jSON server response
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // mistake?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // we forward the exception
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // enter the time of reception
            clientState.setResponseAt();
            // we forward the result to the subscriber
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // we forward the exception to the subscriber
          subscriber.onError(ex);
        } finally {
          // we signal the end of the observable
          // at runtime, we note that this method has no effect if method [onError] has been called previously - in line with theory - so we could place this instruction only in try
          subscriber.onCompleted();
        }
      }
    });
  }
  • riga 2: ricordate che dobbiamo restituire un tipo [Observable<AleasResponse>];
  • riga 3: una riga di log sulla console Android;
  • riga 5: l'oggetto [RestTemplate] garantisce la comunicazione sincrona con il server. Ciò significa che il thread di esecuzione che effettua la richiesta viene bloccato fino alla ricezione della risposta. Nell'esempio Swing, abbiamo visto come trasformare un'azione sincrona in una asincrona utilizzando il metodo [Observable.create]. Qui stiamo seguendo lo stesso approccio;
  • riga 7: il metodo [call] dell'interfaccia [Observable.OnSubscribe<AleasDaoResponse>] della riga 5. Questo metodo viene chiamato quando un osservatore si abbona all'osservabile;
  • righe 10–12: costruzione dell'URL per il servizio di numeri casuali;
  • riga 14: inizializzazione dell'oggetto [ClientState]. Qui registriamo l'ora della richiesta;
  • riga 16: richiesta HTTP sincrona. Viene restituita una risposta JSON. Il metodo [executeRestService] richiede tre parametri:
      1. il metodo HTTP da utilizzare per interrogare il servizio;
      2. l'URL del servizio;
      3. l'oggetto da inviare (tipo Object), null se il metodo HTTP non è POST;
  • 18-21: Deserializzazione della stringa JSON ricevuta in un tipo [AleasServerResponse]. Questo tipo è il seguente:

package android.aleas.dao;
 
import java.util.List;
 
public class AleasServerResponse {
 
  // error code
  private int erreur;
  // error message
  private String message;
  // server waiting time
  private int delay;
  // random numbers
  private List<Integer> aleas;
 
  // getters and setters
...
}
  • riga 23: recupera il codice di errore inviato dal server;
  • righe 24–26: se si verifica un errore, viene inoltrata un'eccezione all'abbonato;
  • riga 29: aggiorniamo [clientState], che farà parte della risposta inviata all'abbonato;
  • righe 31–33: inviamo la risposta all'abbonato. È di tipo [AleasDaoResponse];
  • righe 35–37: gestiamo tutti i casi di errore senza distinzioni. L'errore più probabile è un errore di rete;
  • riga 41: notifica della fine della trasmissione;

9.3.7. Viste dell'applicazione

  

L'applicazione presenta le seguenti due schermate:

La vista richiesta

Image

La vista di risposta

Image

9.3.7.1. La classe [MyFragment]

Ci sono due frammenti:

  • [RequestFragment] per la richiesta;
  • [ResponseFragment] per la risposta;

Entrambi i frammenti estendono la seguente classe [MyFragment]:


package android.aleas.fragments;
 
import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;
 
public abstract class MyFragment extends Fragment {
 
  // ------------- data common to all fragments
  protected MainActivity activity;
  protected Session session;
 
  public abstract void onRefresh();
 
}
  • riga 7: la classe [MyFragment] estende la classe [Fragment] di Android;
  • righe 10–11: dati condivisi da tutti i frammenti;
  • riga 10: ogni frammento conosce l'unica attività dell'applicazione;
  • riga 11: per comunicare tra loro, i frammenti utilizzano una sessione;
  • riga 13: prima di visualizzare un frammento, gli verrà chiesto di aggiornarsi con il contenuto della sessione. Questo metodo è dichiarato astratto perché viene implementato dalle classi figlie. Per questo motivo, la classe stessa è dichiarata astratta (riga 7);

La classe [Session] contiene i dati condivisi dai vari frammenti dell'applicazione. Il suo codice è il seguente:

  

package android.aleas.activity;
 
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
 
public class Session {
 
  // application activity
  private MainActivity activity;
  // number of requests
  private int nbRequests;
  // request characteristics
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL web service / jSON
  private String urlWebJson;
  // operation began
  private boolean onAir;
  // idem but a little later in time
  private boolean operationStarted;
  // the name of the example chosen by the user from the list of examples
  private String exampleName;
  // its number in the list of fragments
  private int examplePosition;
  // the example spinner adapter in the query view
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
 
  // methods
  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 and setters
...
}

Il metodo alla riga 46 crea l'oggetto [Request], che incapsula tutte le informazioni fornite dall'utente nella vista della richiesta:

  

package android.aleas.fragments;
 
public class Request {
 
  // request no
  int id;
  // user input
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
 
  // manufacturers
  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 and setters
....
}

9.3.7.2. Il frammento [RequestFragment] della richiesta

Il frammento della richiesta presenta i seguenti componenti:

Image

L'applicazione presenta una singola vista, composta da due schede:

  • [1]: la scheda della richiesta;
  • [2]: la scheda della risposta;

I componenti del frammento [RequestFragment] sono i seguenti:

N.
Tipo
Nome
Ruolo
3
ModificaTesto
edtNbRequests
numero di richieste da effettuare al servizio del generatore di numeri casuali
4
EditText
edtA, edtB
gli intervalli [a,b] dell'intervallo di generazione dei numeri;
5
EditText
edtMinCount, edtMaxCount
il servizio genera un numero pari a count, dove count è un numero casuale nell'intervallo [minCount, maxCount]
6
Modifica testo
edtMinDelay, edtMaxDelay
Il servizio attende delay millisecondi prima di generare i numeri, dove delay è un numero casuale nell'intervallo [minDelay, maxDelay]
7
Modifica testo
edtUrlServiceRest
l'URL del servizio di generazione di numeri casuali;
8
Spinner
spinnerExamples
l'elenco a discesa degli esempi. Ogni esempio illustra un metodo specifico della classe [Observable];
8
Pulsante
btnExecute
il pulsante che attiva le chiamate al servizio di generazione dei numeri;

Vengono segnalati gli errori di immissione:

Image

I componenti da 1 a 6 sono componenti [TextView] con i seguenti nomi (in ordine): txtErrorRequests, txtErrorInterval, txtErrorCount, txtErrorDelay, txtWebServiceErrorMessage.

9.3.7.3. Il frammento [ResponseFragment] della risposta

Il frammento di risposta è composto dai seguenti elementi:

Image

N.
Tipo
Nome
Ruolo
1
TextView
infoRisposte
numero di risposte ricevute
2
ListView
listResponses
elenco delle stringhe JSON ricevute dal server
3
Pulsante
btnCancel
per annullare le richieste al server

9.3.7.4. L'attività Android [MainActivity]

  

La classe [MainActivity] visualizza la seguente vista:


<?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">
 
  <!-- application bar -->
  <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">
 
    <!-- toolbar -->
    <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">
 
      <!-- waiting image -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
    </android.support.v7.widget.Toolbar>

    <!-- tab container -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>
 
  <!-- view container -->
  <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>

I componenti di questa vista sono i seguenti:

linee
Tipo
Nome
Ruolo
20-34
Barra degli strumenti
barra degli strumenti
barra degli strumenti dell'applicazione
29-34
Barra di avanzamento
pannello di caricamento
immagine segnaposto visualizzata mentre la richiesta dell'utente è in elaborazione
37-40
TabLayout
schede
barra delle schede dell'applicazione
44-51
MyPager
contenitore
il contenitore in cui vengono visualizzati i vari frammenti dell'applicazione

La classe [MyPager] è la seguente:


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 {
 
  // swipe control
  private boolean isSwipeEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // method redefinition
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe authorized?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe authorized?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • La classe [MyPager] estende la classe standard Android [ViewPager]. Utilizziamo la classe [MyPager] al posto della classe [ViewPager] esclusivamente perché vogliamo disabilitare lo scorrimento: per impostazione predefinita, con la classe [ViewPager], è possibile passare da una scheda all'altra scorrendo (scorrendo verso sinistra o verso destra). In questo caso, non vogliamo questo comportamento;
  • riga 11: il valore booleano che controlla lo scorrimento (righe 26 e 36);
  • righe 44–46: il metodo che inizializza il campo alla riga 11;

Lo scheletro dell'attività Android [MainActivity] è il seguente:


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 {
 
  // layer [DAO]
  private IDao dao;
  // the session
  private Session session;
 
  // manufacturer
  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);
  }
 
}
  • Riga 21: la classe [MainActivity] estende la classe standard di Android [AppCompatActivity]. Si tratta quindi di un'attività standard di Android;
  • riga 21: la classe [MainActivity] implementa l'interfaccia [IDao];

Tornando all'architettura dell'applicazione:

il fatto che l'attività implementi l'interfaccia del livello [DAO] permette alle viste di rimanere all'oscuro del livello [DAO]: i loro gestori di eventi comunicheranno con il livello [Activity] quando avranno bisogno di interagire con il server.

  • Riga 24: un riferimento al livello [DAO] inizializzato dal costruttore alla riga 35;
  • riga 26: un riferimento alla sessione condivisa dai frammenti, inizializzata dal costruttore alla riga 33;
  • righe 46–59: implementazione dell'interfaccia [IDao];

La classe [MainActivity] inizializza i componenti della vista ad essa associata come segue:


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

Questo codice è piuttosto standard in un'attività. Spieghiamo alcuni punti:

  • La riga 19 fa riferimento alla seguente classe [Constants]:

package android.aleas.activity;
 
abstract public class Constants {
 
  final static public int VUE_REQUEST = 0;
  final static public int VUE_RESPONSE = 1;
  final static public int CONNECT_TIMEOUT = 1000;
  final static public int READ_TIMEOUT = 6000;
  final static public int DELAY_MAX = 5000;
  final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
  • Righe 31–33: creiamo la prima scheda con il titolo [Request]. A un certo punto, avremo in memoria quanto segue:
    • il frammento [Request];
    • n frammenti di tipo [ExampleXXFragment];

La prima scheda visualizzerà sempre il frammento [Request]. La seconda scheda visualizzerà il frammento [ExampleXXFragment] corrispondente all'esempio scelto dall'utente. Il frammento visualizzato dalla seconda scheda cambia quindi nel tempo;

  • righe 37–48: il codice eseguito quando l'utente clicca su una delle schede;
  • riga 43: viene visualizzato il frammento n. 0;
  • riga 46: viene mostrato il frammento attualmente in uso (visualizzato). Il suo numero viene recuperato dalla sessione;
  • riga 62: crea i frammenti per tutti gli esempi presenti nel selettore di esempi nella vista [RequestFragment] (prima scheda);
  • riga 65: l'immagine di caricamento è attualmente nascosta;

Per comprendere il metodo [showView] (righe 43, 46) e il metodo [createResponseFragments], dobbiamo prima introdurre il gestore dei frammenti in memoria (classe inclusa nel file Java MainActivity):


  // fragment manager - must define getItem, getCount methods
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // managed fragments
    private MyFragment[] fragments;

    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }
 
    // must render fragment no. position
    @Override
    public MyFragment getItem(int position) {
      // the fragment
      return fragments[position];
    }
 
    // makes the number of fragments to manage
    @Override
    public int getCount() {
      // no. of fragments
      return fragments.length;
    }
  }
}
  • La classe [SectionsPagerAdapter] estende la classe Android [FragmentPagerAdapter]. Sovrascrive due metodi della sua classe padre:
    • il metodo [getItem], riga 15;
    • il metodo [getCount], riga 22;
  • La classe [SectionsPagerAdapter] contiene tutti i frammenti dell'applicazione. Questi sono memorizzati alla riga 5. Si noti che sono di tipo [MyFragment], come descritto nella sezione 9.3.7.1;
  • riga 8: per costruirsi, la classe [SectionsPagerAdapter] riceve i frammenti che deve gestire;
  • righe 14–18: il metodo [getItem] restituisce il frammento nella posizione [position];
  • righe 21–25: il metodo [getCount] restituisce il numero totale di frammenti;

Il metodo [createResponseFragments] crea tutti i frammenti necessari all'applicazione:


private void createResponseFragments() {
    // spinner examples
    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);
    // put the adapter in the session so that the [Request] view can retrieve it
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • Riga 3: creiamo un adattatore per lo spinner degli esempi, in questo caso un elenco di stringhe che rappresentano i nomi degli esempi. Questi nomi sono presenti nel file [layout/exemples.xml]:
  

Il file [examples.xml] contiene il seguente codice:


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

Riga 1: Questo file è il secondo parametro del metodo [createFromResource]. In [R.array.examples], [examples] è il nome dell'array (vedi riga 3 sopra), non il nome del file.

  • Riga 5: associamo un layout (display manager) all'adattatore. Ora l'adattatore dispone sia dei dati che della modalità di visualizzazione;
  • Riga 7: Aggiungiamo l'adattatore alla sessione. È qui che il [RequestFragment] che ne ha bisogno lo recupererà;

Continuiamo con il codice per il metodo [createResponseFragments]:


private void createResponseFragments() {
    // spinner examples
    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);
    // put the adapter in the session so that the [Request] view can retrieve it
    session.setSpinnerExemplesAdapter(adapter);
    // create fragment table (1 query, n responses)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // query fragment
    tFragments[0] = new RequestFragment();
    // answer fragments
    for (int i = 1; i < tFragments.length; i++) {
      // we construct the name of the fragment to be instantiated, corresponding to the example chosen by the user
      // this name must be the full name with its package - here it is directly associated with the example number in the spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // instantiate the fragment associated with the example
      MyFragment fragment;
      try {
        // class instantiation
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // the fragment has been created - we put it in the table
      tFragments[i] = fragment;
    }
    // instantiation of the fragment manager with these new fragments
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Set up the ViewPager with the sections adapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // page navigation - this instruction is important
    // here we say that on both sides of the displayed view, we must keep [tFragments.length] views initialized
    // this means that all fragments used by the application are in memory and initialized
    // if you don't do this then the default [OffscreenPageLimit] is 1
    // so if the fragment displayed is no. 3, only fragments 2 and 4 will be initialized
    // this is done by calling the [onCreateView] method of these 2 fragments - this means that in this method, you must plan to
    // regenerate the visual appearance of the fragment the last time it was used
    // there's code that can't stand being run twice - it creates a huge mess and is complex to manage
    // here we've chosen to avoid these difficulties - in the logs, we can see that when the application starts, all fragments are created
    // and their method [onCreateView] executed - it's never executed again -
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
  }
  • riga 9: creazione dell'array che conterrà tutti i frammenti dell'app;
  • riga 11: il primo frammento è il frammento di query;
  • righe 13–28: creeremo tanti frammenti quanti sono gli esempi. Questi frammenti estendono tutti il frammento di risposta [ResponseFragment] e implementano solo ciò che è specifico dell'esempio: la creazione dei valori osservati. Questi valori variano da un esempio all'altro;
  • riga 16: un frammento di esempio ha un nome standard: ExampleXXFragment, dove XX è la sua posizione nello spinner degli esempi più 1. XX è anche il numero del frammento dell'esempio nel gestore dei frammenti;
  • riga 21: istanziazione del frammento dell'esempio n. i dallo spinner:
    • Class.forName(exampleName): carica il frammento in memoria;
    • Class.forName(exampleName).getConstructors()[0]: ottiene un riferimento al primo costruttore della classe. La classe ExampleXXFragment ha un solo costruttore. Pertanto, verrà ottenuto un riferimento a questo costruttore;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) istanzia un oggetto di tipo ExampleXXFragment utilizzando il costruttore del passaggio precedente. new Object[]{} rappresenta i parametri passati a questo costruttore. Poiché il costruttore della classe ExampleXXFragment non prevede alcun parametro, viene passato un array vuoto di oggetti;
  • riga 27: questo frammento viene aggiunto all'array dei frammenti;
  • riga 30: abbiamo visto che il costruttore del gestore di frammenti [SectionsPagerAdapter] si aspettava come parametro l'array di frammenti che doveva gestire. Ora lo passiamo al costruttore;
  • riga 22: il contenitore di frammenti [mViewPager] della vista associata all'attività [MainActivity] viene collegato qui al gestore di frammenti: il contenitore di frammenti [mViewPager] visualizza i frammenti provenienti dal gestore di frammenti;
  • riga 43: leggi i commenti: l'istruzione afferma essenzialmente che tutti i frammenti devono rimanere nello stato in cui il codice li mette, indipendentemente da quale frammento sia attualmente visualizzato. Quindi, quando ci torniamo, lo troviamo nello stato in cui l'abbiamo lasciato;
  • riga 45: il contenitore dei frammenti [mViewPager] è di tipo [MyPager], il che disabilita lo scorrimento;

Il metodo [MainActivity.showView] è il seguente:


  // display view n° [position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // displays the requested view - goes directly to the view (second parameter set to false)
    // without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
    mViewPager.setCurrentItem(position, false);
}
  • riga 3: vogliamo visualizzare il frammento #position;
  • riga 4: questo frammento viene richiesto al gestore dei frammenti e quindi aggiornato. Dall'ultima volta che è stato visualizzato, la sessione potrebbe essere cambiata. Il frammento deve quindi ispezionare la sessione per verificare se è necessario un aggiornamento;
  • riga 7: il frammento viene visualizzato dal [ViewPager]. Poiché questo è stato associato al gestore dei frammenti, verrà visualizzato il frammento #[posizione], quello che abbiamo appena aggiornato alla riga 4;

Concludiamo con i due metodi per gestire l'attesa:


  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. Il frammento [RequestFragment]

La classe [RequestFragment] è la seguente:


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 of the web service
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // number of requests
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // generation interval
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // delay
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // number of values generated
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // button
  private Button btnExecuter;
  // list of answers
  private ListView listReponses;
  private TextView infoReponses;
  // spinner examples
  private Spinner spinnerExemples;
 
  // seizures
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;
 
  // manufacturer
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // recover activity and session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // create the fragment view from its definition XML
    View view = inflater.inflate(R.layout.request, container, false);
    // components
    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);
 
    // execute] button
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });
 
    // initially no error messages
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner examples
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // result
    return view;
  }
...
}
  • Riga 16: la classe [RequestFragment] estende la classe [MyFragment] (vedere la Sezione 9.3.7.1);
  • righe 18–42: i componenti visivi del frammento (vedere la sezione 9.3.7.2);
  • righe 45–52: input dell'utente nel modulo;
  • il costruttore (righe 55–58) e il metodo [onCreateView] vengono eseguiti quando l'attività [MainActivity] crea tutti i frammenti nell'applicazione. Ciò avviene una sola volta;
  • riga 61: il codice del metodo [onCreateView] è standard. Si noti alla riga 102 che l'adattatore spinner degli esempi viene recuperato dalla sessione. Si noti inoltre alla riga 91 che il clic sul pulsante [Execute] viene gestito dal metodo [doExecute];
  • Righe 64–65: i campi [activity] e [session] appartengono alla classe padre [MyFragment];

Il metodo [doExecute] è il seguente:


  // seizures
  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() {
    // valid entries?
    if (isPageValid()) {
      // we put info in session
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // store the URL of the web service
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // action in progress
      session.setOnAir(true);
      // but not started
      session.setOperationStarted(false);
      // the answer fragment is displayed
      activity.selectTab(Constants.VUE_RESPONSE);
      // we start waiting
      beginWaiting();
    }
}
  • riga 15: Non ci soffermeremo sul metodo [ispageValid]. Esso verifica la validità delle voci e restituisce true solo se sono tutte valide. In questo caso, vengono utilizzate per inizializzare i campi nelle righe 2–9;
  • Riga 17: I vari valori inseriti vengono salvati nella sessione:
    • [spinnerExemples.getSelectedItem().toString()] è il nome dell'esempio selezionato dall'utente ed è memorizzato in [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] è l'ID del frammento associato all'esempio, che è stato memorizzato (il frammento) dal gestore dei frammenti. Questo ID viene memorizzato in [session.examplePosition];
  • riga 19: l'URL del servizio web / JSON viene passato all'attività, che a sua volta lo passa al livello [DAO];
  • righe 21–24: si noti che sta per iniziare un'operazione;
  • riga 26: verrà visualizzata la scheda di risposta. Per capire cosa succederà, ricordiamo il codice [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();
}
  • Inizialmente, l'attività aveva creato solo la scheda di richiesta (scheda n. 0);
  • righe 6–11: crea la scheda di risposta (scheda n. 1) se non è stata ancora creata;
  • riga 14: selezioniamo la posizione della scheda (0 o 1). Questo inserisce l'evento [onTabSelected] nella coda del ciclo di eventi dell'app Android;

Il gestore dell'evento [onTabSelected] in [MainActivity] è il seguente:


      @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());
        }
}

Nel caso della scheda [Response], viene eseguita la riga 9. Verrà visualizzato il frammento con ID [session.getExamplePosition()]. Ad esempio, per [example-03], l'ID memorizzato in [session.examplePosition] è 3. La riga 10 visualizza quindi il frammento con ID 3. L'array di frammenti inizialmente creato dall'attività è [RequestFragment, Example01Fragment, Example02Fragment, Example03Fragment,..]. Pertanto, è effettivamente [Example03Fragment] che verrà visualizzato. Viene visualizzato dal seguente codice:


  // display view n° [position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // displays the requested view - goes directly to the view (second parameter set to false)
    // without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
    mViewPager.setCurrentItem(position, false);
}

Possiamo notare che il frammento verrà aggiornato (riga 4) prima di essere visualizzato (riga 7).

9.3.7.6. Il frammento [ResponseFragment]

La classe [ResponseFragment] visualizza le risposte provenienti dal server. Il suo codice è il seguente:


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 {
 
  // list of answers
  private ListView listReponses;
  private TextView infoReponses;
  // button
  private Button btnAnnuler;
 
  // mapper jSON
  private ObjectMapper mapper;
 
  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // recover activity and session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // create the fragment view from its definition XML
    View view = inflater.inflate(R.layout.response, container, false);
    // components
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // result
    return view;
  }
...
  // method to be executed (by explicit code) before each fragment display
  public void onRefresh() {
...
  }
}
  • riga 21: la classe [ResponseFragment] estende la classe [MyFragment];
  • righe 23–27: i componenti del frammento;
  • righe 32–36: il costruttore viene eseguito una sola volta, durante la creazione iniziale dei frammenti di esempio da parte dell'attività. Questo perché tutti i frammenti di esempio estendono il frammento [ResponseFragment]. Quando vengono istanziati, viene chiamato il costruttore della loro classe padre [ResponseFragment];
  • riga 35: inizializza il mappatore JSON della riga 30 utilizzato per visualizzare la stringa JSON di uno stack di eccezioni;
  • righe 38–59: il metodo [onCreateView] viene eseguito una sola volta, durante la creazione iniziale dei frammenti di esempio da parte dell'attività. Contiene codice standard presente in un'applicazione Android;
  • righe 52–56: il metodo eseguito quando si fa clic sul pulsante [Cancel] è il metodo [doCancel];
  • righe 62–64: il metodo [onRefresh] viene eseguito ogni volta che viene visualizzata la scheda [Response];

Grazie ai vari log inseriti nei metodi chiave, possiamo vedere cosa succede all'avvio dell'app:

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
  • riga 1: creazione del frammento [RequestFragment];
  • righe 2–9: creazione dei frammenti per i 4 esempi nell'applicazione;
  • riga 10: inizializzazione del frammento [RequestFragment];
  • righe 11–14: inizializzazione dei frammenti per i 4 esempi nell'applicazione;

Successivamente, non vediamo più alcuna chiamata a questi metodi.

Il metodo [ResponseFragment.onRefresh] è il seguente:


  // 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();
    }
}
  • Riga 5: Verifichiamo se il [RequestFragment] ha effettuato una richiesta (session.isOnAir) e se è stato avviato (isOperationStarted). Se il [RequestFragment] ha effettuato una richiesta e non è già in esecuzione, l'operazione viene avviata (righe 7–8);
  • una volta avviata l'operazione, poiché è asincrona, l'utente può navigare tra le due schede. Se l'utente torna alla scheda [Response] e un'operazione è in corso, le righe 7–8 non vengono eseguite;

Il metodo [doExecute] alla riga 8 esegue l'operazione richiesta dall'utente:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // start waiting
    beginWaiting();
    // preparation execution
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // create and execute observables for the chosen example
    createAndExecuteObservables();
}

// method implemented by child classes
protected abstract void createAndExecuteObservables();
  • riga 10: crea, esegue e osserva gli osservabili. Questi sono diversi per ogni esempio. Ecco perché il metodo [createAndExecuteObservables] è astratto (riga 14). Sarà implementato dai frammenti [ExampleXXFragment] che estendono la classe [ResponseFragment];
  • riga 6: l'elenco delle sottoscrizioni viene cancellato;
  • riga 7: l'elenco che mostra le risposte viene cancellato;
  • riga 8: conta il numero di risposte ricevute;

Le classi figlie [ExampleXXFragment] affidano al seguente metodo [showAlea] il compito di visualizzare gli elementi che osservano:


  protected void showAlea(String data) {
    // one more piece of information
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 more answer
    reponses.add(0, data);
    Log.d("rxjava", data);
    // maj of UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • Riga 1: vediamo che l'elemento osservato arriva come stringa. Si tratta in realtà della stringa JSON dell'elemento osservato. Questo ci permette di avere un unico metodo per visualizzare l'elemento osservato indipendentemente dal suo esatto tipo Java;
  • riga 6: l'elemento [data] osservato viene aggiunto alla prima posizione dell'elenco delle risposte. L'utente vede quindi le risposte più recenti in cima all'elenco;

L'attesa è gestita dai seguenti metodi [beginWaiting] e [cancelWaiting]:


  private void beginWaiting() {
    // we set the hourglass
    activity.beginWaiting();
    // the [Cancel] button is displayed
    btnAnnuler.setVisibility(View.VISIBLE);
  }
 
  protected void cancelWaiting() {
    // end of wait
    activity.cancelWaiting();
    // the [Cancel] button is hidden
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Chiamano i metodi con lo stesso nome nell'attività e semplicemente mostrano o nascondono il pulsante [Annulla].

Il clic sul pulsante [Annulla] viene gestito dal seguente codice:


  protected void doAnnuler() {
    // on annule tous les abonnements
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fin de l'attente
    cancelWaiting();
}
  • righe 3–7: annulla tutte le sottoscrizioni una per una;

9.3.8. Esempi di osservabili

9.3.8.1. Esempio-01

Le classi [ExampleXXFragment] sono progettate per creare, eseguire e osservare gli osservabili. I valori osservati vengono visualizzati dalla classe padre [ResponseFragment].

La classe [Example01Fragment] è la seguente:

  

package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
 
public class Example01Fragment extends ResponseFragment {
 
    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;
 
    // manufacturer
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // filters jSON
        mapperAleasUiResponse = new ObjectMapper();
    }
 
    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // we ask for the random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // observable configuration n° i
            // request to server
            Request request = session.getRequest();
            request.setId(i);
            // observable executed on computation thread
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observation on event loop thread;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // we execute all these 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) {
        // extract the information to be displayed
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • riga 36: l'unico osservabile che verrà generato;
  • righe 37–44: generazione e configurazione dei vari osservabili che vengono uniti (riga 43) nell'osservabile alla riga 36;
  • riga 43: l'osservabile viene eseguito in un thread dello scheduler [Schedulers.io()]. La richiesta HTTP al server verrà eseguita in questo thread;
  • riga 46: l'osservabile finale viene osservato sul thread del ciclo di eventi;
  • righe 48–57: esecuzione degli osservabili, ovvero delle richieste al server dei numeri casuali. Android non supporta ancora Java 8 e le sue lambda. Pertanto, qui vengono utilizzate classi anonime per istanziare le interfacce funzionali di RxJava;
  • righe 49–52: azione eseguita quando l'osservatore riceve un nuovo elemento di tipo [AleasDaoResponse] dall'osservabile (vedere la sezione 9.3.6.1);
  • riga 51: chiamata al metodo [showAlea] della classe padre. Ricordiamo che esso si aspetta una stringa. Questa viene fornita dal metodo [getDataFrom] nelle righe 59–68;
  • riga 63: restituiamo la stringa JSON di tipo [AleasUiResponse] come segue:

package android.aleas.fragments;
 
import android.aleas.dao.AleasDaoResponse;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
public class AleasUiResponse {
 
  // answer [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // observation thread
  private String observedOn;
  // observation time
  private String observedAt;
 
  // manufacturers
  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 and setters
...
}
  • Alla risposta del livello [DAO] (riga 11), aggiungiamo due informazioni:
    • riga 13: il thread di osservazione;
    • riga 15: l'ora dell'osservazione;

Torniamo al codice di sottoscrizione:


    @Override
    public void createAndExecuteObservables() {
...
        // we execute all these 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) {
                // exception is displayed
                showAlea(getMessagesFromThrowable(th));
                // after receiving an exception, the observable receives neither onNext nor onCompleted
                // forced to cancel the subscription by hand
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // end waiting
                cancelWaiting();
            }
        }));
}
  • righe 11–18: caso in cui l'osservatore riceve un'eccezione;
  • riga 14: utilizziamo nuovamente il metodo [showAlea] della classe padre per visualizzare l'eccezione. Il metodo [getMessagesFromThrowable] è un metodo della classe padre [ResponseFragment] che genera una stringa da un'eccezione:

  // 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();
    }
}
  • riga 11: restituisce la stringa JSON di un elenco di messaggi di errore (riga 4);

Torniamo al codice di sottoscrizione dell'osservabile:

  • righe 19–25: il codice eseguito quando l'osservatore riceve la notifica di fine emissione. Quindi annulliamo l'attesa (riga 23), il che aggiorna la GUI;

L'esecuzione dell'Esempio 01 produce un output simile al seguente:

Image

Ogni elemento dell'elenco è la stringa JSON di un valore osservato. I campi della stringa JSON sono i seguenti:

  • aleas: l'elenco di numeri casuali fornito dal server;
  • idClient: il numero della richiesta (si può notare che le risposte sono arrivate in ordine sparso);
  • on: il thread di esecuzione dell'osservabile che ha emesso questo valore;
  • requestAt: ora della richiesta del client;
  • responseAt: ora della risposta del server;
  • delay: ritardo osservato dal server;
  • error: codice di errore restituito dal server (0=nessun errore);
  • message: messaggio di errore restituito dal server (null=nessun errore);
  • observedAt: ora in cui è stato osservato il valore osservato;
  • osservatoSu: thread che osserva il valore osservato;

9.3.8.2. Esempio-02

La classe [Example02Fragment] è la seguente:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
 
public class Example02Fragment extends ResponseFragment {

    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;
 
    // manufacturer
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // filter jSON
        mapperAleasUiResponse = new ObjectMapper();
    }
 
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // we ask for the random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // request preparation
            Request request = session.getRequest();
            request.setId(i);
            // only observables with an even customer number are kept
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // execution on I/O thread
                            .subscribeOn(Schedulers.io()));
        }
        // observation on event loop thread
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // these observables are executed
        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() {
                // end waiting
                cancelWaiting();
            }
        }));
 
    }
 
    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // extract the information to be displayed
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
 
}

Questo esempio è simile al precedente (riga 38). Tuttavia, tra le osservabili ottenute nell'esempio precedente, conserviamo solo quelle con un numero di cliente pari (righe 42–46), utilizzando il metodo [filter] (riga 41).

I risultati ottenuti sono i seguenti (per 10 richieste):

Image

9.3.8.3. Esempio-03

La classe [Example03Fragment] è la seguente:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
import java.util.List;
 
public class Example03Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // we ask for the random numbers
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      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();
        }
      })
        // execution on I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    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() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
 
  private String getDataFrom(List<Integer> aleas) {
    // extract the information to be displayed
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }
 
}

Questo esempio è simile all'Esempio-02:

  • riga 40: definiamo gli stessi osservabili dell'Esempio-02;
  • riga 45: ogni valore emesso dagli osservabili precedenti viene trasformato, utilizzando il metodo [map], in un tipo List<Integer>, che è l'elenco di numeri casuali generati dal server;
  • riga 58: il valore osservato è ora di tipo List<Integer>;

Il risultato ottenuto per 10 richieste è il seguente:

Image

9.3.8.4. Esempio-04

La classe [Example04Fragment] è la seguente:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
public class Example04Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // we ask for the random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      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());
        }
      })
        // execution on an I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    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() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
}

Questo esempio è simile all'Esempio-03, tranne per il fatto che invece di utilizzare il metodo [map] alla riga 42, utilizziamo il metodo [flatMap].

  • riga 55: si noti che il tipo del valore osservato è ora Integer;

Per 10 richieste, otteniamo i seguenti risultati:

Image

Questa volta, i valori osservati sono più numerosi delle richieste.

9.3.8.5. Esempio-05

Descriveremo ora la procedura per aggiungere un nuovo esempio di osservabili all'applicazione.

Supponiamo di voler riprodurre l'esempio [Esempio22h] della Sezione 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 {
        // process
        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()));
        // subscriptions
        ProcessUtils.subscribe(1, process);
    }
}
  • I valori dell'osservabile [Observable.range(1, 10)] vengono prima raggruppati in valori pari e dispari dal metodo [groupBy] (riga 11) e poi combinati in un unico osservabile dal metodo [concatMap] (riga 12);

Passaggio 1

Creiamo un nuovo esempio nel file [examples.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>

Sopra è stata aggiunta la riga 8. Il nome assegnato all'esempio può essere qualsiasi cosa.

Passaggio 2

Duplica la classe [Example04Fragment] in [Example05Fragment]. In questo caso, il nome è fisso.

Passaggio 3

Modifica il codice in [Example05Fragment] come segue:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;
 
public class Example05Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instantiations of functional interfaces
    // filter
    Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
      @Override
      public Boolean call(AleasDaoResponse aleasDaoResponse) {
        return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
      }
    };
    // flatMap
    Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
        return Observable.from(aleasDaoResponse.getAleas());
      }
    };
    // groupBy
    Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
      @Override
      public Boolean call(Integer integer) {
        return integer % 2 == 0;
      }
    };
    // concatMap
    Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
        return integerIntegerGroupedObservable.asObservable();
      }
    };
    // we ask for the random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // execution on an I/O thread
        .subscribeOn(Schedulers.io());
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    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() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
}
  • riga 67: rappresenta l'osservabile dell'Esempio 04: un flusso di numeri interi;
  • riga 68: raggruppiamo questo flusso di numeri interi in base a un criterio booleano che definiremo. Otterremo un osservabile di tipo Observable<GroupedObservable<Boolean, Integer>>, che quindi emette elementi di tipo GroupedObservable<Boolean, Integer>;
  • riga 68: il metodo [concatMap] produrrà elementi di tipo Integer a partire da elementi di tipo GroupedObservable<Boolean, Integer>;
  • righe 32–59: per rendere più leggibile la creazione dell'osservabile nelle righe 67–69, abbiamo isolato le istanze delle interfacce funzionali richieste dai vari operatori [filter, flatMap, groupBy, concatMap];
  • righe 47–52: il metodo [groupBy] richiede un parametro di tipo Func1<T,K>, dove T è il tipo degli elementi raggruppati e K è il tipo del criterio di raggruppamento. Dato un elemento T, l'istanza Func1<T,K> è responsabile della produzione della chiave di raggruppamento K per quell'elemento;
  • righe 48–51: Gli elementi di tipo Integer saranno raggruppati in base alla parità. L'istanza Func1<Integer,Boolean> produce la chiave true o false a seconda che l'elemento debba essere inserito in un gruppo o nell'altro. Il risultato sono due gruppi: il gruppo degli elementi pari con chiave true e il gruppo degli elementi dispari con chiave false;
  • righe 53–59: il metodo [concatMap] richiede un parametro di tipo Func1<T, Observable<R>> e produce un osservabile di elementi di tipo R. Il tipo T qui è il tipo emesso dall'operatore [groupBy], in questo caso un GroupedObservable<Boolean, Integer>;
  • riga 57: dall'elemento di tipo [GroupedObservable<Boolean, Integer>], produciamo un tipo Observable<Integer>. Poiché l'operatore [groupBy] ha prodotto due gruppi, l'operatore [concatMap] produrrà due osservabili di tipo [Observable<Integer>]. Come [flatMap], li appiattirà in un unico osservabile. Ma a differenza di [flatMap], non mescola gli elementi degli osservabili appiattiti. Dovremmo quindi osservare due gruppi separati: i numeri casuali pari e quelli dispari.

Passaggio 4

Eseguiamo l'applicazione:

Image

e otteniamo i seguenti risultati:

Image

  • in [1], i numeri casuali pari; in [2], quelli dispari;

9.3.8.6. Per continuare

Il lettore è ora invitato a creare i propri esempi e anche a sperimentare con vari valori per gli input nella forma che configura le richieste inviate al server dei numeri casuali.

9.3.9. Conclusione

Abbiamo creato la seguente architettura nell'ambiente Android:

Il client Android:

Il livello [DAO] comunica con il server che genera i numeri casuali visualizzati dal tablet Android. Questo server presenta la seguente architettura a due livelli:

Il livello [DAO] ha effettuato n richieste HTTP al server dei numeri casuali, mentre il livello [swing] ha atteso in modo asincrono i risultati di tali richieste per visualizzarli. Queste n richieste HTTP sono state inviate allo stesso server, che ha restituito lo stesso tipo di risposte. Ciò ci ha permesso di unire le risposte in un unico osservabile.

In realtà, le applicazioni Android comunicano con server diversi e probabilmente non uniremo le loro risposte. Le richieste HTTP a questi server saranno gestite indipendentemente l'una dall'altra e i loro risultati saranno osservati utilizzando metodi separati.