Skip to content

9. RxJava en el entorno Android

9.1. Introduction

Aquí retomaremos una aplicación ya tratada en varios documentos:

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

En ella se trata una aplicación cliente/servidor en la que el servidor proporciona de forma asíncrona números aleatorios que el cliente Android muestra:

  • en el documento 1, el cliente Android utiliza una tecnología no estándar;
  • en el documento 2, el cliente Android utiliza la tecnología estándar de Android para las operaciones asíncronas;
  • en el documento 3, el cliente Android utiliza la misma tecnología que en el documento 2, pero simplificada mediante el uso de las anotaciones de la biblioteca Android Annotations;

El cliente Android es el siguiente:

La capa [DAO] se comunica con el servidor que genera los números aleatorios mostrados por la tableta Android. Este servidor tiene la siguiente arquitectura de dos capas:

Los clients consultan a ciertos URL de la capa [web / JSON] y reciben una respuesta de texto en formato JSON (JavaScript Object Notation).

Vamos a dividir el análisis de la aplicación en dos etapas:

El servidor web / jSON

  • su capa [métier];
  • su servicio [web / JSON] implementado con Spring MVC;

El cliente Android

  • su capa [DAO];
  • su actividad;
  • sus vistas;

9.2. El servicio web / jSON

Nota: el servicio web / jSON está implementado mediante la tecnología Spring MVC. El lector que no esté familiarizado con esta tecnología puede:

  • limitarse a leer el apartado 9.2.1, que explica cómo iniciar el servidor y cómo realizar consultas;
  • consultar el documento [Spring MVC et Thymeleaf par l'exemple], en particular el capítulo 4, que presenta las principales anotaciones utilizadas en el código;

9.2.1. El proyecto Intellij Idea

El servicio web / jSON tiene la siguiente arquitectura:

Esta arquitectura se implementa mediante el proyecto Intellij Idea según [1]:

El servidor se inicia mediante [2-3]. A continuación, se muestran los registros en la consola:

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
  • línea 12: indica que el servicio está disponible en el puerto 8080;
  • línea 10: el único URL del servicio web / jSON disponible a través de una operación HTTP GET. Sus parámetros son los siguientes:
    • [a,b]: intervalo de generación de números aleatorios;
    • [minCount, maxCount]: se generan count números aleatorios, donde count es un número aleatorio en el intervalo [minCount, maxCount];
    • [minDelay, maxDelay]: el servicio espera delay milisegundos antes de devolver los números solicitados, donde delay es un número aleatorio en [minDelay, maxDelay];

En un navegador, solicitemos este URL:

 

Se han solicitado:

  • números aleatorios en el intervalo [100, 200];
  • n números aleatorios con n en el intervalo [10, 20];
  • un tiempo de espera de x milisegundos con x en el intervalo [300, 400];

En la respuesta:

  • aleas: lista de números aleatorios generados;
  • delay: el tiempo de espera en milisegundos que el servidor ha retenido;
  • error: un código de error - 0 si no hay error;
  • mensaje: un mensaje de error - null si no hay error;

9.2.2. Las dependencias Gradle del proyecto

  

El proyecto [serveur] es un proyecto Gradle configurado por el siguiente archivo [build.gradle] [1]:


// generado por http://start.spring.io/ (mayo de 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')
}
  • línea 1: un comentario que indica cómo se generó este archivo de configuración;
  • líneas 4 y 10: una dependencia del marco [Spring Boot], una rama del ecosistema Spring. Este marco [http://projects.spring.io/spring-boot/] permite una configuración mínima de Spring. Según los archivos presentes en la ruta de clases del proyecto, [Spring Boot] deduce una configuración plausible o probable para este. Así, si Hibernate se encuentra en el Classpath del proyecto, [Spring Boot] deducirá que la implementación JPA utilizada será Hibernate y configurará Spring en consecuencia. El desarrollador ya no tiene que hacerlo. Solo le queda entonces realizar las configuraciones que [Spring Boot] no ha hecho por defecto o aquellas que [Spring Boot] ha hecho por defecto pero que deben especificarse. En cualquier caso, es la configuración realizada por el desarrollador la que tiene la última palabra;
  • líneas 14-15: dos plugins de Gradle necesarios para utilizar el contenido de este archivo Gradle;
  • líneas 17-20: definen las características del archivo generado para este proyecto;
  • líneas 22-23: para garantizar la compatibilidad con Java 8;
  • líneas 25-27: las dependencias se buscarán en el repositorio global de Maven o en el repositorio local del equipo;
  • línea 30: define una dependencia del artefacto [spring-boot-starter-web]. Este artefacto incluye todos los archivos necesarios para un proyecto Spring MVC. Entre ellos se encuentra el archivo de un servidor Tomcat. Este es el que se utilizará para implementar la aplicación web. Cabe señalar que no se ha mencionado el version de la dependencia. Se utilizará el mencionado en el proyecto importado [spring-boot];

Para actualizar el proyecto, hay que forzar la descarga de las dependencias [1-3]:

Veamos [4] las dependencias que aporta este archivo [build.gradle]:

 

Son muchas. Spring Boot para la web ha incluido las dependencias que probablemente necesitará una aplicación web Spring MVC. Esto significa que algunas pueden ser innecesarias. Spring Boot es ideal para un tutorial:

  • nos proporciona las dependencias que probablemente necesitaremos;
  • veremos que simplifica considerablemente la configuración del proyecto Spring MVC;
  • incluye un servidor Tomcat integrado [1], lo que nos evita tener que implementar la aplicación en un servidor web externo;
  • permite generar un ejecutable jar que incluye todas las dependencias mencionadas anteriormente. Este jar puede trasladarse de una plataforma a otra sin necesidad de reconfiguración.

Encontrará numerosos ejemplos que utilizan Spring Boot en el sitio web del ecosistema Spring [http://spring.io/guides]. Ahora que conocemos las dependencias del proyecto, podemos pasar al código.

9.2.3. La capa [métier]

  

La capa [métier] tendrá la siguiente interfaz [IMetier]:


package dvp.rxjava.server.metier;

public interface IMetier {
  // números aleatorios en el intervalo [a,b]
  // se generan n números, siendo n un número aleatorio en el intervalo [minCount, maxCount]
  // los números se generan tras una espera de delay milisegundos,
  // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Esta interfaz es prácticamente idéntica a la estudiada en el entorno Swing en el apartado 8.4. En la línea 8, el método [getAleas] devuelve el siguiente tipo [AleasMetier]:


package dvp.rxjava.server.metier;

import java.util.List;

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

  // constructores
  public AleasMetier(){

  }

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

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

  // getters y setters
...
}

El código de la clase [Metier] que implementa la interfaz [IMetier] es el siguiente:


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) {
    // números aleatorios en el intervalo [a,b]
    // se generan n números, siendo n un número aleatorio en el intervalo [minCount, maxCount]
    // los números se generan tras una espera de delay milisegundos,
    // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]

    // ¿algunas comprobaciones
    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;
    }
    // ¿errores?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // generador de números aleatorios
    Random random = new Random();
    // ¿espera?
    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);
      }
    }
    // generación del resultado
    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));
    }
    // devolución del resultado
    return new AleasMetier(delay,nombres);
  }

}

No comentamos la clase: es análoga a la que se encuentra en el entorno Swing en el apartado 8.4. Simplemente señalaremos los siguientes puntos:

  • línea 10: la anotación Spring [@Service], que hará que Spring instancie la clase en un único ejemplar (singleton) y ponga su referencia a disposición de otros componentes de Spring. Aquí se podrían haber utilizado otras anotaciones de Spring para obtener el mismo efecto;
  • Líneas 13-14: se inyecta un mapeador jSON. Spring es un contenedor de objetos. Este contenedor se instancia al iniciar la aplicación web y, a continuación, se instancian los objetos definidos en un archivo de configuración, por defecto en un único ejemplar (singleton). Un singleton de Spring puede contener referencias a otros objetos de Spring. Este es el caso aquí: el singleton [metier] (líneas 10-11) tendrá una referencia al singleton [mapper] (líneas 13-14). A esto se le llama inyección de dependencias. Hay dos formas de inyectar un singleton en otro singleton:
    • por su tipo: esto es posible si el singleton que se va a inyectar es el único objeto Spring que tiene ese tipo. Este es el caso aquí para la inyección de las líneas 13-14 (tipo ObjectMapper);
    • por su nombre, si varios objetos Spring tienen el mismo tipo. En ese caso, hay que añadir la anotación @Qualifier(«nomDuSingleton») para especificar el nombre del singleton;

La clase [Metier] lanza excepciones de tipo [AleaException]:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // código de error
  private int code;

  // constructores
  public AleaException() {
  }

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

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

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

  // getters y setters

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • línea 3: [AleasException] extiende la clase [RuntimeException]. Por lo tanto, se trata de una excepción no controlada (no es obligatorio gestionarla con un try / catch);
  • línea 6: se añade un código de error a la clase [RuntimeException];

9.2.4. El servicio web / JSON

  

El servicio web / JSON está implementado por Spring MVC. Spring MVC implementa el modelo de arquitectura denominado MVC (Modelo – Vista – Controlador) de la siguiente manera:

El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:

  1. solicitud: las URL solicitadas tienen el formato http://máquina:puerto/contexto/Acción/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] es la clase de Spring que procesa los URL entrantes. Ella «enruta» el URL hacia la acción que debe procesarlo. Estas acciones son métodos de clases específicas denominadas [Contrôleurs]. La C de MVC es aquí la cadena [Dispatcher Servlet, Contrôleur, Action]. Si no se ha configurado ninguna acción para procesar el URL entrante, el servlet [Dispatcher Servlet] responderá que no se ha encontrado el URL solicitado (error 404 NOT FOUND);
  1. procesamiento
  • la acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden proceder de varias fuentes:
    • la ruta [/param1/param2/...] del URL,
    • de los parámetros [p1=v1&p2=v2] del URL,
    • de los parámetros enviados por el navegador con su solicitud;
  • en el procesamiento de la solicitud del usuario, la acción puede necesitar la capa [metier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
    • una página de error si la solicitud no se ha podido procesar correctamente
    • una página de confirmación en caso contrario
  • la acción solicita que se muestre una vista determinada [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Es la M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
  1. respuesta: la vista V seleccionada utiliza la plantilla M creada por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente y, a continuación, envía dicha respuesta.

Para un servicio web / JSON, la arquitectura anterior se modifica ligeramente:

  • en [4a], el modelo, que es una clase Java, se transforma en una cadena JSON mediante una biblioteca JSON;
  • en [4b], esta cadena JSON se envía al navegador;

Volvamos a la capa [web] de nuestra aplicación:

En nuestra aplicación, solo hay un controlador:

  

El servicio web / JSON enviará a su clients una respuesta de tipo [AleasResponse] como sigue:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // código de error
  private int erreur;
  // mensaje de error
  private String message;

  // fabricantes
  public AleasResponse() {

  }

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

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • línea 5: la clase [AleasResponse] extiende la clase [AleasMetier] y, por lo tanto, hereda todos sus atributos (aleas, delay);
  • línea 8: un código de error (0 si no hay error);
  • línea 10: si es erreur!=0, un mensaje de error; si no hay error, null;

El controlador [AleasController] es el siguiente:


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 {

    // capa de negocio
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // números aleatorios en [a,b]
    // se generan n números con n en el intervalo [minCount, maxCount]
    // los números se generan tras una espera de delay milisegundos,
    // donde [delay] es un número aleatorio en el intervalo [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 {

        // se prepara la respuesta
        AleasResponse response = new AleasResponse();
        // se utiliza la capa de negocio para generar los números aleatorios
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // caso de error (código y mensaje)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // se devuelve la respuesta jSON
        return mapper.writeValueAsString(response);
    }
}
  • línea 16: la anotación [@Controller] convierte la clase [AleasController] en un singleton de Spring. Además, indica que la clase contiene métodos que procesarán solicitudes para determinados URL de la aplicación web. Aquí solo hay una en la línea 29;
  • líneas 20-21: la anotación [@Autowired] le pide a Spring que inyecte en el campo un componente de tipo [IMetier]. Será la clase [Metier] anterior. Esto se debe a que le hemos aplicado la anotación [@Service], por lo que se gestiona como un componente de Spring;
  • líneas 22-23: la anotación [@Autowired] le pide a Spring que inyecte en el campo un componente de tipo [ObjectMapper]. Lo definiremos más adelante;
  • línea 31: el método [getAleas] genera los números aleatorios. Su nombre no tiene importancia. Cuando se ejecuta, los parámetros de las líneas 31-33 han sido inicializados por Spring MVC. Veremos cómo. Por otra parte, si se ejecuta, es porque el servidor web ha recibido una solicitud HTTP GET para el URL de la línea 29 (atributo method);
  • línea 30: la anotación [@ResponseBody] indica que el resultado del método debe enviarse tal cual al cliente. En este caso, le enviaremos una cadena de caracteres que será la cadena jSON de tipo [AleasResponse];
  • línea 29: el URL procesado tiene la forma /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, donde {x} representa una variable. Estas diferentes variables se asignan a los parámetros del método en las líneas 32-33. Esto se realiza mediante la anotación @PathVariable("x"). Cabe señalar que los valores {x} son componentes de un URL y, por lo tanto, son de tipo String. La conversión de String al tipo de los parámetros del método puede fallar. En ese caso, Spring MVC lanza una excepción. Resumiendo: si desde un navegador solicito el URL /100/200/10/20/300/400, el método getAleas de la línea 31 se ejecutará con los parámetros a=100 (línea 31), b=200 (línea 31), minCount=10 (línea 31), maxCount=20 (línea 32), minDelay=300 (línea 32), maxDelay=400 (línea 33);
  • línea 39: se solicita a la capa [métier] una lista de números aleatorios. Recordemos que el método [metier].getAleas puede lanzar una excepción;
  • líneas 42-43: caso de error;
  • línea 46: la respuesta de tipo [AleasResponse] se devuelve en forma de cadena jSON;

9.2.5. Configuración del proyecto Spring

  

Hay varias formas de configurar Spring:

  • con archivos XML;
  • con código Java;
  • con una combinación de ambos;

Hemos optado por configurar nuestra aplicación web con código Java. Es la clase [Config] anterior la que se encarga de esta configuración:


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 {
  // -------------------------------- configuración de la capa [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);
  }

  // mapeador jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • línea 15: le indicamos a Spring en qué paquetes encontrará los objetos que debe instanciar. Encontrará dos:
    • la clase [Metier] anotada por [@Service];
    • la clase [AleasController] anotada por [@Controller];
  • línea 16: la anotación [@EnableWebMvc] induce configuraciones automáticas para el framework Spring MVC;
  • líneas 19-20: inyección del contexto Spring (contenedor de objetos Spring). Esta inyección es necesaria porque el objeto de las líneas 22-26 la necesita;
  • el archivo de configuración de Spring puede definir nuevos objetos Spring mediante métodos anotados [@Bean]. El resultado del método se convierte entonces en un objeto Spring;
  • líneas 22-26: definición del servlet del framework Spring MVC, el que redirige las solicitudes HTTP al controlador y al método correctos. [DispatcherServlet] es una clase de Spring;
  • líneas 28-31: se indica que este servlet procesa todas las URL;
  • líneas 33-36: es la presencia de este bean lo que activará el servidor Tomcat presente en los archivos del proyecto. Esperará las solicitudes en el puerto 8080;
  • líneas 39-42: un mapeador jSON. Este es el que se ha inyectado en los objetos Spring [Metier] y [AleasController];

9.2.6. Ejecución del servidor web

  

El proyecto se ejecuta desde la siguiente clase ejecutable [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) {
    // ejecución de la aplicación
    SpringApplication.run(Config.class, args);
  }

}
  • línea 6: la clase [Application] es una clase ejecutable (líneas 7-10);
  • línea 9: el método estático [SpringApplication.run] es un método de [spring Boot] (línea 4) que iniciará la aplicación. Su primer parámetro es la clase Java que configura el proyecto. En este caso, la clase [Config] que acabamos de describir. El segundo parámetro es la matriz de argumentos pasada al método [main] (línea 7). En este caso, no habrá argumentos;

Para la ejecución propiamente dicha, se invita al lector a volver al apartado 9.2.1.

9.3. El cliente Android

Nota: el siguiente proyecto de Android es bastante complejo. Requiere buenos conocimientos de Android, que se pueden encontrar, por ejemplo, en [Introduction à la programmation de tablettes Android avec Android Studio ].

Actividad

Vistas

Capa

[DAO]

Usuario

Servidor

El cliente tendrá dos componentes:

  1. una capa [Présentation] (vistas + actividad);
  2. una capa [DAO] que se dirige al servicio [web / JSON] que hemos estudiado anteriormente.

9.3.1. RxAndroid

Para comunicarse de forma asíncrona con el servidor de números aleatorios, el cliente Android utilizará la biblioteca RxAndroid. Esta amplía RxJava al entorno Android. Al igual que se hizo para la aplicación Swing, solo utilizaremos una única extensión aportada por RxAndroid, la del programador [AndroidSchedulers.mainThread()]. Una interfaz gráfica de Android obedece a las mismas reglas que una interfaz Swing:

  • los eventos se procesan en un único hilo denominado «event loop» o «hilo del Ui»;
  • cuando un evento inicia acciones asíncronas, los resultados de estas deben recuperarse en el subproceso de Ui si se van a utilizar para actualizar Ui;

El cliente Android:

  • lanzará varias solicitudes asíncronas al servidor de números aleatorios. Estas solicitudes se ejecutarán en el lado del cliente con los subprocesos del programador [Schedulers.io()];
  • estas solicitudes asíncronas devolverán observables que se fusionarán en uno solo (merge);
  • este observable se observará en el lado del cliente en el programador [AndroidSchedulers.mainThread()], impulsado por RxAndroid;

9.3.2. El proyecto Intellij Idea

El proyecto de Android se llama [client]:

Se ejecutará mediante [2].

Nota: la ejecución depende en gran medida de la configuración de IDE, Intellij y Idea. Es probable que la ejecución de [2] anterior no funcione a la primera en un equipo distinto al mío. Configurar correctamente IDE, Intellij y Idea para ejecutar este proyecto puede resultar una tarea complicada para los principiantes. A continuación, se indican algunos puntos a tener en cuenta:

  • en [3], acceder a la estructura del proyecto;
  • en [4-5], el JDK y los SDK de Android presentes en mi equipo. Cabe señalar que el JDK 1.8 no es imprescindible. Android no es compatible con ciertas funcionalidades de Java 8, entre ellas las lambdas. Por lo tanto, para instanciar interfaces funcionales, utilizaremos clases anónimas. En ese caso, basta con un JDK 1.6. Sin embargo, el proyecto tal y como se distribuye ha sido configurado con un JDK 1.8;

El archivo [build.gradle] [6] que configura el proyecto Android es el siguiente:


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

Según los SDK Android presentes, es posible que haya que modificar las versiones de las líneas 8, 24-25 y 29.

Para instalar nuevos SDK de Android, utilice el SDK Manager de la siguiente manera [1]:

El proyecto se ha configurado para:

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

Por último, compruebe la ruta del SDK Android en el archivo [local.properties] [4], línea 11 a continuación:


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

9.3.3. Ejecución del proyecto Intellij Idea

Una vez creado el entorno adecuado para el proyecto, este se puede ejecutar de la siguiente manera:

  • en [1], se inicia el emulador de Android Genymotion;
  • en [2], se ejecuta la configuración de ejecución [app];
  • en [3], para crear una configuración de ejecución;
 
  • en [1, 3], la configuración se ha denominado [app];
  • en [2], corresponde a la ejecución del módulo denominado [app];
  • en [4], se solicita que, durante la ejecución, IDE nos proponga un dispositivo de ejecución. En este caso, siempre será el emulador Genymotion;
  • en [5], se indica que se mantenga este dispositivo para todas las ejecuciones de la configuración;

La ejecución del proyecto en el emulador Genymotion comienza con la siguiente bue inicial:

Image

Para saber qué introducir en [1], abra una ventana de comandos DOS y escriba el siguiente 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


Escriba en [1] una de las direcciones IP de su equipo (líneas 20, 28, 32). Si tiene un cortafuegos de Windows, probablemente tendrá que desactivarlo para que el emulador de Android pueda acceder al servidor de números aleatorios.

La ejecución de las consultas asíncronas con la información anterior da los siguientes resultados:

Image

Cada solicitud genera una respuesta jSON con los siguientes campos:

  • aleas: los números aleatorios generados por el servidor;
  • idClient: el número de la solicitud;
  • on: el hilo de ejecución de la solicitud en el lado del cliente;
  • requestAt: hora de la solicitud;
  • responseAt: hora de recepción de la respuesta;
  • delay: el tiempo de espera que el servidor ha observado antes de enviar su respuesta;
  • error: un código de error; 0 si no hay error;
  • mensaje: un mensaje de error; null si no hay error;
  • observedAt: hora de observación de la respuesta;
  • observedOn: subproceso de observación de la respuesta. Aquí siempre será [main], que designa el subproceso de Ui;

Dado que las solicitudes son asíncronas y que los tiempos de espera impuestos al servidor son aleatorios, las respuestas llegan en orden disperso.

9.3.4. Las dependencias Gradle del proyecto

El proyecto necesita dependencias que registramos en el archivo [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'
}
  • las dependencias de las líneas 2-3 son dependencias estándar de un proyecto Android con el SDK 23;
  • la dependencia de la línea 5 incluye el objeto Spring [RestTemplate] que gestiona la comunicación de la capa [DAO] con el servidor;
  • la dependencia de la línea 6 aporta la biblioteca JSON [Jackson] utilizada por la aplicación;
  • la dependencia de la línea 7 incluye la biblioteca RxAndroid (y con ella la biblioteca RxJava) que la capa Ui utiliza para comunicarse con la capa [DAO];

9.3.5. El manifiesto de la aplicación 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>
  • línea 5: se deben autorizar los accesos a Internet;

9.3.6. La capa [DAO]

 

9.3.6.1. La interfaz [IDao] de la capa [DAO]

La interfaz de la capa [DAO] será la siguiente:


package android.aleas.dao;

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

public interface IDao {

  // números aleatorios en el intervalo [a,b]
  // se generan n números, siendo n un número aleatorio en el intervalo [minCount, maxCount]
  // los números se generan tras una espera de delay milisegundos,
  // donde [delay] es a su vez un número aleatorio en el intervalo [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

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

  // tiempo de espera máximo (ms) de la respuesta del servidor a una solicitud de conexión
  // tiempo de espera (ms) máximo de la respuesta del servidor a una solicitud
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • línea 12: el método de la capa [DAO] que genera los números aleatorios de forma asíncrona;
  • línea 15: para indicar a la implementación [DAO] el URL del servicio de generación de números aleatorios;
  • línea 19: para establecer en la implementación [DAO] unos tiempos de espera máximos, con el fin de evitar un tiempo de espera excesivo cuando el servidor no responde;

El método [getAleas] recibe todos sus parámetros en el siguiente objeto [Request]:


package android.aleas.fragments;

public class Request {

  // n.º de solicitud
  int id;
  // entradas del usuario
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructores
  public Request() {

  }

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

  // getters y setters
...
}

Aquí se reconocen la mayoría de los parámetros del URL del servidor al que hay que consultar.

El método [getAleas] devuelve un tipo Observable<AleasDaoResponse>, donde la clase [AleasDaoResponse] es la siguiente:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // código de error
  private int erreur;
  // mensaje de error
  private String message;
  // tiempo de espera del servidor
  private int delay;
  // números aleatorios generados por el servidor
  private List<Integer> aleas;
  // estado del cliente
  private ClientState clientState;

  // constructores

  public AleasDaoResponse() {
  }

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

  // getters y setters
...
}

El tipo [ClientState] es el siguiente:


package android.aleas.dao;

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

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

public class ClientState {

  // nombre del hilo de ejecución
  private String on;
  // hora de la solicitud
  private String requestAt;
  // hora de la respuesta
  private String responseAt;
  // id del cliente
  private int idClient;

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

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

  // métodos privados

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

  // getters y setters
...
}
  • línea 11: subproceso de ejecución de la capa [DAO];
  • línea 13: hora de la solicitud;
  • línea 15: hora de la respuesta;
  • línea 17: n.º de la solicitud;

Los campos [on, requestAt, idClient] son inicializados por el cliente al inicio de la solicitud. El campo [responseAt] se inicializa cuando el cliente recibe la respuesta del servidor.

9.3.6.2. Implementación de la capa [DAO]

  

La interfaz [IDao] se implementa con la siguiente clase [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 servicio
  private String urlServiceWebJson;

  // mapeador jSON
  private ObjectMapper mapper;

  // constructores
  public Dao() {
    // mapeador jSON
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // se establece el URL del servicio REST
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • línea 22: el objeto [RestTemplate] que se encargará del diálogo con el servidor de números aleatorios;
  • línea 24: el URL del servicio de generación, que se establece mediante el método [setUrlServiceWebJson] de la línea 41;
  • línea 27: el mapeador jSON, que servirá para deserializar la cadena jSON enviada por el servidor de números aleatorios;
  • líneas 30-33: el constructor de la clase;
  • línea 32: se crea el mapeador jSON de la línea 27;

El método [setClientTimeouts] es el siguiente:


  // cliente REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // se establece el tiempo de espera de las solicitudes del cliente REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • el diálogo del cliente con el servidor web / JSON lo gestiona el objeto [RestTemplate] de la línea 2. Por el momento no lo hemos inicializado. Es el método [setClientTimeouts] el que lo hace;
  • línea 8: la clase [HttpComponentsClientHttpRequestFactory] es proporcionada por la dependencia [spring-android-rest-template]. Nos permitirá establecer los tiempos de espera máximos para la respuesta del servidor (líneas 9-10);
  • línea 11: creamos el objeto de tipo [RestTemplate], que servirá de soporte para la comunicación con el servicio web. Le pasamos como parámetro el objeto [factory] que acabamos de crear;
  • línea 12: el diálogo cliente/servidor puede adoptar diversas formas. Los intercambios se realizan mediante líneas de texto y debemos indicar al objeto de tipo [RestTemplate] qué debe hacer con esa línea de texto. Para ello, le proporcionamos convertidores, clases capaces de procesar las líneas de texto. La elección del convertidor se realiza generalmente a través de los encabezados HTTP que acompañan a la línea de texto. Según estos encabezados, el objeto [RestTemplate] elegirá, entre sus convertidores, el que mejor se adapte a la situación. En este caso, solo tendremos un único convertidor, un convertidor String --> String, lo que hace que el tipo String recibido del servidor no sufra ninguna transformación.

El método [getAleas] es el más complejo:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // ejecución del servicio
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL del servicio: /{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());
          // información del cliente
          ClientState clientState = new ClientState(request.getId());
          // solicitud http sincrónica
          String response = executeRestService("get", urlService, null);
          // deserialización de la respuesta jSON del servidor
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // ¿Error?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // se reenvía la excepción
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // se registra la hora de recepción
            clientState.setResponseAt();
            // se reenvía el resultado al suscriptor
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // se reenvía la excepción al suscriptor
          subscriber.onError(ex);
        } finally {
          // se notifica el fin del observable
          // al ejecutarlo, se observa que este método no tiene ningún efecto si se ha llamado previamente al método [onError]     de acuerdo con la teoría, por lo que se podría colocar esta instrucción únicamente en el try
          subscriber.onCompleted();
        }
      }
    });
  }
  • línea 2: hay que recordar que se debe generar un tipo [Observable<AleasResponse>];
  • línea 3: una línea de registro en la consola de Android;
  • línea 5: el objeto [RestTemplate] garantiza un diálogo síncrono con el servidor. Esto significa que el hilo de ejecución que realiza la solicitud queda bloqueado hasta recibir la respuesta. En el ejemplo de Swing, vimos cómo transformar una acción sincrónica en una asincrónica gracias al método [Observable.create]. Es este mismo camino el que seguimos aquí;
  • línea 7: el método [call] de la interfaz [Observable.OnSubscribe<AleasDaoResponse>] de la línea 5. Este es el método que se invoca cuando un observador se suscribe al observable;
  • líneas 10-12: construcción del URL del servicio de números aleatorios;
  • línea 14: inicialización del objeto [ClientState]. Aquí se trata de anotar la hora de la solicitud;
  • línea 16: solicitud HTTP síncrona. Se obtiene una respuesta jSON. El método [executeRestService] espera tres parámetros:
      1. el método HTTP que se va a utilizar para consultar el servicio;
      2. el URL del servicio;
      3. el objeto a enviar de tipo Object, null si el método HTTP no es POST;
  • 18-21: deserialización de la cadena jSON recibida en un tipo [AleasServerResponse]. Este tipo es el siguiente:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // código de error
  private int erreur;
  // mensaje de error
  private String message;
  // tiempo de espera del servidor
  private int delay;
  // números aleatorios
  private List<Integer> aleas;

  // getters y setters
...
}
  • línea 23: se recupera el código de error enviado por el servidor;
  • líneas 24-26: en caso de error, se envía una excepción al suscriptor;
  • línea 29: se actualiza [clientState], que formará parte de la respuesta enviada al suscriptor;
  • líneas 31-33: envío de la respuesta al suscriptor. Es de tipo [AleasDaoResponse];
  • líneas 35-37: tratan todos los casos de error de forma indiferenciada. El error más probable es un error de red;
  • línea 41: notificación de fin de transmisión;

9.3.7. Las vistas de la aplicación

  

La aplicación presenta las dos vistas siguientes:

La vista de la consulta

Image

La vista de la respuesta

Image

9.3.7.1. La clase [MyFragment]

Hay dos fragmentos:

  • [RequestFragment] para la consulta;
  • [ResponseFragment] para la respuesta;

Ambos fragmentos amplían la siguiente clase [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 {

  // ------------- datos comunes a los fragmentos
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • línea 7: la clase [MyFragment] amplía la clase Android [Fragment];
  • líneas 10-11: los datos comunes a todos los fragmentos;
  • línea 10: cada fragmento conoce la única actividad de la aplicación;
  • línea 11: para comunicarse entre sí, los fragmentos utilizan una sesión;
  • línea 13: antes de mostrar un fragmento, se le pedirá que se actualice con el contenido de la sesión. Este método se declara abstracto porque lo implementan las clases hijas. Por este motivo, la propia clase se declara abstracta (línea 7);

La clase [Session] contiene los datos que comparten los diferentes fragmentos de la aplicación. Su código es el siguiente:

  

package android.aleas.activity;

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

public class Session {

  // actividad de la aplicación
  private MainActivity activity;
  // número de solicitudes
  private int nbRequests;
  // características de las solicitudes
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL servicio web / jSON
  private String urlWebJson;
  // la operación ha comenzado
  private boolean onAir;
  // lo mismo, pero un poco más tarde
  private boolean operationStarted;
  // el nombre del ejemplo elegido por el usuario en la lista de ejemplos
  private String exampleName;
  // su n.º en la lista de fragmentos
  private int examplePosition;
  // el adaptador del spinner de ejemplos en la vista de la consulta
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

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

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

  // getters y setters
...
}

El método de la línea 46 permite crear el objeto [Request] que encapsula toda la información proporcionada por el usuario en la vista de consulta:

  

package android.aleas.fragments;

public class Request {

  // n.º de consulta
  int id;
  // entradas del usuario
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // constructores
  public Request() {

  }

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

  // getters y setters
....
}

9.3.7.2. El fragmento [RequestFragment] de la consulta

El fragmento de la consulta tiene los siguientes componentes:

Image

La aplicación tiene una vista única que consta de dos pestañas:

  • [1]: la pestaña de la solicitud;
  • [2]: la pestaña de la respuesta;

Los componentes del fragmento [RequestFragment] son los siguientes:

n.º
Tipo
Nombre
Función
3
EditText
edtNbRequests
número de solicitudes que se deben realizar al servicio de generación de números aleatorios
4
EditText
edtA, edtB
los límites [a,b] del intervalo de generación de números;
5
EditText
edtMinCount, edtMaxCount
el servicio genera count números donde count es un número aleatorio en el intervalo [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
el servicio espera delay milisegundos antes de generar los números donde delay es un número aleatorio en el intervalo [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
URL del servicio de generación de números aleatorios;
8
Spinner
spinnerExemples
la lista desplegable de ejemplos. Cada ejemplo ilustra un método concreto de la clase [Observable];
8
Botón
btnExecuter
el botón que inicia las llamadas al servicio de generación de números;

Se señalan los errores de introducción de datos:

Image

Los componentes del 1 al 6 son componentes [TextView] con los siguientes nombres (por orden): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. El fragmento [ResponseFragment] de la respuesta

El fragmento de la respuesta tiene los siguientes componentes:

Image

n.º
Tipo
Nombre
Función
1
TextView
infoReponses
Número de respuestas recibidas
2
ListView
listReponses
Lista de canales jSON recibidos del servidor
3
Botón
btnAnnuler
para cancelar las solicitudes al servidor

9.3.7.4. La actividad de Android [MainActivity]

  

La clase [MainActivity] muestra la siguiente 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 de aplicaciones -->
  <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 de herramientas -->
    <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">

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

    <!-- contenedor de pestañas -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- contenedor de vistas -->
  <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>

Los componentes de esta vista son los siguientes:

líneas
Tipo
Nombre
Función
20-34
Barra de herramientas
toolbar
barra de herramientas de la aplicación
29-34
ProgressBar
loadingPanel
imagen de espera que se muestra mientras se procesa la solicitud del usuario
37-40
TabLayout
pestañas
la barra de pestañas de la aplicación
44-51
MyPager
contenedor
el contenedor en el que se muestran los diferentes fragmentos de la aplicación

La clase [MyPager] es la siguiente:


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 {

  // control de deslizamiento
  private boolean isSwipeEnabled;

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

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

  // redefinición de métodos
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // ¿Deslizamiento permitido?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

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

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

}
  • la clase [MyPager] amplía la clase estándar de Android [ViewPager]. Se utiliza la clase [MyPager] en lugar de la clase [ViewPager] únicamente porque se quiere desactivar el deslizamiento: por defecto, con la clase [ViewPager], se puede pasar de una pestaña a otra con un deslizamiento (deslizando hacia la izquierda o hacia la derecha). Aquí no queremos este comportamiento;
  • línea 11: el valor booleano que controlará el deslizamiento (líneas 26 y 36);
  • líneas 44-46: el método que permite inicializar el campo de la línea 11;

El esqueleto de la actividad Android [MainActivity] es el siguiente:


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 {

  // capa [DAO]
  private IDao dao;
  // la sesión
  private Session session;

  // constructor
  public MainActivity() {
    // padre
    super();
    // sesión
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getter

  public Session getSession() {
    return session;
  }

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

}
  • línea 21: la clase [MainActivity] extiende la clase estándar de Android [AppCompatActivity]. Por lo tanto, se trata de una actividad estándar de Android;
  • línea 21: la clase [MainActivity] implementa la interfaz [IDao];

Volviendo a la arquitectura de la aplicación:

el hecho de que la actividad implemente la interfaz de la capa [DAO] permite que las vistas no tengan conocimiento de la capa [DAO]: sus gestores de eventos se dirigirán a la capa [activité] cuando quieran comunicarse con el servidor.

  • línea 24: una referencia a la capa [DAO] inicializada por el constructor de la línea 35;
  • línea 26: una referencia a la sesión compartida por los fragmentos, inicializada por el constructor de la línea 33;
  • líneas 46-59: implementación de la interfaz [IDao];

La clase [MainActivity] inicializa los componentes de la vista asociada a ella de la siguiente manera:


  // barra de herramientas
  private Toolbar toolbar;
  // gestor de fragmentos
  private MyPager mViewPager;
  // contenedor de pestañas
  private TabLayout tabLayout;
  // imagen de espera
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // clásico
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // sesión
    session.setActivity(this);
    // configuración de tiempos de espera de la capa [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

    // componentes
    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 de herramientas
    setSupportActionBar(toolbar);

    // al principio solo hay una pestaña
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gestor de eventos
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // se ha seleccionado una pestaña: se cambia el fragmento mostrado por el contenedor de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // pestaña de consulta
          showView(0);
        } else {
          // pestaña de respuesta: depende del ejemplo elegido
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // creación de fragmentos de respuestas
    createResponseFragments();

    // gestión de la imagen de espera
    loadingPanel.setVisibility(View.INVISIBLE);
}

Este código es bastante habitual en una actividad. Aclaremos algunos puntos:

  • la línea 19 hace referencia a la siguiente clase [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";
}
  • líneas 31-33: se crea la primera pestaña con el título [Request]. En un momento dado, tendremos en memoria:
    • el fragmento [Request];
    • n fragmentos de tipo [ExampleXXFragment];

La primera pestaña siempre mostrará el fragmento [Request]. La segunda pestaña mostrará el fragmento [ExampleXXFragment] correspondiente al ejemplo elegido por el usuario. Por lo tanto, el fragmento mostrado por la segunda pestaña cambia con el tiempo;

  • líneas 37-48: el código que se ejecuta cuando el usuario hace clic en una de las pestañas;
  • línea 43: se muestra el fragmento n.º 0;
  • línea 46: se muestra el fragmento que se está utilizando actualmente (visualizado). Su n.º se encuentra en la sesión;
  • línea 62: se crean los fragmentos de todos los ejemplos presentes en el selector de ejemplos de la vista [RequestFragment] (primera pestaña);
  • línea 65: la imagen de espera está oculta por el momento;

Para comprender el método [showView] (líneas 43, 46) y el método [createResponseFragments], primero debemos presentar el gestor de fragmentos en memoria (clase incluida en el archivo Java de MainActivity):


  // el gestor de fragmentos: debe definir los métodos getItem, getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmentos gestionados
    private MyFragment[] fragments;

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

    // debe devolver el fragmento n.º posición
    @Override
    public MyFragment getItem(int position) {
      // el fragmento
      return fragments[position];
    }

    // devuelve el número de fragmentos a gestionar
    @Override
    public int getCount() {
      // número de fragmentos
      return fragments.length;
    }
  }
}
  • la clase [SectionsPagerAdapter] extiende la clase Android [FragmentPagerAdapter]. Redefine dos métodos de su clase padre:
    • el método [getItem], línea 15;
    • el método [getCount], línea 22;
  • la clase [SectionsPagerAdapter] contiene todos los fragmentos de la aplicación. Estos se almacenan en la línea 5. Cabe señalar que son del tipo [MyFragment], presentado en el apartado 9.3.7.1;
  • línea 8: para construirse, la clase [SectionsPagerAdapter] recibe los fragmentos que debe gestionar;
  • líneas 14-18: el método [getItem] devuelve el fragmento en la posición [position];
  • líneas 21-25: el método [getCount] devuelve el número total de fragmentos;

El método [createResponseFragments] crea todos los fragmentos que necesita la aplicación:


private void createResponseFragments() {
    // rueda de ejemplos
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Especificar el diseño que se utilizará cuando aparezca la lista de opciones
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // se coloca el adaptador en la sesión para que la vista [Request] lo recupere
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • línea 3: se crea un adaptador para el spinner de los ejemplos, en este caso una lista de String que representa los nombres de los ejemplos. Estos nombres se encuentran en el archivo [layout/exemples.xml]:
  

El archivo [exemples.xml] contiene el siguiente código:


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

Línea 1: este archivo es el segundo parámetro del método [createFromResource]. En [R.array.exemples], [exemples] es el nombre de la tabla, línea 3 anterior, no el nombre del archivo.

  • Línea 5: se asocia un layout (gestor de visualización) al adaptador. Ahora el adaptador dispone tanto de los datos como de su modo de visualización;
  • línea 7: se pone el adaptador en sesión. Ahí es donde lo recuperará el fragmento [RequestFragment] que lo necesita;

Continuemos con el código del método [createResponseFragments]:


private void createResponseFragments() {
    // spinner de los ejemplos
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Especificar el diseño que se utilizará cuando aparezca la lista de opciones
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // se coloca el adaptador en la sesión para que la vista [Request] lo recupere
    session.setSpinnerExemplesAdapter(adapter);
    // creación de la tabla de fragmentos (1 consulta, n respuestas)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // fragmento de la consulta
    tFragments[0] = new RequestFragment();
    // fragmentos de las respuestas
    for (int i = 1; i < tFragments.length; i++) {
      // se construye el nombre del fragmento que se va a instanciar, correspondiente al ejemplo elegido por el usuario
      // este nombre debe ser el nombre completo con su paquete; aquí se asocia directamente al número del ejemplo en el spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // se instancia el fragmento asociado al ejemplo
      MyFragment fragment;
      try {
        // instanciación de la clase
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // el fragmento se ha creado; lo añadimos a la matriz
      tFragments[i] = fragment;
    }
    // instanciación del gestor de fragmentos con estos nuevos fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Configurar ViewPager con el adaptador de secciones.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navigation entre páginas: esta instrucción es importante
    // aquí se indica que a ambos lados de la vista mostrada se deben conservar las vistas inicializadas
    // esto implica aquí que  todos los fragmentos utilizados por la aplicación están en memoria e inicializados
    // si no se hace esto, entonces por defecto el [OffscreenPageLimit] es 1
    // por lo tanto, si el fragmento visualizado es el n.º 3, solo se inicializarán los fragmentos 2 y 4
    // esto se lleva a cabo mediante la llamada al método [onCreateView] de estos dos fragmentos; esto significa que, en este método, hay que prever
    // regenerar el aspecto visual que tenía el fragmento la última vez que se utilizó; además, no debe haber en este método
    // haya código que no admita ser ejecutado dos veces; esto genera un caos tremendo y es complejo de gestionar
    // aquí hemos preferido evitar estas dificultades; en los registros se ve que, al iniciar la aplicación, se crean todos los fragmentos
    // y se ejecuta su método [onCreateView]; después, nunca más se vuelve a ejecutar
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // se desactiva el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);
  }
  • línea 9: creación de la matriz que contendrá todos los fragmentos de la aplicación;
  • línea 11: el primer fragmento es el de la solicitud;
  • líneas 13-28: vamos a crear tantos fragmentos como ejemplos haya. Todos estos fragmentos heredan del fragmento de la respuesta [ResponseFragment] e implementan únicamente lo que es específico del ejemplo: la creación de los valores observados. Estos, de hecho, difieren de un ejemplo a otro;
  • línea 16: el fragmento de un ejemplo lleva un nombre estándar: ExampleXXFragment, donde XX es su posición en el spinner de ejemplos incrementada en 1. XX es también el n.º del fragmento del ejemplo en el gestor de fragmentos;
  • línea 21: instanciación del fragmento del ejemplo n.º i del spinner:
    • Class.forName(exampleName): carga el fragmento en memoria;
    • Class.forName(exampleName).getConstructors()[0]: obtiene la referencia del primer constructor de la clase. La clase ExampleXXFragment solo tiene un constructor. Por lo tanto, se obtendrá una referencia a este;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instancia un objeto de tipo ExampleXXFragment utilizando el constructor del paso anterior. new Object[]{} representa los parámetros pasados a este constructor. Dado que el constructor de la clase ExampleXXFragment no espera parámetros, se pasa una matriz de objetos vacía;
  • línea 27: este fragmento se añade a la matriz de fragmentos;
  • línea 30: hemos visto que el constructor del gestor de fragmentos [SectionsPagerAdapter] esperaba en sus parámetros la matriz de fragmentos que debía gestionar. Ahora se le pasa;
  • línea 22: el contenedor de fragmentos [mViewPager] de la vista asociada a la actividad [MainActivity] se asocia aquí al gestor de fragmentos: el contenedor de fragmentos [mViewPager] muestra los fragmentos del gestor de fragmentos;
  • línea 43: leamos los comentarios: la instrucción viene a decir que todos los fragmentos deben permanecer en el estado en el que los deja el código, sea cual sea el fragmento que se muestre actualmente. De modo que, cuando volvemos a él, lo encontramos en el estado en el que lo dejamos;
  • línea 45: el contenedor de fragmentos [mViewPager] es de tipo [MyPager], lo que permite desactivar el deslizamiento;

El método [MainActivity.showView] es el siguiente:


  // visualización de la vista n.º [position]
  private void showView(int position) {
    // se actualiza el fragmento antes de su visualización
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // se muestra la vista solicitada; se va directamente a la vista (segundo parámetro en false)
    // sin este parámetro, se va por defecto a la vista deseada mostrando rápidamente las vistas intermedias - comportamiento indeseable
    mViewPager.setCurrentItem(position, false);
}
  • línea 3: queremos mostrar el fragmento n.º posición;
  • línea 4: este fragmento se solicita al gestor de fragmentos y, a continuación, se actualiza. De hecho, desde la última vez que se mostró, la sesión puede haber cambiado. El fragmento debe entonces inspeccionarla para ver si debe actualizarse;
  • línea 7: el fragmento se muestra mediante el [ViewPager]. Como este se ha asociado al gestor de fragmentos, se mostrará el fragmento n.º [position], el que acabamos de actualizar en la línea 4;

Terminemos con los dos métodos de gestión de la espera:


  public void beginWaiting() {
    // gestión de la imagen de espera
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // gestión de la imagen de espera
    loadingPanel.setVisibility(View.INVISIBLE);
    // fin de la ejecución
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. El fragmento [RequestFragment]

La clase [RequestFragment] es la siguiente:


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 servicio web
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // número de solicitudes
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // intervalo de generación
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // retardo
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // número de valores generados
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // botón
  private Button btnExecuter;
  // lista de respuestas
  private ListView listReponses;
  private TextView infoReponses;
  // rueda de ejemplos
  private Spinner spinnerExemples;

  // las entradas
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // se recuperan la actividad y la sesión
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // se crea la vista del fragmento a partir de su definición XML
    View view = inflater.inflate(R.layout.request, container, false);
    // componentes
    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);

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

    // al principio no hay mensajes de error
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner de los ejemplos
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // resultado
    return view;
  }
...
}
  • línea 16: la clase [RequestFragment] extiende la clase [MyFragment] (véase el apartado 9.3.7.1);
  • líneas 18-42: los componentes visuales del fragmento (véase el apartado 9.3.7.2);
  • líneas 45-52: las entradas realizadas por el usuario en el formulario;
  • el constructor (líneas 55-58) y el método [onCreateView] se ejecutan cuando la actividad [MainActivity] crea todos los fragmentos de la aplicación. Es la única vez;
  • línea 61: el código del método [onCreateView] es clásico. Cabe destacar, en la línea 102, que el adaptador del spinner de los ejemplos se toma de la sesión. Cabe destacar también la línea 91, donde el clic en el botón [Exécuter] es gestionado por el método [doExecuter];
  • líneas 64-65: los campos [activity] y [session] pertenecen a la clase padre [MyFragment];

El método [doExecuter] es el siguiente:


  // las entradas
  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() {
    // ¿Datos válidos?
    if (isPageValid()) {
      // se guardan los datos en la sesión
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // se memoriza el URL del servicio web
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // acción en curso
      session.setOnAir(true);
      // pero no ha comenzado
      session.setOperationStarted(false);
      // se muestra el fragmento de la respuesta
      activity.selectTab(Constants.VUE_RESPONSE);
      // se inicia la espera
      beginWaiting();
    }
}
  • línea 15: no comentaremos el método [ispageValid]. Comprueba la validez de las entradas y devuelve «true» solo si todas son válidas. En ese caso, se utilizan para inicializar los campos de las líneas 2-9;
  • líneas 17: se activan las diferentes entradas:
    • [spinnerExemples.getSelectedItem().toString()] es el nombre del ejemplo seleccionado por el usuario y se almacena en [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] es el n.º del fragmento asociado al ejemplo y que ha sido memorizado (el fragmento) por el gestor de fragmentos. Este n.º se memoriza en [session.examplePosition];
  • línea 19: el URL del servicio web / jSON se transmite a la actividad, que a su vez lo transmite a la capa [DAO];
  • líneas 21-24: se observa que va a iniciarse una operación;
  • línea 26: se mostrará la pestaña de la respuesta. Para entender lo que va a suceder, hay que recordar el código [MainActivity.selectTab]:

  // selección de una pestaña
  public void selectTab(int position) {
    // hay como máximo 2 pestañas
    // al principio solo hay una, la de la solicitud
    // si la pestaña solicitada es la n.º 1 y aún no existe, hay que crearla
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 pestaña más
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // se selecciona la pestaña por programa, lo que activará el evento [onTabSelected]
    // que asociará la vista correcta a esta pestaña
    tabLayout.getTabAt(position).select();
}
  • inicialmente, la actividad solo había creado la pestaña de la solicitud (pestaña n.º 0);
  • líneas 6-11: se crea la pestaña de respuesta (pestaña n.º 1) si aún no se había creado;
  • línea 14: se selecciona la pestaña n.º position (0 o 1). Esto coloca el evento [onTabSelected] en la cola del evento loop de la aplicación Android;

El gestor del evento [onTabSelected] en [MainActivity] es el siguiente:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // se ha seleccionado una pestaña: se cambia el fragmento mostrado por el contenedor de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // pestaña de consulta
          showView(0);
        } else {
          // pestaña de respuesta: depende del ejemplo elegido
          showView(session.getExamplePosition());
        }
}

En el caso de la pestaña [Response], se ejecuta la línea 9. Se mostrará el fragmento n.º [session.getExamplePosition()]. Por ejemplo, para el ejemplo [exemple-03], el n.º que se ha registrado en [session.examplePosition] es 3. La línea 10 muestra entonces el fragmento n.º 3. La tabla de fragmentos creada inicialmente por la actividad es [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Por lo tanto, es el fragmento [Exemple03Fragment] el que se mostrará. Esto se hace mediante el siguiente código:


  // visualización de la vista n.º [position]
  private void showView(int position) {
    // se actualiza el fragmento antes de su visualización
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // se muestra la vista solicitada - se va directamente a la vista (segundo parámetro en false)
    // sin este parámetro, se va por defecto a la vista deseada mostrando rápidamente las vistas intermedias - comportamiento indeseable
    mViewPager.setCurrentItem(position, false);
}

Se observa que el fragmento se actualizará (línea 4) antes de mostrarse (línea 7).

9.3.7.6. El fragmento [ResponseFragment]

La clase [ResponseFragment] muestra las respuestas del servidor. Su código es el siguiente:


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 {

  // lista de respuestas
  private ListView listReponses;
  private TextView infoReponses;
  // botón
  private Button btnAnnuler;

  // mapeador 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) {
    // se recuperan la actividad y la sesión
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // se crea la vista del fragmento a partir de su definición XML
    View view = inflater.inflate(R.layout.response, container, false);
    // componentes
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // botón [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // resultado
    return view;
  }
...
  // método a ejecutar (mediante código explícito) antes de cada visualización del fragmento
  public void onRefresh() {
...
  }
}
  • línea 21: la clase [ResponseFragment] extiende la clase [MyFragment];
  • líneas 23-27: los componentes del fragmento;
  • líneas 32-36: el constructor solo se ejecuta una vez, durante la creación inicial de los fragmentos de los ejemplos por parte de la actividad. De hecho, todos los fragmentos de los ejemplos extienden el fragmento [ResponseFragment]. Al instanciarse, se invoca el constructor de su clase padre [ResponseFragment];
  • línea 35: inicializa el mapeador jSON de la línea 30 utilizado para mostrar la cadena jSON de una pila de excepciones;
  • líneas 38-59: el método [onCreateView] solo se ejecuta una vez, durante la creación inicial de los fragmentos de los ejemplos por parte de la actividad. En él se encuentra código clásico de una aplicación Android;
  • líneas 52-56: el método que se ejecuta al hacer clic en el botón [Annuler] es el método [doAnnuler];
  • líneas 62-64: el método [onRefresh] se ejecuta cada vez que se muestra la pestaña [Response];

Gracias a los diferentes registros colocados en los métodos importantes, podemos ver lo que ocurre al iniciar la aplicación:

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
  • línea 1: construcción del fragmento [RequestFragment];
  • líneas 2-9: construcción de los fragmentos de los 4 ejemplos de la aplicación;
  • línea 10: inicialización del fragmento [RequestFragment];
  • líneas 11-14: inicialización de los fragmentos de los 4 ejemplos de la aplicación;

A partir de ahí, ya no se vuelven a ver llamadas a estos métodos.

El método [ResponseFragment.onRefresh] es el siguiente:


  // método que se debe ejecutar (mediante código explícito) antes de cada visualización del fragmento
  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()));
    // ¿Ejecución en curso?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // ejecución de la consulta
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • línea 5: se comprueba si el fragmento [RequestFragment] ha realizado una solicitud (session.isOnAir) y si esta se ha iniciado (isOperationStarted). Si el fragmento [RequestFragment] ha realizado una solicitud y esta aún no se está ejecutando, se inicia la operación (líneas 7-8);
  • una vez iniciada la operación, dado que esta es asíncrona, el usuario puede navegar entre las dos pestañas. Si vuelve a la pestaña [Response] y hay una operación en curso, entonces no se ejecutan las líneas 7-8;

El método [doExecuter] de la línea 8 ejecuta la operación solicitada por el usuario:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // Inicio de la espera
    beginWaiting();
    // preparación de la ejecución
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // se crean y ejecutan los observables del ejemplo elegido
    createAndExecuteObservables();
}

// método implementado por clases hijas
protected abstract void createAndExecuteObservables();
  • línea 10: crea, ejecuta y observa observables. Estos son diferentes para cada ejemplo. Por eso, el método [createAndExecuteObservables] es abstracto (línea 14). Se implementará mediante los fragmentos [ExampleXXFragment] que extienden la clase [ResponseFragment];
  • línea 6: se vacía la lista de suscripciones;
  • línea 7: se vacía la lista que muestra las respuestas;
  • línea 8: cuenta el número de respuestas recibidas;

Las clases hijas [ExampleXXFragment] confían al siguiente método [showAlea] la tarea de mostrar los elementos que observan:


  protected void showAlea(String data) {
    // más información
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 respuesta más
    reponses.add(0, data);
    Log.d("rxjava", data);
    // actualización de UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • línea 1: vemos que el elemento observado llega en forma de cadena. De hecho, será la cadena jSON del elemento observado. Esto nos permite disponer de un único método para mostrar el elemento observado, sea cual sea su tipo Java exacto;
  • línea 6: el elemento observado [data] se añade en primera posición de la lista de respuestas. De este modo, el usuario ve al principio de la lista las respuestas más recientes;

La espera se gestiona mediante los siguientes métodos [beginWaiting] y [cancelWaiting]:


  private void beginWaiting() {
    // se pone el reloj de arena
    activity.beginWaiting();
    // se muestra el botón [Annuler]
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // fin de la espera
    activity.cancelWaiting();
    // se oculta el botón [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Recurren a los métodos del mismo nombre de la actividad y se limitan a mostrar/ocultar el botón [Annuler].

El clic en el botón [Annuler] se gestiona mediante el siguiente código:


  protected void doAnnuler() {
    // se cancelan todas las suscripciones
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fin de la espera
    cancelWaiting();
}
  • líneas 3-7: se cancelan una a una todas las suscripciones;

9.3.8. Ejemplos de observables

9.3.8.1. Exemple-01

Las clases [ExampleXXFragment] tienen la funcionalidad de crear, ejecutar y observar observables. La visualización de los valores observados la realiza la clase padre [ResponseFragment].

La clase [Example01Fragment] es la siguiente:

  

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 {

    // mapeadores jSON
    private ObjectMapper mapperAleasUiResponse;

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

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // se solicitan números aleatorios
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // configuración observable n.º i
            // solicitud que se debe realizar al servidor
            Request request = session.getRequest();
            request.setId(i);
            // observable ejecutado en un hilo de cálculo
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observación en el hilo del evento loop;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // se ejecutan todos estos observables
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
...
        }, new Action0() {
...
    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // se extrae la información que se va a mostrar
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • línea 36: el único observable que se va a generar;
  • líneas 37-44: generación y configuración de los diferentes observables que se fusionan (línea 43) en el observable de la línea 36;
  • línea 43: el observable se ejecuta en un subproceso del programador [Schedulers.io()]. La llamada HTTP al servidor se ejecutará en este subproceso;
  • línea 46: el observable final se observa en el hilo del evento loop;
  • líneas 48-57: ejecución de los observables y, por tanto, de las solicitudes al servidor de números aleatorios. Android aún no es compatible con Java 8 y sus lambdas. Por lo tanto, aquí se utilizan clases anónimas para instanciar las interfaces funcionales de RxJava;
  • líneas 49-52: acción ejecutada cuando el observador recibe un nuevo elemento de tipo [AleasDaoResponse] del observable (véase el apartado 9.3.6.1);
  • línea 51: llamada al método [showAlea] de la clase padre. Recordemos que este espera una cadena de caracteres. Dicha cadena es proporcionada por el método [getDataFrom] de las líneas 59-68;
  • línea 63: se devuelve la cadena jSON del tipo [AleasUiResponse] siguiente:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // respuesta [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // hilo de observación
  private String observedOn;
  // hora de observación
  private String observedAt;

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

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

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// getters y setters
...
}
  • a la respuesta de la capa [DAO] (línea 11), se añaden dos datos:
    • línea 13: el hilo de observación;
    • línea 15: la hora de observación;

Volvamos al código de suscripción:


    @Override
    public void createAndExecuteObservables() {
...
        // se ejecutan todos estos observables
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                // se muestra la excepción
                showAlea(getMessagesFromThrowable(th));
                // tras recibir una excepción, el observable no recibe ni onNext ni onCompleted
                // obligado a cancelar la suscripción manualmente
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fin de la espera
                cancelWaiting();
            }
        }));
}
  • líneas 11-18: caso en el que el observador recibe una excepción;
  • línea 14: se vuelve a utilizar el método [showAlea] de la clase padre para mostrar la excepción. El método [getMessagesFromThrowable] es un método de la clase padre [ResponseFragment] que, a partir de una excepción, genera una cadena de caracteres:

  // mensajes de una excepción
  protected String getMessagesFromThrowable(Throwable ex) {
    // se crea una lista con los mensajes de error de la pila de excepciones
    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();
    }
}
  • línea 11: se devuelve la cadena jSON de una lista de mensajes de error (línea 4);

Volvamos al código de suscripción al observable:

  • líneas 19-25: el código que se ejecuta cuando el observador recibe la notificación de fin de emisión. A continuación, se cancela la espera (línea 23), lo que actualiza la interfaz gráfica;

El resultado de la ejecución del ejemplo 01 produce un resultado similar al siguiente:

Image

Cada elemento de la lista es la cadena jSON de un valor observado. Los campos de la cadena jSON son los siguientes:

  • aleas: la lista de números aleatorios proporcionada por el servidor;
  • idClient: el número de la solicitud (se puede ver que las respuestas han llegado en orden disperso);
  • on: el hilo de ejecución del observable que ha emitido este valor;
  • requestAt: hora de la solicitud del cliente;
  • responseAt: hora de la respuesta del servidor;
  • delay: tiempo de espera observado por el servidor;
  • error: código de error devuelto por el servidor (0 = sin error);
  • mensaje: mensaje de error devuelto por el servidor (null = sin error);
  • observedAt: hora de observación del valor observado;
  • observedOn: subproceso de observación del valor observado;

9.3.8.2. Exemple-02

La clase [Example02Fragment] es la siguiente:


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 {

    // mapeadores jSON
    private ObjectMapper mapperAleasUiResponse;

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

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // se solicitan números aleatorios
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // preparación de la solicitud
            Request request = session.getRequest();
            request.setId(i);
            // solo se conservan los observables con un número de cliente par
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // ejecución en el hilo de E/S
                            .subscribeOn(Schedulers.io()));
        }
        // observación en el hilo del evento loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // se ejecutan estos observables
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fin de la espera
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // se extrae la información que se va a mostrar
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Este ejemplo es análogo al anterior (línea 38). Sin embargo, de las observables obtenidas en el ejemplo anterior, solo se conservan aquellas con un número de cliente par (líneas 42-46), gracias al método [filter] (línea 41).

Los resultados obtenidos son los siguientes (para 10 consultas):

Image

9.3.8.3. Exemple-03

La clase [Example03Fragment] es la siguiente:


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 {

  // mapas jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // se solicitan los números aleatorios
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparación de la solicitud
      Request request = session.getRequest();
      request.setId(i);
      // configuración observable
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).map(new Func1<AleasDaoResponse, List<Integer>>() {
        @Override
        public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getAleas();
        }
      })
        // ejecución en el hilo de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observación en el hilo del evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // se ejecutan estos observables
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fin de la espera
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // se extrae la información que se va a mostrar
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Este ejemplo es similar al Ejemplo-02:

  • línea 40: se definen los mismos observables que en el Ejemplo-02;
  • línea 45: cada uno de los valores emitidos por los observables anteriores se transforma, mediante el método [map], en un tipo List<Integer>, que es la lista de números aleatorios generados por el servidor;
  • línea 58: a partir de ahora, el valor observado es de tipo List<Integer>;

El resultado obtenido para 10 solicitudes es el siguiente:

Image

9.3.8.4. Exemple-04

La clase [Example04Fragment] es la siguiente:


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 {

  // mapas jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // se solicitan los números aleatorios
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparación de la solicitud
      Request request = session.getRequest();
      request.setId(i);
      // configuración de observables
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // ejecución en un hilo de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observación en el hilo del evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // se ejecutan estos observables
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fin de la espera
            cancelWaiting();
          }
        }
      ));

  }
}

Este ejemplo es similar al Ejemplo-03, salvo que, en lugar de utilizar, en la línea 42, el método [map], se utiliza el método [flatMap].

  • línea 55: cabe señalar que, a partir de ahora, el tipo del valor observado es Integer;

Para 10 consultas, se obtienen los siguientes resultados:

Image

En esta ocasión, hay más valores observados que consultas.

9.3.8.5. Exemple-05

A continuación, explicamos el procedimiento a seguir para añadir un nuevo ejemplo de observables a la aplicación.

Supongamos que queremos reproducir el ejemplo [Exemple22h] del apartado 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 {
        // proceso
        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()));
        // suscripciones
        ProcessUtils.subscribe(1, process);
    }
}
  • los valores de la observable [Observable.range(1, 10)] se agrupan primero en valores pares e impares mediante el método [groupBy] (línea 11) y luego se reagrupan en una sola observable mediante el método [concatMap] (línea 12);

paso 1

Se crea un nuevo ejemplo en el archivo [exemples.xml]:

  

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

Arriba se ha añadido la línea 8. El nombre dado al ejemplo puede ser cualquiera.

Paso 2

Duplicamos la clase [Example04Fragment] en [Example05Fragment]. En este caso, el nombre viene impuesto.

Paso 3

Modificamos el código de [Example05Fragment] de la siguiente manera:


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 {

  // mapas jSON
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instancias de las interfaces funcionales
    // 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();
      }
    };
    // se solicitan los números aleatorios
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparación de la solicitud
      Request request = session.getRequest();
      request.setId(i);
      // configuración observable
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // ejecución en un hilo de E/S
        .subscribeOn(Schedulers.io());
    }
    // observación en el hilo del evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // se ejecutan estos observables
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fin de la espera
            cancelWaiting();
          }
        }
      ));

  }
}
  • línea 67: representa el observable del ejemplo 04: un flujo de enteros;
  • línea 68: agrupamos este flujo de enteros según un criterio booleano que vamos a definir. Obtendremos un observable de tipo Observable<GroupedObservable<Boolean, Integer>> que, por lo tanto, emite elementos de tipo GroupedObservable<Boolean, Integer>;
  • línea 68: el método [concatMap] generará elementos de tipo Integer a partir de los elementos de tipo GroupedObservable<Boolean, Integer>;
  • líneas 32-59: para que la creación del observable de las líneas 67-69 resulte más legible, hemos aislado las instancias de las interfaces funcionales que necesitan los diferentes operadores [filter, flatMap, groupBy, concatMap];
  • líneas 47-52: el método [groupBy] espera un parámetro de tipo Func1<T,K>, donde T es el tipo de los elementos agrupados y K el tipo del criterio de agrupación. A partir del elemento T, la instancia Func1<T,K> se encarga de generar la clave K de agrupación del elemento;
  • líneas 48-51: los elementos de tipo Integer se agruparán por paridad. La instancia Func1<Integer,Boolean> genera la clave true o false, dependiendo de si el elemento debe colocarse en un grupo u otro. En la salida, tenemos dos grupos: el grupo de elementos pares con clave true y el grupo de elementos impares con clave false;
  • líneas 53-59: el método [concatMap] espera un parámetro de tipo Func1<T,Observable<R>> y produce un observable de elementos de tipo R. El tipo T será aquí el tipo emitido por el operador [groupBy], en este caso un tipo GroupedObservable<Boolean, Integer>;
  • línea 57: a partir del elemento de tipo [GroupedObservable<Boolean, Integer>], se genera un tipo Observable<Integer>. Como el operador [groupBy] ha producido dos grupos, el operador [concatMap] producirá dos observables de tipo [Observable<Integer>]. Al igual que [flatMap], los aplanará en un único observable. Pero, a diferencia de [flatMap], no mezcla los elementos de los observables aplanados. Por lo tanto, se deben observar dos grupos aislados: los números aleatorios pares y los demás impares.

paso 4

Ejecutamos la aplicación:

Image

y se obtienen los siguientes resultados:

Image

  • en [1], los números aleatorios pares; en [2], los impares;

9.3.8.6. Para continuar

Ahora se invita al lector a crear sus propios ejemplos y también a experimentar con diversos valores para las entradas del formulario que configura las solicitudes realizadas al servidor de números aleatorios.

9.3.9. Conclusión

Hemos creado en el entorno Android la siguiente arquitectura:

El cliente Android:

La capa [DAO] se comunica con el servidor que genera los números aleatorios mostrados por la tableta Android. Este servidor tiene la siguiente arquitectura de dos capas:

La capa [DAO] realizaba n solicitudes HTTP al servidor de números aleatorios y la capa [swing] esperaba de forma asíncrona los resultados de estas para mostrarlos. Estas n solicitudes HTTP se realizaban al mismo servidor, que proporcionaba los mismos tipos de respuestas. Esto nos permitió fusionar (mergeWith) las respuestas en un único observable.

En la realidad, las aplicaciones de Android se dirigen a servidores diferentes y probablemente no se fusionarán sus respuestas. Las solicitudes HTTP a estos servidores se gestionarán de forma independiente unas de otras y sus resultados se observarán mediante métodos separados.