Skip to content

9. RxJava nell'ambiente Android

9.1. Introduction

Riprenderemo qui un’applicazione già trattata in diversi documenti:

  1. [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (capitolo 4);
  2. [Introduction à la programmation de tablettes Android par l'exemple] (capitolo 9);
  3. [Introduction à la programmation de tablettes Android par l'exemple - version 2] (paragrafo 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 del livello [web / JSON] e ricevono una risposta testuale nel formato JSON (JavaScript Object Notation).

Suddivideremo l’analisi dell’applicazione in due fasi:

Il server web / jSON

  • il suo livello [métier];
  • 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 tramite la tecnologia Spring MVC. Il lettore che non la conosce può:

  • limitarsi a leggere il paragrafo 9.2.1, che spiega come avviare il server e come interrogarlo;
  • consultare il documento [Spring MVC et Thymeleaf par l'exemple], in particolare il capitolo 4 che illustra 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 da [2-3]. Vengono quindi visualizzati i log sulla 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'unico URL del servizio web / jSON disponibile tramite un'operazione HTTP GET. I suoi parametri sono i seguenti:
    • [a,b]: intervallo di generazione dei numeri casuali;
    • [minCount, maxCount]: count numeri casuali vengono generati, dove count è un numero casuale nell’intervallo [minCount, maxCount];
    • [minDelay, maxDelay]: il servizio attende delay millisecondi prima di restituire i numeri richiesti, dove delay è un numero casuale compreso in [minDelay, maxDelay];

In un browser, richiediamo questo URL:

 

Sono stati richiesti:

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

Nella risposta:

  • aleas: elenco dei numeri casuali generati;
  • delay: il tempo di attesa in millisecondi che il server ha mantenuto;
  • errore: un codice di errore - 0 se non ci sono errori;
  • messaggio: un messaggio di errore - null se non ci sono errori;

9.2.2. Le dipendenze Gradle del progetto

  

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


// generato da http://start.spring.io/ (maggio 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 minima di Spring. In base ai file presenti nel Classpath del progetto, [Spring Boot] deduce una configurazione plausibile o probabile per lo stesso. Pertanto, se Hibernate è presente nel Classpath del progetto, [Spring Boot] dedurrà che l’implementazione JPA utilizzata sarà Hibernate e configurerà Spring di conseguenza. Lo sviluppatore non deve più occuparsene. Gli resta quindi solo da effettuare le configurazioni che [Spring Boot] non ha impostato di default o quelle che [Spring Boot] ha impostato di default ma che devono essere specificate. In ogni caso, è la configurazione effettuata dallo sviluppatore ad avere l’ultima parola;
  • 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 verranno cercate nel repository globale di Maven o nel repository locale del computer;
  • 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 vi è l'archivio di un server Tomcat. Sarà proprio questo a essere utilizzato per distribuire l'applicazione web. Si noti che la versione della dipendenza non è stata specificata. Verrà utilizzata quella indicata nel progetto importato [spring-boot];

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

Esaminiamo le dipendenze introdotte dal file [4], ovvero il file [build.gradle]:

 

Sono davvero numerose. Spring Boot per il web ha incluso le dipendenze di cui un'applicazione web Spring MVC avrà probabilmente bisogno. Ciò significa che alcune potrebbero essere superflue. Spring Boot è l'ideale per un tutorial:

  • fornisce le dipendenze di cui probabilmente avremo bisogno;
  • vedremo che semplifica notevolmente la configurazione del progetto Spring MVC;
  • include un server Tomcat integrato [1], il che ci evita di dover distribuire l’applicazione su un server web esterno;
  • consente di generare un file JAR eseguibile che include tutte le dipendenze sopra citate. Questo file JAR può essere trasferito da una piattaforma all’altra senza necessità di riconfigurazione.

Sul sito dell’ecosistema Spring [http://spring.io/guides] sono disponibili numerosi esempi che utilizzano Spring Boot. Ora che conosciamo le dipendenze del progetto, possiamo passare al codice.

9.2.3. Il livello [métier]

  

Il livello [métier] avrà la seguente interfaccia [IMetier]:


package dvp.rxjava.server.metier;

public interface IMetier {
  // numeri casuali nell'intervallo [a,b]
  // vengono generati n numeri, dove n è a sua volta un numero casuale nell'intervallo [minCount, maxCount]
  // i numeri vengono generati dopo un ritardo di delay millisecondi,
  // dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Questa interfaccia è quasi identica a quella esaminata nell’ambiente Swing al paragrafo 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 {
  // campi
  private int delay;
  private List<Integer> aleas;

  // costruttori
  public AleasMetier(){

  }

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

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

  // getter e setter
...
}

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) {
    // numeri casuali nell'intervallo [a,b]
    // vengono generati n numeri, dove n è a sua volta un numero casuale nell'intervallo [minCount, maxCount]
    // i numeri vengono generati dopo un ritardo di delay millisecondi,
    // dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]

    // alcune verifiche
    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;
    }
    // errori?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // generatore di numeri casuali
    Random random = new Random();
    // in attesa?
    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);
      }
    }
    // generazione del risultato
    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));
    }
    // restituzione del risultato
    return new AleasMetier(delay,nombres);
  }

}

Non commentiamo la classe: è analoga a quella incontrata nell'ambiente Swing al paragrafo 8.4. Si notino semplicemente i seguenti punti:

  • riga 10: l’annotazione Spring [@Service], che farà sì che Spring istanzi la classe in un unico esemplare (singleton) e renda il suo riferimento disponibile per altri componenti Spring. Qui avrebbero potuto essere utilizzate 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 e gli oggetti definiti in un file di configurazione vengono quindi istanziati, per impostazione predefinita, in un unico esemplare (singleton). Un singleton Spring può contenere riferimenti ad altri oggetti Spring. È proprio questo il caso: il singleton [metier] (righe 10-11) avrà un riferimento al singleton [mapper] (righe 13-14). Questo processo è chiamato 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, in questo esempio, dell’iniezione alle righe 13-14 (tipo ObjectMapper);
    • tramite il suo nome, se più oggetti Spring hanno lo stesso tipo. In tal caso è necessario aggiungere l’annotazione @Qualifier(“nomDuSingleton”) per specificare il nome del singleton;

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


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // codice di errore
  private int code;

  // costruttori
  public AleaException() {
  }

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

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

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

  // getter e setter

  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 controllata (non è obbligatorio gestirla con un try/catch);
  • riga 6: alla classe [RuntimeException] viene aggiunto un codice di errore;

9.2.4. Il servizio web / JSON

  

Il servizio web / JSON è implementato da Spring MVC. Spring MVC implementa il modello di architettura denominato MVC (Modello – Vista – Controller) nel modo seguente:

L’elaborazione di una richiesta da parte di un cliente avviene nel modo seguente:

  1. richiesta – le URL richieste hanno la forma http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] è la classe di Spring che gestisce le URL in entrata. Essa "instradano" l'URL verso l'azione che deve elaborarla. Queste azioni sono metodi di classi specifiche denominate [Contrôleurs]. La parte "C" di MVC è in questo caso la stringa [Dispatcher Servlet, Contrôleur, Action]. Se non è stata configurata alcuna azione per elaborare l’URL in entrata, il servlet [Dispatcher Servlet] risponderà che l’URL richiesto non è stato trovato (errore 404 NOT FOUND);
  1. elaborazione
  • l'azione selezionata può utilizzare i parametri parami che il servlet [Dispatcher Servlet] le ha trasmesso. Questi possono provenire da diverse fonti:
    • dal percorso [/param1/param2/...] del URL,
    • dai parametri [p1=v1&p2=v2] del URL,
    • dai parametri inviati dal browser insieme alla sua richiesta;
  • nell'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere il livello [metier] [2b]. Una volta elaborata la richiesta del cliente, questa può generare diverse risposte. Un esempio classico è:
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  • l’azione richiede la visualizzazione di una determinata vista [3]. Questa vista visualizzerà i dati denominati modello della vista. È la M di MVC. L’azione creerà questo modello M [2c] e richiederà la visualizzazione di una vista V [3];
  1. risposta: la vista V selezionata utilizza il modello M creato dall’azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia tale risposta.

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

  • in [4a], il modello, che è una classe Java, viene trasformato 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 {

  // codice di errore
  private int erreur;
  // messaggio di errore
  private String message;

  // costruttori
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getter e setter

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • riga 5: la classe [AleasResponse] estende la classe [AleasMetier] e ne riprende quindi tutti gli attributi (aleas, delay);
  • riga 8: un codice di errore (0 se non c’è errore);
  • riga 10: se erreur!=0, un messaggio di errore; se non ci sono errori, null;

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 {

    // livello business
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // numeri casuali in [a,b]
    // vengono generati n numeri con n nell'intervallo [minCount, maxCount]
    // i numeri vengono generati dopo un'attesa di delay millisecondi,
    // dove [delay] è un numero casuale nell'intervallo [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 {

        // si prepara la risposta
        AleasResponse response = new AleasResponse();
        // si utilizza il livello business per generare i numeri casuali
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // caso di errore (codice e messaggio)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // si restituisce la risposta 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 alcune URL dell'applicazione web. In questo caso, ce n’è solo una alla riga 29;
  • righe 20-21: l’annotazione [@Autowired] richiede a Spring di iniettare nel campo un componente di tipo [IMetier]. Si tratterà della precedente classe [Metier]. Poiché a quest’ultima abbiamo applicato l’annotazione [@Service], essa viene gestita come un componente Spring;
  • righe 22-23: l’annotazione [@Autowired] richiede a Spring di iniettare nel campo un componente di tipo [ObjectMapper]. Lo definiremo tra poco;
  • riga 31: il metodo [getAleas] genera i numeri casuali. Il suo nome non ha importanza. Quando viene eseguito, i parametri delle righe 31-33 sono stati inizializzati da Spring MVC. Vedremo come. Inoltre, se viene eseguito, è perché il server web ha ricevuto una richiesta HTTP GET per l’URL della riga 29 (attributo method);
  • riga 30: l'annotazione [@ResponseBody] indica che il risultato del metodo deve essere inviato così com'è al client. In questo caso, gli invieremo una stringa di caratteri che sarà la stringa jSON di tipo [AleasResponse];
  • riga 29: il codice URL elaborato ha la forma /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, dove {x} rappresenta una variabile. Queste diverse 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 dei parametri del metodo potrebbe non riuscire. In tal caso, Spring MVC genera un'eccezione. Riassumiamo: se con un browser richiedo URL /100/200/10/20/300/400, 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: si richiede al livello [métier] un elenco di numeri casuali. Si ricorda che il metodo [metier].getAleas può generare un'eccezione;
  • righe 42-43: caso di errore;
  • riga 46: la risposta di tipo [AleasResponse] viene restituita sotto forma di stringa jSON;

9.2.5. Configurazione del progetto Spring

  

Esistono diversi modi per configurare Spring:

  • con file XML;
  • con codice Java;
  • con una combinazione dei due;

Abbiamo scelto di configurare la nostra applicazione web con codice Java. È la classe [Config] sopra riportata che garantisce 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 {
  // -------------------------------- configurazione del livello [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: si indica a Spring in quali pacchetti troverà gli oggetti da istanziare. Ne troverà due:
    • la classe [Metier] annotata da [@Service];
    • la classe [AleasController] annotata da [@Controller];
  • riga 16: l'annotazione [@EnableWebMvc] genera configurazioni automatiche per il framework Spring MVC;
  • righe 19-20: iniezione del contesto Spring (contenitore degli oggetti Spring). Questa iniezione è necessaria poiché l'oggetto delle righe 22-26 ne ha bisogno;
  • 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 verso il controller e il metodo corretti. [DispatcherServlet] è una classe di Spring;
  • righe 28-31: si specifica che questa servlet gestisce tutte le richieste URL;
  • righe 33-36: è la presenza di questo bean che attiverà il server Tomcat presente negli archivi del progetto. Attenderà le richieste sulla porta 8080;
  • righe 39-42: un mappatore jSON. È proprio questo che è stato iniettato negli oggetti Spring [Metier] e [AleasController];

9.2.6. Esecuzione del server web

  

Il progetto viene eseguito a partire 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) {
    // esecuzione dell'applicazione
    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, la classe [Config] che abbiamo appena descritto. Il secondo parametro è l'array di argomenti passato al metodo [main] (riga 7). In questo caso, non ci saranno argomenti;

Per l’esecuzione vera e propria, si rimanda il lettore al paragrafo 9.2.1.

9.3. Il client Android

Nota: il progetto Android che segue è piuttosto complesso. Richiede una buona conoscenza di Android, che si può trovare ad esempio in [Introduction à la programmation de tablettes Android avec Android Studio ].

Attività

Viste

Layer

[DAO]

Utente

Server

Il client avrà due componenti:

  1. un livello [Présentation] (viste + attività);
  2. un livello [DAO] che si rivolge al servizio [web / JSON] che abbiamo esaminato 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 estende RxJava all’ambiente Android. Come è stato fatto per l’applicazione Swing, utilizzeremo solo un’unica estensione fornita da RxAndroid, ovvero quella dello scheduler [AndroidSchedulers.mainThread()]. Un’interfaccia grafica Android obbedisce alle stesse regole di un’interfaccia Swing:

  • gli eventi vengono gestiti in un unico thread chiamato «event loop» o thread dell’interfaccia utente;
  • quando un evento avvia azioni asincrone, i risultati di queste devono essere recuperati nel thread dell'interfaccia utente se devono servire ad aggiornare l'interfaccia utente;

Il client Android:

  • invierà diverse richieste asincrone al server dei numeri casuali. Queste richieste verranno eseguite sul lato client utilizzando i thread dello scheduler [Schedulers.io()];
  • queste richieste asincrone restituiranno degli osservabili che verranno uniti in un unico osservabile (merge);
  • questo osservabile verrà monitorato sul lato client nello scheduler [AndroidSchedulers.mainThread()], avviato da RxAndroid;

9.3.2. Il progetto IntelliJ IDEA

Il progetto Android si chiama [client]:

Lo si eseguirà tramite [2].

Nota: l'esecuzione dipende in larga misura dalla configurazione di IntelliJ IDEA utilizzata. È probabile che l'esecuzione di [2] sopra indicata non funzioni al primo tentativo su un computer diverso dal mio. Configurare correttamente IntelliJ IDEA per eseguire questo progetto può rivelarsi 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 i SDK Android presenti sul mio computer. Da notare che il JDK 1.8 non è indispensabile. Android non supporta alcune funzionalità di Java 8, tra cui i lambda. Pertanto, per istanziare interfacce funzionali, utilizzeremo classi anonime. È quindi sufficiente un JDK 1.6. Tuttavia, il progetto così come viene 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 {
    // sostituire con la versione corrente del plugin Android
    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 dei file SDK Android presenti, potrebbe essere necessario modificare le versioni alle righe 8, 24-25 e 29.

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

Il progetto è stato configurato per:

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

Infine, controllate il percorso di SDK Android nel file [local.properties] [4], riga 11 qui sotto:


## Questo file è generato automaticamente da Android Studio.
# Non modificare questo file -- YOUR CHANGES WILL BE ERASED!
#
# Questo file deve *NOT* essere archiviato nei sistemi di controllo delle versioni,
# poiché contiene informazioni specifiche relative alla configurazione locale.
#
# Percorso del file SDK. Viene utilizzato solo da Gradle.
# Per la personalizzazione quando si utilizza un sistema di controllo delle versioni, si prega di leggere la
# nota nell'intestazione.
#Giovedì 7 aprile 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Esecuzione del progetto in IntelliJ IDEA

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

  • in [1], si avvia l'emulatore Android Genymotion;
  • in [2], si esegue 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], si richiede che, al momento dell'esecuzione, IDE proponga un dispositivo di esecuzione. In questo caso sarà sempre l'emulatore Genymotion;
  • in [5], si specifica di mantenere questo dispositivo per tutte le esecuzioni della configurazione;

L'esecuzione del progetto sull'emulatore Genymotion inizia con la seguente riga iniziale:

Image

Per sapere cosa inserire in [1], aprite una finestra di comando DOS e digitate 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


Digitate in [1] uno degli indirizzi IP del vostro computer (righe 20, 28, 32). Se disponete di un firewall di Windows, probabilmente dovrete disattivarlo affinché l’emulatore Android possa raggiungere il server dei numeri casuali.

L'esecuzione delle richieste asincrone con le informazioni sopra riportate fornisce i seguenti risultati:

Image

Ogni richiesta genera una risposta jSON con i seguenti campi:

  • aleas: i numeri casuali generati dal server;
  • idClient: il numero della richiesta;
  • on: il thread di esecuzione della richiesta sul lato client;
  • requestAt: ora della richiesta;
  • responseAt: ora di ricezione della risposta;
  • delay: il tempo di attesa osservato dal server prima di inviare la risposta;
  • errore: un codice di errore - 0 se non ci sono errori;
  • messaggio: un messaggio di errore - null se non ci sono errori;
  • observedAt: ora di osservazione della risposta;
  • observedOn: thread in cui è stata osservata la risposta. In questo caso sarà sempre [main], che indica il thread dell’interfaccia utente;

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

9.3.4. Le dipendenze Gradle del progetto

Il progetto necessita di alcune dipendenze che inseriamo 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 delle righe 2-3 sono dipendenze standard di un progetto Android con il SDK 23;
  • la dipendenza della riga 5 introduce l'oggetto Spring [RestTemplate] che gestisce la comunicazione tra il livello [DAO] e il server;
  • la dipendenza della riga 6 include la libreria JSON e [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: gli accessi a Internet devono essere autorizzati;

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 {

  // numeri casuali nell’intervallo [a,b]
  // vengono generati n numeri, dove n è a sua volta un numero casuale nell'intervallo [minCount, maxCount]
  // i numeri vengono generati dopo un ritardo di delay millisecondi,
  // dove [delay] è a sua volta un numero casuale nell'intervallo [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL del servizio web
  public void setUrlServiceWebJson(String url);

  // tempo di attesa massimo (ms) della risposta del server a una richiesta di connessione
  // tempo di attesa massimo (ms) della risposta del server a una richiesta
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • riga 12: il metodo del livello [DAO] che genera i numeri casuali in modo asincrono;
  • riga 15: per indicare all’implementazione [DAO] il servizio di generazione di numeri casuali URL;
  • riga 19: per impostare nell'implementazione [DAO] i tempi di attesa massimi, al fine di evitare tempi di attesa eccessivamente lunghi quando il server non risponde;

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


package android.aleas.fragments;

public class Request {

  // n. richiesta
  int id;
  // dati inseriti dall'utente
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // costruttori
  public Request() {

  }

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

  // getter e setter
...
}

Qui si riconoscono la maggior parte dei parametri dell’oggetto URL del server da interrogare.

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 {

  // codice di errore
  private int erreur;
  // messaggio di errore
  private String message;
  // tempo di attesa del server
  private int delay;
  // numeri casuali generati dal server
  private List<Integer> aleas;
  // stato del client
  private ClientState clientState;

  // costruttori

  public AleasDaoResponse() {
  }

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

  // getter e setter
...
}

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 {

  // nome del thread di esecuzione
  private String on;
  // ora della richiesta
  private String requestAt;
  // ora della risposta
  private String responseAt;
  // ID del cliente
  private int idClient;

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

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

  // metodi privati

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

  // getter e setter
...
}
  • riga 11: thread di esecuzione del livello [DAO];
  • riga 13: ora della richiesta;
  • riga 15: ora della 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 con la 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 {

  // cliente REST
  private RestTemplate restTemplate;
  // URL servizio
  private String urlServiceWebJson;

  // mappatore jSON
  private ObjectMapper mapper;

  // costruttori
  public Dao() {
    // mappatore jSON
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // si imposta il URL del servizio REST
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • riga 22: l'oggetto [RestTemplate] che garantirà il dialogo con il server dei numeri casuali;
  • riga 24: l'oggetto URL del servizio di generazione, impostato dal metodo [setUrlServiceWebJson] alla riga 41;
  • riga 27: il mappatore jSON che servirà a 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:


  // cliente REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // si imposta il timeout delle richieste del client REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • il dialogo tra il client e il server web / JSON è gestito dall’oggetto [RestTemplate] della riga 2. Per il momento non lo abbiamo inizializzato. È il metodo [setClientTimeouts] a occuparsene;
  • 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: creiamo l’oggetto di tipo [RestTemplate] che fungerà da supporto per la comunicazione con il servizio web. Gli passiamo come parametro l’oggetto [factory] appena creato;
  • riga 12: il dialogo client/server può assumere diverse forme. Gli scambi avvengono tramite righe di testo e dobbiamo indicare all’oggetto di tipo [RestTemplate] cosa deve fare con quella riga di testo. A tal fine, gli forniamo dei convertitori, ovvero classi in grado di elaborare le righe di testo. La scelta del convertitore avviene in genere tramite le intestazioni HTTP che accompagnano la riga di testo. In base a tali intestazioni, l’oggetto [RestTemplate] sceglierà, tra i propri convertitori, quello più adatto alla situazione. In questo caso, avremo un unico convertitore, un convertitore String --> String, il che fa sì che il tipo String ricevuto dal server non subisca 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()));
    // esecuzione del servizio
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL del servizio: /{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());
          // informazioni sul cliente
          ClientState clientState = new ClientState(request.getId());
          // richiesta HTTP sincrona
          String response = executeRestService("get", urlService, null);
          // deserializzazione della risposta jSON dal server
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // errore?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // si inoltra l'eccezione
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // si registra l'ora di ricezione
            clientState.setResponseAt();
            // si inoltra il risultato all'abbonato
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // si inoltra l'eccezione all'abbonato
          subscriber.onError(ex);
        } finally {
          // si segnala la fine dell'osservabile
          // durante l'esecuzione, si nota che questo metodo non ha alcun effetto se il metodo [onError] è stato chiamato     in precedenza - come previsto dalla teoria - si potrebbe quindi inserire questa istruzione solo nel blocco try
          subscriber.onCompleted();
        }
      }
    });
  }
  • riga 2: è importante ricordare che si deve generare un tipo [Observable<AleasResponse>];
  • riga 3: una riga di log sulla console Android;
  • riga 5: l'oggetto [RestTemplate] garantisce un dialogo sincrono con il server. Ciò significa che il thread di esecuzione che effettua la richiesta rimane bloccato fino alla ricezione della risposta. Nell’esempio Swing abbiamo visto come trasformare un’azione sincrona in un’azione asincrona grazie al metodo [Observable.create]. È proprio questa la strada che seguiamo qui;
  • riga 7: il metodo [call] dell'interfaccia [Observable.OnSubscribe<AleasDaoResponse>] della riga 5. È questo metodo che viene chiamato quando un osservatore si abbona all'osservabile;
  • righe 10-12: creazione dell'oggetto URL del servizio dei numeri casuali;
  • riga 14: inizializzazione dell'oggetto [ClientState]. In questo caso si tratta di registrare l'ora della richiesta;
  • riga 16: richiesta sincrona HTTP. Si ottiene 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 di 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 {

  // codice di errore
  private int erreur;
  // messaggio di errore
  private String message;
  // tempo di attesa del server
  private int delay;
  // numeri casuali
  private List<Integer> aleas;

  // getter e setter
...
}
  • riga 23: si recupera il codice di errore inviato dal server;
  • righe 24-26: in caso di errore, si genera un'eccezione da inoltrare all'abbonato;
  • riga 29: si aggiorna [clientState], che farà parte della risposta inviata all’abbonato;
  • righe 31-33: invio della risposta all'abbonato. La risposta è del tipo [AleasDaoResponse];
  • righe 35-37: gestiscono tutti i casi di errore in modo indifferenziato. L'errore più probabile è un errore di rete;
  • riga 41: notifica di fine trasmissione;

9.3.7. Le viste dell'applicazione

  

L'applicazione presenta le due seguenti viste:

La vista della richiesta

Image

La vista della 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 {

  // ------------- dati comuni ai frammenti
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • riga 7: la classe [MyFragment] estende la classe Android [Fragment];
  • righe 10-11: i dati comuni a 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à richiesto di aggiornarsi con il contenuto della sessione. Questo metodo è dichiarato astratto poiché 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 {

  // attività dell'applicazione
  private MainActivity activity;
  // numero di richieste
  private int nbRequests;
  // caratteristiche delle richieste
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL servizio web / jSON
  private String urlWebJson;
  // operazione avviata
  private boolean onAir;
  // idem, ma un po' più tardi
  private boolean operationStarted;
  // il nome dell'esempio scelto dall'utente dall'elenco degli esempi
  private String exampleName;
  // il suo numero nell'elenco dei frammenti
  private int examplePosition;
  // l'adattatore dello spinner degli esempi nella vista della query
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

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

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

  // getter e setter
...
}

Il metodo alla riga 46 consente di creare l’oggetto [Request] che incapsula tutte le informazioni fornite dall’utente nella vista di query:

  

package android.aleas.fragments;

public class Request {

  // numero della query
  int id;
  // dati inseriti dall'utente
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // costruttori
  public Request() {

  }

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

  // getter e setter
....
}

9.3.7.2. Il frammento [RequestFragment] della query

Il frammento della query presenta i seguenti componenti:

Image

L'applicazione presenta un'unica 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
EditText
edtNbRequests
numero di richieste da inviare al servizio di generazione di numeri casuali
4
EditText
edtA, edtB
i limiti [a,b] dell'intervallo di generazione dei numeri;
5
EditText
edtMinCount, edtMaxCount
il servizio genera count numeri dove count è un numero casuale nell'intervallo [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
il servizio attende delay millisecondi prima di generare i numeri, dove delay è un numero casuale compreso nell'intervallo [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
URL del servizio di generazione di numeri casuali;
8
Spinner
spinnerExemples
l'elenco a discesa degli esempi. Ogni esempio illustra un metodo specifico della classe [Observable];
8
Pulsante
btnExecuter
il pulsante che avvia 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, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. Il frammento [ResponseFragment] della risposta

Il frammento della risposta presenta i seguenti componenti:

Image

n.
Tipo
Nome
Ruolo
1
TextView
infoReponses
numero di risposte ricevute
2
ListView
listReponses
elenco dei canali jSON ricevuti dal server
3
Pulsante
btnAnnuler
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">

  <!-- barra delle applicazioni -->
  <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">

    <!-- barra degli strumenti -->
    <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">

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

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

  <!-- contenitore delle viste -->
  <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:

righe
Tipo
Nome
Ruolo
20-34
Barra degli strumenti
toolbar
barra degli strumenti dell'applicazione
29-34
ProgressBar
loadingPanel
immagine di caricamento visualizzata mentre la richiesta dell'utente è in esecuzione
37-40
TabLayout
schede
la barra delle schede dell'applicazione
44-51
MyPager
container
il contenitore in cui vengono visualizzati i diversi 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 {

  // controllo dello swipe
  private boolean isSwipeEnabled;

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

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

  // ridefinizione dei metodi
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // scorrimento consentito?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // scorrimento consentito?
    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]. Si utilizza la classe [MyPager] al posto della classe [ViewPager] solo perché si vuole disabilitare lo swipe: per impostazione predefinita, con la classe [ViewPager], è possibile passare da una scheda all'altra con uno swipe (scorrendo verso sinistra o destra). In questo caso, non si desidera questo comportamento;
  • riga 11: il valore booleano che controllerà lo swipe (righe 26 e 36);
  • righe 44-46: il metodo che permette di inizializzare il campo della 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 {

  // livello [DAO]
  private IDao dao;
  // la sessione
  private Session session;

  // costruttore
  public MainActivity() {
    // genitore
    super();
    // sessione
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getters

  public Session getSession() {
    return session;
  }

  // implementazione 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 Android [AppCompatActivity]. Si tratta quindi di un'attività Android standard;
  • 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] consente alle viste di non essere a conoscenza del livello [DAO]: i loro gestori di eventi si rivolgeranno al livello [activité] quando vorranno comunicare con il server.

  • riga 24: un riferimento al livello [DAO] inizializzato dal costruttore della riga 35;
  • riga 26: un riferimento alla sessione condivisa dai frammenti inizializzato dal costruttore della riga 33;
  • righe 46-59: implementazione dell’interfaccia [IDao];

La classe [MainActivity] inizializza i componenti della vista ad essa associata nel modo seguente:


  // barra degli strumenti
  private Toolbar toolbar;
  // gestore dei frammenti
  private MyPager mViewPager;
  // contenitore di schede
  private TabLayout tabLayout;
  // immagine di caricamento
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // classico
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // sessione
    session.setActivity(this);
    // configurazione dei timeout del livello [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

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

    // barra degli strumenti
    setSupportActionBar(toolbar);

    // all'inizio c'è una sola scheda
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gestore eventi
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // è stata selezionata una scheda - si modifica il frammento visualizzato dal contenitore dei frammenti
        int position = tab.getPosition();
        if (position == 0) {
          // scheda richiesta
          showView(0);
        } else {
          // scheda risposta - dipende dall'esempio scelto
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // creazione dei frammenti delle risposte
    createResponseFragments();

    // gestione dell'immagine di attesa
    loadingPanel.setVisibility(View.INVISIBLE);
}

Questo codice è piuttosto comune in un'attività. Chiariamo 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: si crea la prima scheda con il titolo [Request]. A un certo punto, avremo in memoria:
    • il frammento [Request];
    • n frammenti di tipo [ExampleXXFragment];

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

  • righe 37-48: il codice eseguito quando l'utente fa clic su una delle schede;
  • riga 43: viene visualizzato il frammento n. 0;
  • riga 46: viene visualizzato il frammento attualmente in uso (visualizzato). Il suo numero si trova nella sessione;
  • riga 62: si creano i frammenti di tutti gli esempi presenti nel selettore degli esempi nella vista [RequestFragment] (prima scheda);
  • riga 65: l'immagine di attesa è per il momento nascosta;

Per comprendere il metodo [showView] (righe 43, 46) e il metodo [createResponseFragments], occorre innanzitutto presentare il gestore dei frammenti in memoria (classe inclusa nel file Java di MainActivity):


  // il gestore dei frammenti - deve definire i metodi getItem, getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti gestiti
    private MyFragment[] fragments;

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

    // deve restituire il frammento n. posizione
    @Override
    public MyFragment getItem(int position) {
      // il frammento
      return fragments[position];
    }

    // restituisce il numero di frammenti da gestire
    @Override
    public int getCount() {
      // numero di frammenti
      return fragments.length;
    }
  }
}
  • la classe [SectionsPagerAdapter] estende la classe Android [FragmentPagerAdapter]. Ridefinisce 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], descritto al paragrafo 9.3.7.1;
  • riga 8: per essere creata, 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 degli esempi
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Specificare il layout da utilizzare quando viene visualizzato l'elenco delle opzioni
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // si inserisce l'adattatore nella sessione affinché la vista [Request] lo recuperi
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • riga 3: si crea un adattatore per lo spinner degli esempi, in questo caso un elenco di String che rappresenta i nomi degli esempi. Questi nomi sono presenti nel file [layout/exemples.xml]:
  

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


<!-- esempi -->
<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.exemples], [exemples] è il nome dell'array, riga 3 sopra, non il nome del file.

  • riga 5: si associa un layout (gestore di visualizzazione) all'adattatore. Ora l'adattatore dispone sia dei dati che della relativa modalità di visualizzazione;
  • riga 7: si avvia la sessione dell’adattatore. È qui che verrà recuperato dal frammento [RequestFragment] che ne ha bisogno;

Continuiamo con il codice del metodo [createResponseFragments]:


private void createResponseFragments() {
    // esempi di spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Specificare il layout da utilizzare quando viene visualizzato l'elenco delle opzioni
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // inserire l’adattatore nella sessione affinché la vista [Request] lo recuperi
    session.setSpinnerExemplesAdapter(adapter);
    // creazione della tabella dei frammenti (1 richiesta, n risposte)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // frammento della richiesta
    tFragments[0] = new RequestFragment();
    // frammenti delle risposte
    for (int i = 1; i < tFragments.length; i++) {
      // si costruisce il nome del frammento da istanziare corrispondente all'esempio scelto dall'utente
      // questo nome deve essere il nome completo con il relativo pacchetto - in questo caso è direttamente associato al numero dell'esempio nel selettore
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // si istanzia il frammento associato all'esempio
      MyFragment fragment;
      try {
        // istanziazione della classe
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // il frammento è stato creato - lo si inserisce nell'array
      tFragments[i] = fragment;
    }
    // istanziazione del gestore dei frammenti con questi nuovi frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Configurazione di ViewPager con l'adattatore delle sezioni.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navigazione tra le pagine - questa istruzione è importante
    // Qui si afferma che su entrambi i lati della vista visualizzata è necessario mantenere le viste inizializzate [tFragments.length]
    // ciò comporta che tutti i frammenti utilizzati dall’applicazione siano in memoria e inizializzati
    // se non si esegue questa operazione, per impostazione predefinita il valore di [OffscreenPageLimit] è pari a 1
    // quindi, se il frammento visualizzato è il n. 3, verranno inizializzati solo i frammenti 2 e 4
    // ciò avviene tramite la chiamata del metodo [onCreateView] di questi due frammenti; ciò significa che in questo metodo è necessario prevedere di
    // rigenerare l’aspetto visivo che il frammento aveva l’ultima volta che è stato utilizzato; inoltre, in questo metodo
    // non ci sia codice che non possa essere eseguito due volte: ciò crea un caos enorme ed è complesso da gestire
    // qui si è preferito evitare queste difficoltà - nei log si vede che all’avvio dell’applicazione vengono creati tutti i frammenti
    // e il loro metodo [onCreateView] viene eseguito – dopodiché non viene più eseguito –
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // si disabilita lo swipe tra i frammenti
    mViewPager.setSwipeEnabled(false);
  }
  • riga 9: creazione dell'array che conterrà tutti i frammenti dell'applicazione;
  • riga 11: il primo frammento è quello della richiesta;
  • righe 13-28: creeremo tanti frammenti quanti sono gli esempi. Questi frammenti estendono tutti il frammento della risposta [ResponseFragment] e implementano solo ciò che è specifico dell’esempio: la creazione dei valori osservati. Questi, infatti, differiscono da un esempio all’altro;
  • riga 16: il frammento di un esempio ha un nome standard: ExampleXXFragment, dove XX è la sua posizione nello spinner degli esempi aumentata di 1. XX è anche il numero del frammento dell’esempio nel gestore dei frammenti;
  • riga 21: istanziazione del frammento dell’esempio n. i dello spinner:
    • Class.forName(exampleName): carica il frammento in memoria;
    • Class.forName(exampleName).getConstructors()[0]: ottiene il riferimento al primo costruttore della classe. La classe ExampleXXFragment ha un solo costruttore. Verrà quindi ottenuto un riferimento a quest'ultimo;
    • 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 tale costruttore. Poiché il costruttore della classe ExampleXXFragment non richiede parametri, viene passato un array di oggetti vuoto;
  • riga 27: questo frammento viene aggiunto all'array dei frammenti;
  • riga 30: abbiamo visto che il costruttore del gestore di frammenti [SectionsPagerAdapter] richiedeva tra i suoi parametri l’array dei frammenti che doveva gestire. È ora che glielo passiamo;
  • riga 22: il contenitore di frammenti [mViewPager] della vista associata all’attività [MainActivity] viene qui associato al gestore di frammenti: il contenitore di frammenti [mViewPager] visualizza i frammenti del gestore di frammenti;
  • riga 43: si leggano i commenti: l’istruzione equivale a dire che tutti i frammenti devono rimanere nello stato in cui il codice li pone, indipendentemente dal frammento attualmente visualizzato. Pertanto, quando si ritorna a esso, lo si ritrova nello stato in cui è stato lasciato;
  • riga 45: il contenitore di frammenti [mViewPager] è di tipo [MyPager], che consente di inibire lo swipe;

Il metodo [MainActivity.showView] è il seguente:


  // visualizzazione vista n. [position]
  private void showView(int position) {
    // si aggiorna il frammento prima della sua visualizzazione
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // si visualizza la vista richiesta - si passa direttamente alla vista (secondo parametro impostato su false)
    // senza questo parametro, si passa per impostazione predefinita alla vista desiderata visualizzando rapidamente le viste intermedie - comportamento indesiderato
    mViewPager.setCurrentItem(position, false);
}
  • riga 3: si desidera visualizzare il frammento n. posizione;
  • riga 4: questo frammento viene richiesto al gestore dei frammenti e quindi aggiornato. Infatti, dall’ultima volta che è stato visualizzato, la sessione potrebbe essere cambiata. Il frammento deve quindi ispezionarla per verificare se deve aggiornarsi;
  • riga 7: il frammento viene visualizzato tramite [ViewPager]. Poiché quest’ultimo è stato associato al gestore dei frammenti, verrà visualizzato il frammento n. [position], ovvero quello appena aggiornato alla riga 4;

Concludiamo con i due metodi di gestione dell’attesa:


  public void beginWaiting() {
    // gestione dell'immagine di attesa
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // gestione dell'immagine di attesa
    loadingPanel.setVisibility(View.INVISIBLE);
    // fine esecuzione
    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 del servizio web
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // numero di richieste
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // intervallo di generazione
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // ritardo
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // numero di valori generati
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // pulsante
  private Button btnExecuter;
  // elenco delle risposte
  private ListView listReponses;
  private TextView infoReponses;
  // selettore degli esempi
  private Spinner spinnerExemples;

  // i dati inseriti
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // si recuperano l'attività e la sessione
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // si crea la vista del frammento a partire dalla sua definizione XML
    View view = inflater.inflate(R.layout.request, container, false);
    // componenti
    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);

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

    // Inizialmente nessun messaggio di errore
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner degli esempi
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // risultato
    return view;
  }
...
}
  • riga 16: la classe [RequestFragment] estende la classe [MyFragment] (cfr. paragrafo 9.3.7.1);
  • righe 18-42: i componenti visivi del frammento (cfr. paragrafo 9.3.7.2);
  • righe 45-52: i dati inseriti dall'utente nel modulo;
  • il costruttore (righe 55-58) e il metodo [onCreateView] vengono eseguiti quando l'attività [MainActivity] crea tutti i frammenti dell'applicazione. Si tratta dell'unica occasione;
  • riga 61: il codice del metodo [onCreateView] è standard. Si noti, alla riga 102, che l’adattatore dello spinner degli esempi viene acquisito nella sessione. Si noti inoltre, alla riga 91, che il clic sul pulsante [Exécuter] è gestito dal metodo [doExecuter];
  • righe 64-65: i campi [activity] e [session] appartengono alla classe padre [MyFragment];

Il metodo [doExecuter] è il seguente:


  // i dati inseriti
  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() {
    // dati inseriti validi?
    if (isPageValid()) {
      // si inseriscono le informazioni nella sessione
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // si memorizza l'URL del servizio web
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // azione in corso
      session.setOnAir(true);
      // ma non ancora avviata
      session.setOperationStarted(false);
      // viene visualizzato il frammento della risposta
      activity.selectTab(Constants.VUE_RESPONSE);
      // si avvia l'attesa
      beginWaiting();
    }
}
  • riga 15: non commenteremo il metodo [ispageValid]. Esso verifica la validità degli inserimenti e restituisce true solo se sono tutti validi. In tal caso, vengono utilizzati per inizializzare i campi delle righe 2-9;
  • riga 17: i vari dati inseriti vengono salvati nella sessione:
    • [spinnerExemples.getSelectedItem().toString()] è il nome dell'esempio selezionato dall'utente e viene memorizzato in [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] è il numero del frammento associato all’esempio e che è stato memorizzato (il frammento) dal gestore dei frammenti. Questo numero viene memorizzato in [session.examplePosition];
  • riga 19: il codice URL del servizio web / jSON viene trasmesso all'attività, che a sua volta lo trasmette al livello [DAO];
  • righe 21-24: si nota che sta per iniziare un'operazione;
  • riga 26: verrà visualizzata la scheda della risposta. Per comprendere cosa accadrà, occorre ricordare il codice [MainActivity.selectTab]:

  // selezione di una scheda
  public void selectTab(int position) {
    // ci sono al massimo 2 schede
    // inizialmente ce n'è solo una, quella della richiesta
    // se la scheda richiesta è la n. 1 e questa non esiste ancora, allora bisogna crearla
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 scheda in più
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // si seleziona la scheda tramite programma, il che attiverà l’evento [onTabSelected]
    // che assocerà la vista corretta a questa scheda
    tabLayout.getTabAt(position).select();
}
  • inizialmente, l'attività aveva creato solo la scheda della richiesta (scheda n. 0);
  • righe 6-11: si crea la scheda della risposta (scheda n. 1) se non era stata creata;
  • riga 14: si seleziona la scheda n. position (0 o 1). Ciò inserisce l'evento [onTabSelected] nella coda dell'event loop dell'applicazione Android;

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


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // è stata selezionata una scheda: si modifica il frammento visualizzato dal contenitore dei frammenti
        int position = tab.getPosition();
        if (position == 0) {
          // scheda richiesta
          showView(0);
        } else {
          // scheda risposta - dipende dall'esempio scelto
          showView(session.getExamplePosition());
        }
}

Nel caso della scheda [Response], viene eseguita la riga 9. Verrà visualizzato il frammento n. [session.getExamplePosition()]. Ad esempio, per l’esempio [exemple-03], il numero registrato in [session.examplePosition] è 3. La riga 10 visualizza quindi il frammento n. 3. La tabella dei frammenti inizialmente creata dall’attività è [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. È quindi proprio il frammento [Exemple03Fragment] che verrà visualizzato. Ciò avviene tramite il seguente codice:


  // visualizzazione della vista n. [position]
  private void showView(int position) {
    // si aggiorna il frammento prima della sua visualizzazione
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // si visualizza la vista richiesta - si passa direttamente alla vista (secondo parametro impostato su false)
    // senza questo parametro, si passa per impostazione predefinita alla vista desiderata visualizzando rapidamente le viste intermedie - comportamento indesiderato
    mViewPager.setCurrentItem(position, false);
}

Si nota 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 del 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 {

  // elenco delle risposte
  private ListView listReponses;
  private TextView infoReponses;
  // pulsante
  private Button btnAnnuler;

  // mappatore 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) {
    // si recuperano l'attività e la sessione
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // si crea la vista del frammento a partire dalla sua definizione XML
    View view = inflater.inflate(R.layout.response, container, false);
    // componenti
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // pulsante [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // risultato
    return view;
  }
...
  // metodo da eseguire (tramite codice esplicito) prima di ogni visualizzazione del frammento
  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 degli esempi da parte dell’attività. Infatti, tutti i frammenti degli esempi estendono il frammento [ResponseFragment]. Al momento della loro istanziazione, 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 una pila di eccezioni;
  • righe 38-59: il metodo [onCreateView] viene eseguito una sola volta, durante la creazione iniziale dei frammenti degli esempi da parte dell’attività. Qui si trova il codice classico di un’applicazione Android;
  • righe 52-56: il metodo eseguito quando si fa clic sul pulsante [Annuler] è il metodo [doAnnuler];
  • righe 62-64: il metodo [onRefresh] viene eseguito ogni volta che viene visualizzata la scheda [Response];

Grazie ai vari log inseriti nei metodi principali, è possibile vedere cosa succede all’avvio dell’applicazione:

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: costruzione del frammento [RequestFragment];
  • righe 2-9: costruzione dei frammenti dei 4 esempi dell'applicazione;
  • riga 10: inizializzazione del frammento [RequestFragment];
  • righe 11-14: inizializzazione dei frammenti dei 4 esempi dell'applicazione;

Successivamente, non si vedono più chiamate a questi metodi.

Il metodo [ResponseFragment.onRefresh] è il seguente:


  // metodo da eseguire (tramite codice esplicito) prima di ogni visualizzazione del frammento
  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()));
    // esecuzione in corso?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // richiesta di esecuzione
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • riga 5: si verifica se il frammento [RequestFragment] ha effettuato una richiesta (session.isOnAir) e se questa è stata avviata (isOperationStarted). Se il frammento [RequestFragment] ha effettuato una richiesta e questa non è già in esecuzione, l’operazione viene avviata (righe 7-8);
  • una volta avviata l’operazione, poiché questa è asincrona, l’utente può passare da una scheda all’altra. Se torna alla scheda [Response] e un’operazione è in corso, le righe 7-8 non vengono eseguite;

Il metodo [doExecuter] 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()));
    // inizio attesa
    beginWaiting();
    // preparazione all'esecuzione
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // si creano ed eseguono gli osservabili dell'esempio scelto
    createAndExecuteObservables();
}

// metodo implementato dalle classi figlie
protected abstract void createAndExecuteObservables();
  • riga 10: crea, esegue e osserva gli osservabili. Questi sono diversi per ogni esempio. Per questo motivo, il metodo [createAndExecuteObservables] è astratto (riga 14). Verrà implementato dai frammenti [ExampleXXFragment] che estendono la classe [ResponseFragment];
  • riga 6: l’elenco degli abbonamenti viene svuotato;
  • riga 7: l’elenco che mostra le risposte viene svuotato;
  • 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) {
    // un'ulteriore informazione
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 risposta in più
    reponses.add(0, data);
    Log.d("rxjava", data);
    // aggiornamento di UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • riga 1: si nota che l'elemento osservato arriva sotto forma di stringa. Si tratterà infatti della stringa jSON dell'elemento osservato. Ciò ci consente di disporre di un unico metodo per visualizzare l'elemento osservato, indipendentemente dal suo esatto tipo Java;
  • riga 6: l’elemento osservato [data] viene aggiunto in prima posizione nell’elenco delle risposte. L’utente vede quindi all’inizio dell’elenco le risposte più recenti;

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


  private void beginWaiting() {
    // si attiva la clessidra
    activity.beginWaiting();
    // viene visualizzato il pulsante [Annuler]
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // fine dell'attesa
    activity.cancelWaiting();
    // il pulsante [Annuler] viene nascosto
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Essi richiamano i metodi con lo stesso nome dell'attività e si limitano a mostrare/nascondere il pulsante [Annuler].

Il clic sul pulsante [Annuler] è gestito dal codice seguente:


  protected void doAnnuler() {
    // vengono annullati tutti gli abbonamenti
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fine dell'attesa
    cancelWaiting();
}
  • righe 3-7: si annullano uno per uno tutti gli abbonamenti;

9.3.8. Esempi di osservabili

9.3.8.1. Exemple-01

Le classi [ExampleXXFragment] hanno la funzione di creare, eseguire e osservare gli osservabili. La visualizzazione dei valori osservati è gestita 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 {

    // mappatori jSON
    private ObjectMapper mapperAleasUiResponse;

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

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // richiesta di numeri casuali
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // configurazione osservabile n. i
            // richiesta da inviare al server
            Request request = session.getRequest();
            request.setId(i);
            // osservabile eseguito sul thread di calcolo
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // osservazione sul thread dell'event loop;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // si eseguono tutti questi osservabili
        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) {
        // si estraggono le informazioni da visualizzare
        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 diversi osservabili che vengono fusi (riga 43) nell’osservabile della riga 36;
  • riga 43: l'osservabile viene eseguito in un thread dello scheduler [Schedulers.io()]. La chiamata HTTP al server verrà eseguita in questo thread;
  • riga 46: l'osservabile finale viene osservato sul thread dell'event loop;
  • righe 48-57: esecuzione degli osservabili, quindi delle richieste al server dei numeri casuali. Android non supporta ancora Java 8 e le sue lambda. Si utilizzano quindi qui 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 (cfr. paragrafo 9.3.6.1);
  • riga 51: chiamata del metodo [showAlea] della classe padre. Si ricorda che esso richiede una stringa di caratteri. Questa viene fornita dal metodo [getDataFrom] delle righe 59-68;
  • riga 63: si restituisce la stringa jSON del tipo [AleasUiResponse] come segue:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // risposta [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // thread di osservazione
  private String observedOn;
  // ora di osservazione
  private String observedAt;

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

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

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// getter e setter
...
}
  • alla risposta del livello [DAO] (riga 11) si aggiungono due informazioni:
    • riga 13: il thread di osservazione;
    • riga 15: l'ora di osservazione;

Torniamo al codice di sottoscrizione:


    @Override
    public void createAndExecuteObservables() {
...
        // si eseguono tutti questi osservabili
        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) {
                // viene visualizzata l'eccezione
                showAlea(getMessagesFromThrowable(th));
                // dopo aver ricevuto un'eccezione, l'osservabile non riceve né onNext, né onCompleted
                // è necessario annullare manualmente la sottoscrizione
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fine attesa
                cancelWaiting();
            }
        }));
}
  • righe 11-18: caso in cui l’osservatore riceva un’eccezione;
  • riga 14: si utilizza nuovamente il metodo [showAlea] della classe padre per visualizzare l'eccezione. Il metodo [getMessagesFromThrowable] è un metodo della classe padre [ResponseFragment] che, a partire da un'eccezione, genera una stringa di caratteri:

  // messaggi di un'eccezione
  protected String getMessagesFromThrowable(Throwable ex) {
    // si crea un elenco con i messaggi di errore della pila delle eccezioni
    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: si restituisce la stringa jSON da un elenco di messaggi di errore (riga 4);

Torniamo al codice di sottoscrizione all’osservabile:

  • righe 19-25: il codice eseguito quando l'osservatore riceve la notifica di fine trasmissione. A questo punto si annulla l'attesa (riga 23), il che aggiorna l'interfaccia grafica;

Il risultato dell’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 generato questo valore;
  • requestAt: ora della richiesta del client;
  • responseAt: ora della risposta del server;
  • delay: tempo di attesa rilevato dal server;
  • errore: codice di errore restituito dal server (0=nessun errore);
  • messaggio: messaggio di errore restituito dal server (null=nessun errore);
  • observedAt: ora di rilevamento del valore osservato;
  • observedOn: thread di rilevamento del valore rilevato;

9.3.8.2. Exemple-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 {

    // mappatori jSON
    private ObjectMapper mapperAleasUiResponse;

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

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // richiesta di numeri casuali
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // preparazione della richiesta
            Request request = session.getRequest();
            request.setId(i);
            // si mantengono solo gli osservabili con un numero di cliente pari
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // esecuzione sul thread di I/O
                            .subscribeOn(Schedulers.io()));
        }
        // osservazione sul thread dell'event loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // si eseguono questi osservabili
        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() {
                // fine attesa
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // si estrae l'informazione da visualizzare
        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 è analogo al precedente (riga 38). Tuttavia, tra gli osservabili ottenuti nell’esempio precedente, vengono mantenuti solo quelli con un numero cliente pari (righe 42-46), grazie al metodo [filter] (riga 41).

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

Image

9.3.8.3. Exemple-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 {

  // mappatori jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // vengono richiesti i numeri casuali
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparazione della richiesta
      Request request = session.getRequest();
      request.setId(i);
      // configurazione dell'osservabile
      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();
        }
      })
        // esecuzione sul thread di I/O
        .subscribeOn(Schedulers.io()));
    }
    // osservazione sul thread dell'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // si eseguono questi osservabili
    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() {
            // fine attesa
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // si estrae l'informazione da visualizzare
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Questo esempio è analogo all'Esempio-02:

  • riga 40: si definiscono gli stessi osservabili dell’Esempio-02;
  • riga 45: ciascuno dei valori emessi dalle osservabili precedenti viene trasformato, tramite il metodo [map], in un tipo List<Integer>, ovvero l'elenco dei numeri casuali generati dal server;
  • riga 58: ora il valore osservato è di tipo List<Integer>;

Il risultato ottenuto per 10 richieste è il seguente:

Image

9.3.8.4. Exemple-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 {

  // mappatori jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // si richiedono i numeri casuali
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparazione della richiesta
      Request request = session.getRequest();
      request.setId(i);
      // configurazione degli osservabili
      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());
        }
      })
        // esecuzione su un thread di I/O
        .subscribeOn(Schedulers.io()));
    }
    // osservazione su un thread dell'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // si eseguono questi osservabili
    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() {
            // fine attesa
            cancelWaiting();
          }
        }
      ));

  }
}

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

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

Per 10 richieste, si ottengono i seguenti risultati:

Image

In questo caso, si hanno più valori osservati rispetto alle richieste.

9.3.8.5. Exemple-05

Illustriamo ora la procedura da seguire per aggiungere un nuovo esempio di osservabili all’applicazione.

Supponiamo di voler riprodurre l’esempio [Exemple22h] del paragrafo 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 {
        // processo
        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()));
        // sottoscrizioni
        ProcessUtils.subscribe(1, process);
    }
}
  • i valori dell’osservabile [Observable.range(1, 10)] vengono prima raggruppati in valori pari e dispari tramite il metodo [groupBy] (riga 11), quindi riuniti in un unico osservabile tramite il metodo [concatMap] (riga 12);

fase 1

Si crea un nuovo esempio nel file [exemples.xml]:

  

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

Nella figura sopra è stata aggiunta la riga 8. Il nome assegnato all'esempio può essere qualsiasi.

Fase 2

Si duplica la classe [Example04Fragment] in [Example05Fragment]. In questo caso il nome è predefinito.

Fase 3

Si modifica il codice di [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 {

  // mappatori jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // istanze delle interfacce funzionali
    // filtro
    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();
      }
    };
    // si richiedono i numeri casuali
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparazione della richiesta
      Request request = session.getRequest();
      request.setId(i);
      // configurazione dell'osservabile
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // esecuzione su un thread di I/O
        .subscribeOn(Schedulers.io());
    }
    // osservazione su un thread dell'event loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // si eseguono questi osservabili
    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() {
            // fine attesa
            cancelWaiting();
          }
        }
      ));

  }
}
  • riga 67: rappresenta l'osservabile dell'esempio 04: un flusso di numeri interi;
  • riga 68: raggruppiamo questo flusso di interi in base a un criterio booleano che definiremo. Otterremo un osservabile di tipo Observable<GroupedObservable<Boolean, Integer>> che emette quindi elementi di tipo GroupedObservable<Boolean, Integer>;
  • riga 68: il metodo [concatMap] produrrà elementi di tipo Integer a partire dagli elementi di tipo GroupedObservable<Boolean, Integer>;
  • righe 32-59: per rendere più leggibile la creazione dell’osservabile alle righe 67-69, abbiamo isolato le istanze delle interfacce funzionali necessarie ai diversi 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. A partire dall'elemento T, l'istanza Func1<T,K> ha il compito di generare la chiave di raggruppamento K dell'elemento;
  • righe 48-51: gli elementi di tipo Integer saranno raggruppati in base alla parità. L'istanza Func1<Integer,Boolean> genera la chiave true o false a seconda che l'elemento debba essere inserito in un gruppo o nell'altro. In uscita si ottengono 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 sarà in questo caso il tipo emesso dall’operatore [groupBy], ovvero un tipo GroupedObservable<Boolean, Integer>;
  • riga 57: dall’elemento di tipo [GroupedObservable<Boolean, Integer>] si genera 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. Si devono quindi osservare due gruppi distinti: i numeri casuali pari e gli altri dispari.

Fase 4

Si esegue l’applicazione:

Image

e si ottengono 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 diversi valori per i campi del modulo che configurano le richieste inviate al server dei numeri casuali.

9.3.9. Conclusione

Abbiamo creato nell'ambiente Android la seguente architettura:

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] effettuava n richieste HTTP al server dei numeri casuali e il livello [swing] attendeva in modo asincrono i risultati di tali richieste per visualizzarli. Queste n richieste HTTP venivano inviate allo stesso server, che forniva lo stesso tipo di risposte. Ciò ci ha permesso di unire (mergeWith) le risposte in un unico osservabile.

In realtà, le applicazioni Android si rivolgono a server diversi e probabilmente non si procederà alla fusione delle loro risposte. Le richieste HTTP a questi server saranno gestite indipendentemente l’una dall’altra e i loro risultati saranno osservati tramite metodi separati.