9. RxJava en el entorno Android
9.1. Introduction
Aquí retomaremos una aplicación que ya se ha tratado en varios documentos:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (capítulo 4);
- [Introduction à la programmation de tablettes Android par l'exemple] (capítulo 9);
- [Introduction à la programmation de tablettes Android par l'exemple - version 2] (párrafo 1.11);
Se trata de una aplicación cliente/servidor en la que el servidor genera de manera asíncrona números aleatorios que el cliente Android muestra:
- en el documento 1, el cliente de Android utiliza una tecnología no estándar;
- en el documento 2, el cliente de Android utiliza la tecnología estándar de Android para las operaciones asincrónicas;
- 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 de Android es el siguiente:
![]() |
La capa [DAO] se comunica con el servidor que genera los números aleatorios que muestra la tableta Android. Este servidor tiene la siguiente arquitectura de dos capas:
![]() |
Los clientes consultan 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 de 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 párrafo 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 está implementada por el siguiente proyecto de IntelliJ IDEA: [1]:
![]() | ![]() |
El servidor se inicia mediante [2-3]. A continuación, se muestran los registros en la consola:
- 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 dentro de [minDelay, maxDelay];
En un navegador, solicitemos este URL:
![]() |
Se solicitaron:
- 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 los 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 de Gradle del proyecto
![]() |
El proyecto [serveur] es un proyecto de 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 el Classpath del proyecto, [Spring Boot] deduce una configuración plausible o probable para este. Así, si Hibernate se encuentra en el Classpath del proyecto, entonces [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 realizar las configuraciones que [Spring Boot] no ha establecido por defecto o aquellas que [Spring Boot] sí ha establecido por defecto, pero que deben especificarse. En cualquier caso, la configuración realizada por el desarrollador es la que prevalece;
- líneas 14-15: dos complementos de Gradle necesarios para aprovechar 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 de la máquina;
- 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 la versión de la dependencia. Se utilizará la que figura en el proyecto importado [spring-boot];
Para actualizar el proyecto, hay que forzar la descarga de las dependencias [1-3]:
![]() | ![]() ![]() |
Veamos en [4] las dependencias que trae este archivo [build.gradle]:
![]() |
Son muchas. Spring Boot para la web ha incluido las dependencias que probablemente necesitará una aplicación web de Spring MVC. Esto significa que algunas tal vez sean innecesarias. Spring Boot es ideal para un tutorial:
- 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 archivo JAR ejecutable que incluye todas las dependencias mencionadas anteriormente. Este archivo JAR se puede trasladar de una plataforma a otra sin necesidad de reconfigurarlo.
En el sitio web del ecosistema de Spring [http://spring.io/guides] se pueden encontrar numerosos ejemplos que utilizan Spring Boot. 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 en sí mismo 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 casi idéntica a la analizada en el entorno Swing en el párrafo 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 en sí mismo 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 verificaciones
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();
// ¿esperando?
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));
}
// retorno del resultado
return new AleasMetier(delay,nombres);
}
}
No comentaremos la clase: es análoga a la que se presentó en el entorno Swing en el párrafo 8.4. Solo señalaremos los siguientes puntos:
- línea 10: la anotación de 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 lograr 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 solo 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 de 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 de Spring tienen el mismo tipo. En ese caso, hay que agregar 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 manejarla con un try / catch);
- línea 6: se agrega 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:
- solicitud: las URL solicitadas tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] es la clase de Spring que procesa las URL entrantes. Esta «enruta» el URL hacia la acción que debe procesarlo. Estas acciones son métodos de clases específicas llamadas [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 encontró el URL solicitado (error 404 NOT FOUND);
- procesamiento
- la acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden provenir 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 junto con su solicitud;
- al procesar la solicitud del usuario, la acción puede requerir 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 pudo procesar correctamente
- una página de confirmación en caso contrario
- la acción solicita que se muestre una vista específica: [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Este es el M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
- respuesta: la vista V seleccionada utiliza la plantilla M generada 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 sus clientes una respuesta del 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;
// constructores
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 a la clase [AleasController] en un singleton de Spring. Además, indica que la clase contiene métodos que procesarán solicitudes para ciertas 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]. Esta será la clase [Metier] anterior. Esto se debe a que le hemos aplicado la anotación [@Service], por lo que se maneja como un componente de Spring;
- líneas 22-23: la anotación [@Autowired] le indica a Spring que inyecte en el campo un componente de tipo [ObjectMapper]. Definiremos este componente 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 ya han sido inicializados por Spring MVC. Veremos cómo. Por otra parte, si se ejecuta, es porque el servidor web recibió 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. Aquí, 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 y 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 le 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
![]() |
Existen diversas formas de configurar Spring:
- con archivos XML;
- con código Java;
- con una combinación de ambos;
Decidimos 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 con [@Service];
- la clase [AleasController], anotada por [@Controller];
- línea 16: la anotación [@EnableWebMvc] genera configuraciones automáticas para el marco Spring MVC;
- líneas 19-20: inyección del contexto de Spring (contenedor de objetos de Spring). Esta inyección es necesaria porque el objeto de las líneas 22-26 la requiere;
- el archivo de configuración de Spring puede definir nuevos objetos de Spring mediante métodos anotados con [@Bean]. El resultado del método se convierte entonces en un objeto de Spring;
- líneas 22-26: definición del servlet del marco Spring MVC, el que enruta las solicitudes HTTP hacia el controlador y el 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 que se encuentra en los archivos del proyecto. Este esperará las solicitudes en el puerto 8080;
- líneas 39-42: un mapeador jSON. Este es el que se ha inyectado en los objetos de Spring [Metier] y [AleasController];
9.2.6. Ejecución del servidor web
![]() |
El proyecto se ejecuta a partir de 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 el array de argumentos que se pasa al método [main] (línea 7). En este caso, no habrá argumentos;
Para la ejecución propiamente dicha, se invita al lector a consultar el párrafo 9.2.1.
9.3. El cliente de 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:
- una capa [Présentation] (vistas + actividad);
- una capa [DAO] que se comunica con el servicio [web / JSON] que analizamos anteriormente.
9.3.1. RxAndroid
Para comunicarse de manera asíncrona con el servidor de números aleatorios, el cliente de Android utilizará la biblioteca RxAndroid. Esta amplía RxJava al entorno de Android. Al igual que se hizo para la aplicación Swing, solo utilizaremos una única extensión que aporta 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 «bucle de eventos» o «hilo de la interfaz de usuario»;
- cuando un evento inicia acciones asincrónicas, los resultados de estas deben recuperarse en el hilo de la interfaz de usuario si se van a utilizar para actualizarla;
El cliente de Android:
- enviará varias solicitudes asincrónicas al servidor de números aleatorios. Estas solicitudes se ejecutarán del lado del cliente con los hilos del programador [Schedulers.io()];
- estas solicitudes asincrónicas devolverán observables que se fusionarán en uno solo (merge);
- este observable se observará del 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 IntelliJ IDEA que se utilice. Es probable que la ejecución de [2] mencionada anteriormente no funcione a la primera en una computadora distinta a la mía. Configurar correctamente IntelliJ IDEA para ejecutar este proyecto puede ser una tarea abrumadora para los principiantes. A continuación, te indico algunos puntos a tener en cuenta:
- en [3], acceder a la estructura del proyecto;
![]() | ![]() |
- en [4-5], el JDK y los SDK de Android que tengo instalados en mi equipo. Cabe señalar que el JDK 1.8 no es imprescindible. Android no admite 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 como se distribuye, ha sido configurado con un JDK 1.8;
El archivo [build.gradle] [6] que configura el proyecto de Android es el siguiente:
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
// reemplazar por la versión actual del complemento de Android
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
jcenter()
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "android.aleas"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
Según los archivos SDK de Android presentes, es posible que sea necesario modificar las versiones de las líneas 8, 24-25 y 29.
Para instalar nuevos SDK de Android, utilice el Administrador de SDK 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, verifica la ruta de SDK Android en el archivo [local.properties] [4], línea 11 a continuación:
## Este archivo es generado automáticamente por Android Studio.
# No modifique este archivo -- YOUR CHANGES WILL BE ERASED!
#
# Este archivo debe *NOT* registrarse en los sistemas de control de versiones,
# ya que contiene información específica de su configuración local.
#
# Ubicación del archivo SDK. Solo lo utiliza Gradle.
# Para personalizarlo al usar un sistema de control de versiones, lee la
# nota del encabezado.
#Jueves 7 de abril 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. Ejecución del proyecto en 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, al ejecutarse, 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 línea inicial:

Para saber qué poner en [1], abre una ventana de comandos DOS y escribe 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
Escribe en [1] una de las direcciones IP de tu equipo (líneas 20, 28, 32). Si tienes un firewall de Windows, probablemente tendrás que desactivarlo para que el emulador de Android pueda conectarse al servidor de números aleatorios.
Al ejecutar las consultas asincrónicas con la información anterior, se obtienen los siguientes resultados:

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 del lado del cliente;
- requestAt: hora de la solicitud;
- responseAt: hora de recepción de la respuesta;
- delay: el tiempo de espera que el servidor observó 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: hilo en el que se observó la respuesta. Aquí siempre será [main], que designa el hilo de la interfaz de usuario;
Dado que las solicitudes son asincrónicas y los tiempos de espera impuestos al servidor son aleatorios, las respuestas llegan en orden disperso.
9.3.4. Las dependencias de 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 y 3 son dependencias estándar de un proyecto de 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 incluye la biblioteca JSON y [Jackson], que utiliza 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 de 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 en sí mismo 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 máximo (ms) 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 manera 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 tiempos de espera máximos en la implementación [DAO], con el fin de evitar tiempos de espera excesivamente largos 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 objeto URL del servidor al que se debe 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;
}
// getter y setter
...
}
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: hilo 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úmero 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á de la comunicación 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());
}
- la comunicación del cliente con el servidor web / JSON la 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 medio 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 entre el cliente y el 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, es decir, 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 de String a String, lo que implica que el tipo String recibido del servidor no sufrirá 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 enví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 el método [onError] se ha llamado previamente —conforme a la teoría—, por lo que esta instrucción solo se podría colocar en el try
subscriber.onCompleted();
}
}
});
}
- línea 2: hay que recordar que debemos generar un tipo [Observable<AleasResponse>];
- línea 3: una entrada 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 convertir una acción sincrónica en una asincrónica mediante el método [Observable.create]. Aquí seguimos ese mismo procedimiento;
- 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 registrar la hora de la solicitud;
- línea 16: solicitud HTTP sincrónica. Se obtiene una respuesta jSON. El método [executeRestService] espera tres parámetros:
- el método HTTP que se utilizará para consultar el servicio;
- el URL del servicio;
- 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. El formato es [AleasDaoResponse];
- líneas 35-37: tratan todos los casos de error de manera 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

La vista de la respuesta

9.3.7.1. La clase [MyFragment]
Hay dos fragmentos:
- [RequestFragment] para la consulta;
- [ResponseFragment] para la respuesta;
Ambos fragmentos extienden 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] extiende la clase de 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 esta razón, la clase misma se declara abstracta (línea 7);
La clase [Session] contiene los datos que comparten los distintos 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 consultas
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úmero en la lista de fragmentos
private int examplePosition;
// el adaptador del spinner de los 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 cuenta con los siguientes componentes:

La aplicación cuenta con una sola vista que consta de dos pestañas:
- [1]: la pestaña de la consulta;
- [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 el número count, 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 | el URL del servicio de generación de números aleatorios; |
8 | Spinner | spinnerExemples | la lista desplegable de ejemplos. Cada ejemplo ilustra un método específico 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 entrada:

Los componentes del 1 al 6 son componentes [TextView] con los siguientes nombres (en 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:

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 aplicación -->
<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 distintos 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 {
// controla el 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] extiende 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). En este caso, no queremos ese comportamiento;
- línea 11: la variable booleana 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 de 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, es 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 controladores de eventos se comunicarán con la capa [activité] cuando deseen interactuar 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;
// administrador 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 que muestra 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 los fragmentos de las respuestas
createResponseFragments();
// gestión de la imagen de espera
loadingPanel.setVisibility(View.INVISIBLE);
}
Este código es bastante común 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 del 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 que muestra 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úmero 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 administrador de fragmentos en memoria (clase incluida en el archivo Java de MainActivity):
// el administrador de fragmentos: debe definir los métodos getItem y 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 que se deben gestionar
@Override
public int getCount() {
// número de fragmentos
return fragments.length;
}
}
}
- La clase [SectionsPagerAdapter] extiende la clase de 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 párrafo 9.3.7.1;
- línea 8: para crearse, 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() {
// selector de ejemplos
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Especifica 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 (administrador de visualización) al adaptador. Ahora el adaptador cuenta tanto con los datos como con 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() {
// ejemplos de spinner
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Especifica 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;
}
//: se ha creado el fragmento; lo agregamos a la matriz
tFragments[i] = fragment;
}
// instanciación del administrador de fragmentos con estos nuevos fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// Configurar el ViewPager con el adaptador de secciones.
mViewPager.setAdapter(mSectionsPagerAdapter);
// navegación entre páginas: esta instrucción es importante
// aquí se indica que, a ambos lados de la vista mostrada, se deben mantener las vistas inicializadas [tFragments.length]
// Esto implica que todos los fragmentos utilizados por la aplicación están en memoria e inicializados
// si no se hace esto, entonces, por defecto, el valor de [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 tolere ejecutarse dos veces; esto genera un caos enorme y es complicado de manejar
// aquí se prefirió 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 consulta;
- líneas 13-28: se crearán tantos fragmentos como ejemplos haya. Todos estos fragmentos heredan del fragmento de 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 tiene un nombre estándar: ExampleXXFragment, donde XX es su posición en el spinner de ejemplos más 1. XX es también el número del fragmento del ejemplo en el administrador 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 dicho 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 agrega a la matriz de fragmentos;
- línea 30: hemos visto que el constructor del administrador de fragmentos [SectionsPagerAdapter] esperaba en sus parámetros la matriz de fragmentos que debía administrar. Ahora se le pasa dicha matriz;
- línea 22: el contenedor de fragmentos [mViewPager] de la vista asociada a la actividad [MainActivity] se asocia aquí al administrador de fragmentos: el contenedor de fragmentos [mViewPager] muestra los fragmentos del administrador de fragmentos;
- línea 43: leamos los comentarios; la instrucción equivale a decir que todos los fragmentos deben permanecer en el estado en que el código los coloca, sin importar cuál sea el fragmento que se esté mostrando actualmente. De modo que, cuando regresamos a él, lo encontramos en el estado en 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 mostrarlo
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 no deseado
mViewPager.setCurrentItem(position, false);
}
- línea 3: se quiere mostrar el fragmento n.º posición;
- línea 4: este fragmento se solicita al administrador de fragmentos y luego se actualiza. De hecho, desde la última vez que se mostró, la sesión pudo haber cambiado. El fragmento debe entonces revisar la sesión para ver si debe actualizarse;
- línea 7: el fragmento se muestra mediante el [ViewPager]. Como este se ha asociado al administrador 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 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;
// retraso
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;
// selector 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;
// generador
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);
// indicador de carga de los ejemplos
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// resultado
return view;
}
...
}
- línea 16: la clase [RequestFragment] extiende la clase [MyFragment] (véase el párrafo 9.3.7.1);
- líneas 18-42: los componentes visuales del fragmento (véase el párrafo 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 estándar. Cabe destacar, en la línea 102, que el adaptador del spinner de los ejemplos se incluye en la sesión. También cabe destacar, en la línea 91, que al hacer clic en el botón [Exécuter] se gestiona mediante 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 guarda 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]. Este verifica la validez de los datos ingresados y devuelve «true» únicamente si todos son válidos. En ese caso, se utilizan para inicializar los campos de las líneas 2 a 9;
- línea 17: se registran los distintos datos introducidos:
- [spinnerExemples.getSelectedItem().toString()] es el nombre del ejemplo seleccionado por el usuario y se almacena en [session.exampleName];
- [spinnerExemples.getSelectedItemPosition() + 1] es el número del fragmento asociado al ejemplo y que ha sido almacenado (el fragmento) por el administrador de fragmentos. Este número se almacena en [session.examplePosition];
- línea 19: el URL del servicio web / jSON se transmite a la actividad, la cual a su vez lo transmite a la capa [DAO];
- líneas 21-24: se observa que está a punto de 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 consulta
// si la pestaña solicitada es la n.º 1 y aún no existe, entonces 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 mediante un programa, lo que activará el evento [onTabSelected]
// que asociará la vista correcta a esa 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 fila de espera del bucle de eventos de la aplicación para Android;
El manejador 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 que muestra 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úmero que se registró 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 mostrarlo
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 no deseado
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 que se debe 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 instanciarlos, se invoca el constructor de su clase padre [ResponseFragment];
- línea 35: inicializa el mapeador jSON de la línea 30, que se utiliza 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. Aquí se encuentra código clásico de una aplicación de 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 distintos registros colocados en los métodos importantes, podemos ver qué sucede al iniciar la aplicación:
- 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 cuatro ejemplos de la aplicación;
A partir de ahí, ya no vuelven a aparecer 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()));
// ¿Se está ejecutando?
if (session.isOnAir() && !session.isOperationStarted()) {
// ejecución de la solicitud
session.setOperationStarted(true);
doExecuter();
}
}
- línea 5: se verifica 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 asincrónica, 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). Será implementado por 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] le asignan 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 del UI
listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
- línea 1: se observa que el elemento observado llega en forma de cadena. De hecho, será la cadena jSON del elemento observado. Esto nos permite tener un único método para mostrar el elemento observado, sin importar cuál sea su tipo exacto en Java;
- línea 6: el elemento observado [data] se agrega en la primera posición de la lista de respuestas. De este modo, el usuario ve al inicio de la lista las respuestas más recientes;
La espera se gestiona mediante los siguientes métodos [beginWaiting] y [cancelWaiting]:
private void beginWaiting() {
// se activa 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);
}
Estos métodos utilizan los métodos del mismo nombre de la actividad y simplemente muestran u ocultan el botón [Annuler].
Al hacer clic en el botón [Annuler], se ejecuta 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 por una todas las suscripciones;
9.3.8. Ejemplos de observables
9.3.8.1. Exemple-01
Las clases [ExampleXXFragment] tienen la función 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 enviar 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 bucle de eventos;
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 generará;
- líneas 37-44: generación y configuración de los distintos observables que se fusionan (línea 43) en el observable de la línea 36;
- línea 43: el observable se ejecuta en un hilo del programador [Schedulers.io()]. La llamada HTTP al servidor se ejecutará en este hilo;
- línea 46: el observable final se observa en el hilo del bucle de eventos;
- líneas 48-57: ejecución de los observables y, por lo 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 que se ejecuta cuando el observador recibe un nuevo elemento de tipo [AleasDaoResponse] del observable (véase el párrafo 9.3.6.1);
- línea 51: llamada al método [showAlea] de la clase padre. Recordemos que este método 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] de la siguiente manera:
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 agregan 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() {
...
// ejecutamos 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));
// después de recibir una excepción, el observable no recibe ni onNext ni onCompleted
// me veo 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. Entonces se cancela la espera (línea 23), lo que actualiza la interfaz gráfica;
El resultado de la ejecución del ejemplo 01 es similar al siguiente:

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 consulta (se puede observar que las respuestas llegaron en orden disperso);
- on: el hilo de ejecución del observable que generó este valor;
- requestAt: hora de la solicitud del cliente;
- responseAt: hora de la respuesta del servidor;
- delay: demora observada 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 en que se observó el valor;
- observedOn: hilo 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 consulta
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 bucle de eventos
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 similar 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):

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 {
// mapadores 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 consulta
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 hilo de E/S
.subscribeOn(Schedulers.io()));
}
// observación en el hilo del bucle de eventos
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:

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 consulta
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 un hilo del bucle de eventos
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:

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 agregar un nuevo ejemplo de observables a la aplicación.
Supongamos que queremos reproducir el ejemplo [Exemple22h] del párrafo 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 del observable [Observable.range(1, 10)] se agrupan primero en valores pares e impares mediante el método [groupBy] (línea 11) y luego se combinan en un solo 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 agregado la línea 8. El nombre que se le dé al ejemplo puede ser cualquiera.
Paso 2
Duplicamos la clase [Example04Fragment] en [Example05Fragment]. En este caso, el nombre es obligatorio.
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 consulta
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 un hilo del bucle de eventos
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 números enteros;
- línea 68: agrupamos este flujo de enteros según un criterio booleano que definiremos. 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 facilitar la lectura de la creación del observable de las líneas 67-69, hemos aislado las instancias de las interfaces funcionales que necesitan los distintos 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 es 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. Al final, tenemos dos grupos: el grupo de elementos pares con la clave true y el grupo de elementos impares con la clave false;
- líneas 53-59: el método [concatMap] espera un parámetro de tipo Func1<T,Observable<R>> y devuelve 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] generó dos grupos, el operador [concatMap] generará dos observables de tipo [Observable<Integer>]. Al igual que [flatMap], los fusionará en un único observable. Pero, a diferencia de [flatMap], no mezcla los elementos de los observables fusionados. 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:

y se obtienen los siguientes resultados:

- 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 los campos del formulario que configuran las consultas realizadas al servidor de números aleatorios.
9.3.9. Conclusión
Hemos creado en el entorno de Android la siguiente arquitectura:
El cliente de Android:
![]() |
La capa [DAO] se comunica con el servidor que genera los números aleatorios que se muestran en 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 manera asíncrona los resultados de estas para mostrarlos. Estas n solicitudes HTTP se realizaban al mismo servidor, el cual proporcionaba los mismos tipos de respuestas. Esto nos permitió fusionar (mergeWith) las respuestas en un único observable.
En la práctica, las aplicaciones de Android se comunican con servidores diferentes y probablemente no se fusionarán sus respuestas. Las solicitudes HTTP a estos servidores se gestionarán de manera independiente unas de otras y sus resultados se observarán mediante métodos separados.

















































