9. RxJava en el entorno Android
9.1. Introducción
Aquí volveremos a examinar una aplicación que ya se ha tratado en varios documentos:
- [Android para desarrolladores JEE: un modelo asíncrono para clientes Android] (Capítulo 4);
- [Introducción a la programación de tabletas Android a través de ejemplos] (Capítulo 9);
- [Introducción a la programación para tabletas Android a través de ejemplos - Versión 2] (Sección 1.11);
Se trata de una aplicación cliente/servidor en la que el servidor envía de forma asíncrona números aleatorios que el cliente Android muestra:
- En el documento 1, el cliente Android utiliza tecnología no estándar;
- en el documento 2, el cliente Android utiliza la tecnología estándar de Android para operaciones asíncronas;
- En el documento 3, el cliente Android utiliza la misma tecnología que en el documento 2, pero simplificada mediante el uso de anotaciones de la biblioteca Android Annotations;
El cliente Android es el siguiente:
![]() |
La capa [DAO] se comunica con el servidor que genera los números aleatorios que muestra la tableta Android. Este servidor tiene la siguiente arquitectura de dos capas:
![]() |
Los clientes consultan determinadas URL en la capa [web / JSON] y reciben una respuesta de texto en formato JSON (JavaScript Object Notation).
Desglosaremos el análisis de la aplicación en dos pasos:
El servidor web/JSON
- su capa [de negocio];
- su servicio [web / JSON] implementado con Spring MVC;
El cliente Android
- su capa [DAO];
- su actividad;
- sus vistas;
9.2. El servicio web / JSON
Nota: El servicio web / JSON se implementa utilizando la tecnología Spring MVC. Los lectores que no estén familiarizados con esta tecnología pueden:
- leer simplemente la sección 9.2.1, que explica cómo iniciar el servidor y cómo realizar consultas;
- consultar el documento [Spring MVC y Thymeleaf con ejemplos], en particular el capítulo 4, que presenta las principales anotaciones utilizadas en el código;
9.2.1. El proyecto IntelliJ IDEA
El servicio web / JSON tiene la siguiente arquitectura:
![]() |
Esta arquitectura se implementa mediante el siguiente proyecto de IntelliJ IDEA [1]:
![]() | ![]() |
El servidor se inicia mediante [2-3]. A continuación, se muestran los registros de la consola:
- Línea 12: indica que el servicio está disponible en el puerto 8080;
- línea 10: la URL única del servicio web / JSON disponible a través de una operación HTTP GET. Sus parámetros son los siguientes:
- [a,b]: rango para generar números aleatorios;
- [minCount, maxCount]: número de números aleatorios que se generan, donde count es un número aleatorio en el intervalo [minCount, maxCount];
- [minDelay, maxDelay]: el servicio espera un retraso de milisegundos antes de devolver los números solicitados, donde el retraso es un número aleatorio en [minDelay, maxDelay];
En un navegador, solicitemos esta URL:
![]() |
Solicitamos:
- números aleatorios en el intervalo [100, 200];
- n números aleatorios, donde n está en el intervalo [10, 20];
- un tiempo de espera de x milisegundos, donde x está en el intervalo [300, 400];
En la respuesta:
- aleas: lista de números aleatorios generados;
- delay: el tiempo de espera en milisegundos que ha establecido el servidor;
- error: un código de error; 0 si no hay error;
- message: un mensaje de error; null si no hay error;
9.2.2. Las dependencias de Gradle del proyecto
![]() |
El proyecto [server] es un proyecto Gradle configurado mediante el siguiente archivo [build.gradle] [1]:
// généré par http://start.spring.io/ (mai 2016)
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'spring-boot'
jar {
baseName = 'serveur'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
}
- línea 1: un comentario que explica 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. Basándose en las bibliotecas presentes en la ruta de clases del proyecto, [Spring Boot] deduce una configuración plausible o probable para el proyecto. Así, si Hibernate está en la ruta de clases del proyecto, [Spring Boot] deducirá que la implementación de JPA que se está utilizando es Hibernate y configurará Spring en consecuencia. El desarrollador ya no tiene que hacer esto. Lo único que le queda por hacer es configurar los ajustes que [Spring Boot] no ha configurado por defecto, o aquellos que [Spring Boot] sí ha configurado por defecto pero que deben especificarse. En cualquier caso, la configuración realizada por el desarrollador tiene prioridad;
- líneas 14-15: dos complementos de Gradle necesarios para utilizar el contenido de este archivo Gradle;
- líneas 17-20: definen las características del archivo generado para este proyecto;
- líneas 22-23: para 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 del servidor Tomcat. Este es el que se utilizará para implementar la aplicación web. Tenga en cuenta que no se ha especificado la versión de la dependencia. Se utilizará la versión especificada en el proyecto [spring-boot] importado;
Para actualizar el proyecto, debe forzar la descarga de las dependencias [1-3]:
![]() | ![]() ![]() |
Echemos un vistazo a [4] las dependencias incluidas en este archivo [build.gradle]:
![]() |
Hay muchas. Spring Boot para la web ha incluido las dependencias que probablemente necesitará una aplicación web Spring MVC. Esto significa que algunas pueden ser innecesarias. Spring Boot es ideal para un tutorial:
- incluye las dependencias que probablemente necesitaremos;
- veremos que simplifica enormemente la configuración del proyecto Spring MVC;
- incluye un servidor Tomcat integrado [1], lo que nos ahorra tener que implementar la aplicación en un servidor web externo;
- nos permite generar un archivo JAR ejecutable que incluye todas las dependencias mencionadas. Este archivo JAR se puede trasladar de una plataforma a otra sin necesidad de reconfigurarlo.
Puedes encontrar muchos ejemplos que utilizan Spring Boot en el sitio web del ecosistema Spring [http://spring.io/guides]. Ahora que conocemos las dependencias del proyecto, podemos pasar al código.
9.2.3. La capa [de negocio]
![]() |
![]() |
La capa [de negocio] tendrá la siguiente interfaz [IMetier]:
package dvp.rxjava.server.metier;
public interface IMetier {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}
Esta interfaz es prácticamente idéntica a la descrita en el entorno Swing en la sección 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 {
// fields
private int delay;
private List<Integer> aleas;
// manufacturers
public AleasMetier(){
}
public AleasMetier(int delay, List<Integer> aleas){
this.delay=delay;
this.aleas=aleas;
}
public AleasMetier(AleasMetier aleasMetier){
this.delay=aleasMetier.delay;
this.aleas=aleasMetier.aleas;
}
// getters and setters
...
}
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) {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
// some checks
List<String> messages = new ArrayList<>();
int erreur = 0;
if (a < 0) {
messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
erreur |= 2;
}
if (a >= b) {
messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
erreur |= 4;
}
if (minCount < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
erreur |= 16;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
erreur |= 32;
}
if (minDelay < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
erreur |= 64;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
erreur |= 128;
}
if (maxDelay > 5000) {
messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
erreur |= 256;
}
// mistakes?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// random number generator
Random random = new Random();
// waiting?
int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
String message = null;
try {
message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
} catch (JsonProcessingException e1) {
throw new AleasException(e1,512);
}
throw new AleasException(message, 1024);
}
}
// result generation
int count = minCount + random.nextInt(maxCount - minCount + 1);
List<Integer> nombres = new ArrayList<Integer>();
for (int i = 0; i < count; i++) {
nombres.add(a + random.nextInt(b - a + 1));
}
// return result
return new AleasMetier(delay,nombres);
}
}
No haremos comentarios sobre la clase: es similar a la que se encuentra en el entorno Swing en la sección 8.4. Simplemente señalaremos los siguientes puntos:
- línea 10: la anotación de Spring [@Service], que hace que Spring instancie la clase como una única instancia (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 cuando se inicia la aplicación web, y los objetos definidos por un archivo de configuración se instancian a continuación, por defecto como una única instancia (singleton). Un singleton de Spring puede contener referencias a otros objetos de Spring. Este es el caso aquí: el singleton [business] (líneas 10-11) tendrá una referencia al singleton [mapper] (líneas 13-14). Esto se denomina inyección de dependencias. Hay dos formas de inyectar un singleton en otro singleton:
- por su tipo: esto es posible si el singleton que se va a inyectar es el único objeto Spring de ese tipo. Este es el caso aquí para la inyección en las líneas 13-14 (tipo ObjectMapper);
- por su nombre, si varios objetos Spring tienen el mismo tipo. En este caso, debe añadir la anotación @Qualifier("singletonName") para especificar el nombre del singleton;
La clase [Metier] lanza excepciones de tipo [AleaException]:
package android.exemples.server.metier;
public class AleaException extends RuntimeException {
// error code
private int code;
// manufacturers
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters and setters
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- línea 3: [AleasException] extiende la clase [RuntimeException]. Por lo tanto, es una excepción no controlada (no es necesario tratarla con un try/catch);
- línea 6: se añade un código de error a la clase [RuntimeException];
9.2.4. El servicio web / JSON
![]() |
![]() |
El servicio web / JSON está implementado mediante Spring MVC. Spring MVC implementa el patrón arquitectónico MVC (Modelo-Vista-Controlador) de la siguiente manera:
![]() |
El procesamiento de una solicitud del 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&... El [Dispatcher Servlet] es la clase de Spring que gestiona las URL entrantes. «Enruta» la URL a la acción que debe procesarla. Estas acciones son métodos de clases específicas denominadas [Controladores]. La C de MVC aquí es la cadena [Servlet Dispatcher, Controlador, Acción]. Si no se ha configurado ninguna acción para gestionar la URL entrante, el [Servlet Dispatcher] responderá que no se ha encontrado la URL solicitada (error 404 NOT FOUND);
- el procesamiento
- la acción seleccionada puede utilizar los parámetros que el [Servlet Dispatcher] le ha pasado. Estos pueden provenir de varias fuentes:
- la ruta [/param1/param2/...] de la URL,
- los parámetros de la URL [p1=v1&p2=v2],
- de los parámetros enviados por el navegador con su solicitud;
- al procesar la solicitud del usuario, la acción puede necesitar la capa [de negocio] [2b]. Una vez procesada la solicitud del cliente, puede desencadenar diversas respuestas. Un ejemplo clásico es:
- una página de error si la solicitud no se ha podido procesar correctamente
- una página de confirmación en caso contrario
- la acción indica que se muestre una vista específica [3]. Esta vista mostrará datos conocidos como el modelo de vista. Esta es la M de MVC. La acción creará este modelo M [2c] e indicará que se muestre una vista V [3];
- respuesta: la vista V seleccionada utiliza el modelo M construido 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 esta respuesta.
En el caso de un servicio web / JSON, la arquitectura anterior se modifica ligeramente:
![]() |
- en [4a], el modelo, que es una clase Java, se convierte 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 de tipo [AleasResponse] de la siguiente manera:
package dvp.rxjava.server.web;
import dvp.rxjava.server.metier.AleasMetier;
public class AleasResponse extends AleasMetier {
// error code
private int erreur;
// error message
private String message;
// manufacturers
public AleasResponse() {
}
public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
super(aleasMetier);
this.erreur = erreur;
this.message = message;
}
// getters and setters
public void setAleasMetier(AleasMetier aleasMetier) {
this.setDelay(aleasMetier.getDelay());
this.setAleas(aleasMetier.getAleas());
}
...
}
- 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 error != 0, un mensaje de error; null si no hay error;
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 {
// business layer
@Autowired
private IMetier metier;
@Autowired
private ObjectMapper mapper;
// random numbers in [a,b]
// n numbers are generated with n in the range [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is a random number in the range [minDelay, maxDelay]
@RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
@PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
@PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {
// we prepare the answer
AleasResponse response = new AleasResponse();
// the business layer is used to generate random numbers
try {
response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
} catch (AleasException e) {
// case of error (code and message)
response.setErreur(e.getCode());
response.setMessage(e.getMessage());
}
// we return the answer jSON
return mapper.writeValueAsString(response);
}
}
- línea 16: la anotación [@Controller] convierte a la clase [AleasController] en un singleton de Spring. También indica que la clase contiene métodos que gestionarán las solicitudes de determinadas URL en la aplicación web. Aquí solo hay uno, en la línea 29;
- líneas 20–21: la anotación [@Autowired] indica a Spring que inyecte un componente de tipo [IMetier] en el campo. Este será la clase [Metier] anterior. Como le hemos añadido la anotación [@Service], se trata como un componente de Spring;
- líneas 22-23: la anotación [@Autowired] indica a Spring que inyecte un componente de tipo [ObjectMapper] en el campo. Definiremos esto en breve;
- línea 31: el método [getAleas] genera números aleatorios. Su nombre es irrelevante. Cuando se ejecuta, los parámetros de las líneas 31–33 han sido inicializados por Spring MVC. Veremos cómo. Además, se ejecuta porque el servidor web recibió una solicitud HTTP GET para la 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 que será la cadena JSON de tipo [AleasResponse];
- Línea 29: La URL procesada tiene el formato /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, donde {x} representa una variable. Estas diversas variables se asignan a los parámetros del método en las líneas 32–33. Esto se hace mediante la anotación @PathVariable("x"). Tenga en cuenta que los valores de {x} son componentes de una URL y, por lo tanto, son de tipo String. La conversión de String al tipo de parámetro del método puede fallar. Spring MVC lanza entonces una excepción. En resumen: si solicito la URL /100/200/10/20/300/400 en un navegador, 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: solicitamos una lista de números aleatorios a la capa [business]. Recuerda que el método [business].getRandom puede lanzar una excepción;
- líneas 42–43: gestión de errores;
- línea 46: la respuesta [AleasResponse] se devuelve como una cadena JSON;
9.2.5. Configuración del proyecto Spring
![]() |
Hay varias formas de configurar Spring:
- utilizando archivos XML;
- con código Java;
- utilizando una combinación de ambos;
Hemos optado por configurar nuestra aplicación web utilizando código Java. La clase [Config] anterior 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 {
// -------------------------------- layer configuration [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mapper jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- 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 con [@Controller];
- línea 16: la anotación [@EnableWebMvc] activa las 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 Spring utilizando métodos anotados con [@Bean]. El resultado del método se convierte entonces en un objeto Spring;
- líneas 22-26: definición del servlet del marco Spring MVC, que redirige las solicitudes HTTP al controlador y al método correctos. [DispatcherServlet] es una clase de Spring;
- líneas 28-31: esto especifica que este servlet gestiona todas las URL;
- líneas 33–36: la presencia de este bean activará el servidor Tomcat incluido en los archivos del proyecto. Escuchará 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 desde la siguiente clase ejecutable [Application]:
package android.exemples.server.boot;
import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
// application execution
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, es la clase [Config] que acabamos de describir. El segundo parámetro es la matriz de argumentos que se pasan al método [main] (línea 7). En este caso, no habrá argumentos;
Para la ejecución real, se invita al lector a consultar la sección 9.2.1.
9.3. El cliente Android
Nota: El siguiente proyecto de Android es bastante complejo. Requiere un conocimiento sólido de Android, que se puede encontrar, por ejemplo, en [Introducción a la programación de tabletas Android con Android Studio].
ActivityViewsLayer[DAO]UserServer
El cliente tendrá dos componentes:
- una capa [Presentación] (vistas + actividad);
- una capa [DAO] que se comunica con el servicio [web / JSON] que hemos estudiado anteriormente.
9.3.1. RxAndroid
Para comunicarse de forma asíncrona con el servidor de números aleatorios, el cliente Android utilizará la biblioteca RxAndroid. Esta biblioteca amplía RxJava al ecosistema Android. Al igual que hicimos con la aplicación Swing, utilizaremos solo una única extensión proporcionada por RxAndroid: el programador [AndroidSchedulers.mainThread()]. Una interfaz gráfica de usuario (GUI) de Android sigue 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 desencadena acciones asíncronas, los resultados de dichas acciones deben recuperarse en el hilo de la interfaz de usuario si se van a utilizar para actualizar la interfaz de usuario;
El cliente de Android:
- enviará múltiples solicitudes asíncronas al servidor de números aleatorios. Estas solicitudes se ejecutarán en el lado del cliente utilizando los subprocesos del programador [Schedulers.io()];
- Estas solicitudes asíncronas devolverán observables que se fusionarán en un único observable;
- Este observable se observará en el lado del cliente en el programador [AndroidSchedulers.mainThread()] proporcionado por RxAndroid;
9.3.2. El proyecto IntelliJ IDEA
El proyecto de Android se llama [client]:
![]() | ![]() ![]() |
Se ejecutará a través de [2].
Nota: La ejecución depende en gran medida de la configuración del IDE IntelliJ IDEA que se utilice. Es probable que la ejecución [2] anterior no funcione a la primera en un equipo distinto al mío. Configurar correctamente el IDE IntelliJ IDEA para ejecutar este proyecto puede resultar una tarea abrumadora para los principiantes. A continuación se indican algunos puntos a tener en cuenta:
- en [3], acceda a la estructura del proyecto;
![]() | ![]() |
- en [4-5], el JDK y los SDK de Android instalados en mi equipo. Tenga en cuenta que el JDK 1.8 no es imprescindible. Android no admite ciertas características de Java 8, incluidas las lambdas. Por lo tanto, para instanciar interfaces funcionales, utilizaremos clases anónimas. Por lo tanto, basta con un JDK 1.6. Sin embargo, el proyecto tal y como se distribuye ha sido configurado con un JDK 1.8;
El archivo [build.gradle] [6] que configura el proyecto de Android es el siguiente:
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
// replace with the current version of the Android plugin
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
jcenter()
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "android.aleas"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
Dependiendo de los SDK de Android disponibles, es posible que haya que modificar las versiones que aparecen en 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:
- SDK API 23 [2];
- Herramientas de compilación del SDK 23.0.3 [3];
- Herramienta SDK 25.1.3 [4]
Por último, comprueba la ruta del SDK de Android en el archivo [local.properties] [4], en la línea 11 que aparece a continuación:
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Apr 07 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. Ejecución del proyecto IntelliJ IDEA
Una vez creado el entorno adecuado para el proyecto, se puede ejecutar de la siguiente manera:
![]() | ![]() | ![]() |
- En [1], inicie el emulador de Android Genymotion;
- en [2], ejecute la configuración de ejecución [app];
- en [3], para crear una configuración de ejecución;
![]() |
- En [1, 3], la configuración se denominó [app];
- en [2], corresponde a la ejecución del módulo denominado [app];
- en [4], especificamos que, durante la ejecución, el IDE debe ofrecernos un dispositivo de ejecución. En este caso, siempre será el emulador Genymotion;
- en [5], especificamos que este dispositivo debe utilizarse para todas las ejecuciones de la configuración;
La ejecución del proyecto en el emulador Genymotion comienza con el siguiente comando inicial:

Para saber qué introducir en [1], abre una ventana de comandos de 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
Introduce [1] para una de las direcciones IP de tu equipo (líneas 20, 28, 32). Si tienes un cortafuegos de Windows, es probable que tengas que desactivarlo para que el emulador de Android pueda conectarse al servidor de números aleatorios.
Al ejecutar solicitudes asíncronas con la información anterior se obtienen los siguientes resultados:

Cada solicitud devuelve una respuesta JSON con los siguientes campos:
- aleas: los números aleatorios generados por el servidor;
- idClient: el ID de la solicitud;
- on: el subproceso del lado del cliente que ejecuta la solicitud;
- requestAt: hora de la solicitud;
- responseAt: hora en que se recibió la respuesta;
- delay: el tiempo de espera que el servidor observó antes de devolver su respuesta;
- error: un código de error; 0 si no hay error;
- message: un mensaje de error; nulo si no hay error;
- observadoA: momento en que se observó la respuesta;
- observadoEn: subproceso que observa la respuesta. En este caso, siempre será [main], que hace referencia al subproceso de la interfaz de usuario;
Dado que las solicitudes son asíncronas y los tiempos de espera impuestos al servidor son aleatorios, las respuestas se devuelven en un orden disperso.
9.3.4. Las dependencias de Gradle del proyecto
El proyecto requiere dependencias, que especificamos 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 para un proyecto de Android que utiliza el SDK 23;
- La dependencia de la línea 5 incorpora el objeto Spring [RestTemplate], que gestiona la comunicación entre la capa [DAO] y el servidor;
- la dependencia de la línea 6 incorpora la biblioteca JSON [Jackson] utilizada por la aplicación;
- La dependencia de la línea 7 incorpora la biblioteca RxAndroid (y con ella la biblioteca RxJava) que la capa de interfaz de usuario utiliza para comunicarse con la capa [DAO];
9.3.5. El manifiesto de la aplicación Android
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.aleas">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="android.aleas.activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- Línea 5: Se debe permitir el acceso 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 {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
public Observable<AleasDaoResponse> getAleas(final Request request);
// URL of the web service
public void setUrlServiceWebJson(String url);
// max wait time (ms) for server response to connection request
// max wait time (ms) for server response to a request
public void setClientTimeouts(int connectTimeout, int readTimeOut);
}
- línea 12: el método de la capa [DAO] que genera números aleatorios de forma asíncrona;
- línea 15: para proporcionar a la implementación [DAO] la URL del servicio de generación de números aleatorios;
- línea 19: establecer los tiempos de espera máximos para la implementación [DAO], a fin de evitar esperas excesivamente largas 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 {
// request no
int id;
// user input
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// manufacturers
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// getters and setters
...
}
Aquí podemos ver la mayoría de los parámetros de la URL del servidor que hay que consultar.
El método [getAleas] devuelve un tipo Observable<AleasDaoResponse>, donde la clase [AleasDaoResponse] es la siguiente:
package android.aleas.dao;
import java.util.List;
public class AleasDaoResponse {
// error code
private int erreur;
// error message
private String message;
// server waiting time
private int delay;
// random numbers delivered by the server
private List<Integer> aleas;
// customer status
private ClientState clientState;
// manufacturers
public AleasDaoResponse() {
}
public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
this.erreur = erreur;
this.message = message;
this.delay = delay;
this.aleas = aleas;
this.clientState = clientState;
}
// getters and setters
...
}
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 {
// name of execution thread
private String on;
// query time
private String requestAt;
// response time
private String responseAt;
// customer id
private int idClient;
// manufacturer
public ClientState() {
on = Thread.currentThread().getName();
requestAt = getTimeStamp();
}
public ClientState(int idClient) {
this();
this.idClient = idClient;
}
// private methods
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// getters and setters
...
}
- línea 11: hilo de ejecución de la capa [DAO];
- línea 13: tiempo de solicitud;
- línea 15: tiempo de respuesta;
- línea 17: número de 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 {
// customer REST
private RestTemplate restTemplate;
// URL service
private String urlServiceWebJson;
// mapper jSON
private ObjectMapper mapper;
// manufacturers
public Dao() {
// mapper jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// set the URL of the REST service
this.urlServiceWebJson = urlServiceWebJson;
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
}
}
- línea 22: el objeto [RestTemplate] que se encargará de la comunicación con el servidor de números aleatorios;
- línea 24: la URL del servicio de generación, establecida por el método [setUrlServiceWebJson] en la línea 41;
- línea 27: el mapeador JSON utilizado 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:
// client REST
private RestTemplate restTemplate;
...
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
// on fixe le timeout des requêtes du client REST
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(readTimeOut);
factory.setConnectTimeout(connectTimeout);
restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
- La comunicación del cliente con el servidor web / JSON se gestiona mediante el objeto [RestTemplate] de la línea 2. Aún no lo hemos inicializado. El método [setClientTimeouts] se encarga de ello;
- Línea 8: La clase [HttpComponentsClientHttpRequestFactory] la proporciona la dependencia [spring-android-rest-template]. Nos permitirá establecer los tiempos máximos de espera para la respuesta del servidor (líneas 9–10);
- Línea 11: creamos el objeto [RestTemplate], que servirá como canal de comunicación con el servicio web. Le pasamos como parámetro el objeto [factory] que acabamos de crear;
- línea 12: el diálogo cliente/servidor puede adoptar diversas formas. Los intercambios se producen a través de líneas de texto, y debemos indicar al objeto [RestTemplate] qué hacer con esta línea de texto. Para ello, le proporcionamos convertidores: clases capaces de procesar 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. Basándose en estos encabezados, el objeto [RestTemplate] seleccionará, de entre sus convertidores, el más adecuado para la situación. Aquí, solo tendremos un único convertidor, un convertidor String --> String, lo que significa 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()));
// service execution
return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
@Override
public void call(Subscriber<? super AleasDaoResponse> subscriber) {
try {
// URL of the service: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
// customer information
ClientState clientState = new ClientState(request.getId());
// synchronous http request
String response = executeRestService("get", urlService, null);
// deserialization of jSON server response
AleasServerResponse aleasServerResponse = mapper.readValue(
response,
new TypeReference<AleasServerResponse>() {
});
// mistake?
int erreur = aleasServerResponse.getErreur();
if (erreur != 0) {
// we forward the exception
subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
} else {
// enter the time of reception
clientState.setResponseAt();
// we forward the result to the subscriber
subscriber.onNext(
new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
}
} catch (Exception ex) {
// we forward the exception to the subscriber
subscriber.onError(ex);
} finally {
// we signal the end of the observable
// at runtime, we note that this method has no effect if method [onError] has been called previously - in line with theory - so we could place this instruction only in try
subscriber.onCompleted();
}
}
});
}
- línea 2: recuerda que debemos devolver un tipo [Observable<AleasResponse>];
- línea 3: una línea de registro en la consola de Android;
- línea 5: el objeto [RestTemplate] garantiza la comunicación síncrona con el servidor. Esto significa que el hilo de ejecución que realiza la solicitud queda bloqueado hasta que se recibe la respuesta. En el ejemplo de Swing, vimos cómo transformar una acción síncrona en una asíncrona utilizando el método [Observable.create]. Aquí seguimos el mismo enfoque;
- línea 7: el método [call] de la interfaz [Observable.OnSubscribe<AleasDaoResponse>] de la línea 5. Este método se invoca cuando un observador se suscribe al observable;
- líneas 10-12: construcción de la URL para el servicio de números aleatorios;
- línea 14: inicialización del objeto [ClientState]. Aquí registramos la hora de la solicitud;
- línea 16: solicitud HTTP síncrona. Se devuelve una respuesta JSON. El método [executeRestService] espera tres parámetros:
- el método HTTP que se va a utilizar para consultar el servicio;
- la URL del servicio;
- el objeto que se va a enviar (tipo Object), nulo 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 {
// error code
private int erreur;
// error message
private String message;
// server waiting time
private int delay;
// random numbers
private List<Integer> aleas;
// getters and setters
...
}
- línea 23: recuperar el código de error enviado por el servidor;
- líneas 24-26: si se produce un error, se reenvía una excepción al suscriptor;
- línea 29: actualizamos [clientState], que formará parte de la respuesta enviada al suscriptor;
- líneas 31-33: enviamos la respuesta al suscriptor. Es de tipo [AleasDaoResponse];
- líneas 35-37: gestionamos todos los casos de error sin distinción. El error más probable es un error de red;
- línea 41: notificación del fin de la transmisión;
9.3.7. Vistas de la aplicación
![]() |
![]() |
La aplicación tiene las dos vistas siguientes:
La vista de solicitud

La vista de respuesta

9.3.7.1. La clase [MyFragment]
Hay dos fragmentos:
- [RequestFragment] para la solicitud;
- [ResponseFragment] para la respuesta;
Ambos fragmentos heredan de 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 {
// ------------- data common to all fragments
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- línea 7: la clase [MyFragment] extiende la clase [Fragment] de Android;
- líneas 10-11: datos compartidos por todos los fragmentos;
- línea 10: cada fragmento conoce la única actividad de la aplicación;
- línea 11: para comunicarse entre sí, los fragmentos utilizan una sesión;
- línea 13: antes de mostrar un fragmento, se le pedirá que se actualice con el contenido de la sesión. Este método se declara abstracto porque lo implementan las clases hijas. Por este motivo, la propia clase se declara abstracta (línea 7);
La clase [Session] contiene los datos compartidos por 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 {
// application activity
private MainActivity activity;
// number of requests
private int nbRequests;
// request characteristics
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// URL web service / jSON
private String urlWebJson;
// operation began
private boolean onAir;
// idem but a little later in time
private boolean operationStarted;
// the name of the example chosen by the user from the list of examples
private String exampleName;
// its number in the list of fragments
private int examplePosition;
// the example spinner adapter in the query view
private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
// methods
public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
this.urlWebJson = urlWebJson;
this.exampleName = exampleName;
this.examplePosition = examplePosition;
}
public Request getRequest() {
return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
}
// getters and setters
...
}
El método de la línea 46 crea el objeto [Request], que encapsula toda la información proporcionada por el usuario en la vista de solicitud:
![]() |
package android.aleas.fragments;
public class Request {
// request no
int id;
// user input
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// manufacturers
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// getters and setters
....
}
9.3.7.2. El fragmento [RequestFragment] de la solicitud
El fragmento de solicitud tiene los siguientes componentes:

La aplicación tiene una única vista, que es una vista con dos pestañas:
- [1]: la pestaña de solicitud;
- [2]: la pestaña de 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 del generador 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 números 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 rango [minDelay, maxDelay] |
7 | EditText | edtUrlServiceRest | la URL del servicio de generación de números aleatorios; |
8 | Spinner | spinnerExamples | la lista desplegable de ejemplos. Cada ejemplo ilustra un método específico de la clase [Observable]; |
8 | Botón | btnExecute | el botón que activa las llamadas al servicio de generación de números; |
Se notifican los errores de entrada:

Los componentes del 1 al 6 son componentes [TextView] con los siguientes nombres (por orden): txtErrorRequests, txtErrorInterval, txtErrorCount, txtErrorDelay, txtWebServiceErrorMessage.
9.3.7.3. El fragmento [ResponseFragment] de la respuesta
El fragmento de respuesta consta de los siguientes componentes:

N.º | Tipo | Nombre | Función |
1 | TextView | infoRespuestas | número de respuestas recibidas |
2 | ListView | listResponses | lista de cadenas JSON recibidas del servidor |
3 | Botón | btnCancel | 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">
<!-- application bar -->
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<!-- toolbar -->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
<!-- waiting image -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- tab container -->
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<!-- view container -->
<android.aleas.activity.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:layout_marginBottom="100dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
Los componentes de esta vista son los siguientes:
líneas | Tipo | Nombre | Función |
20-34 | Barra de herramientas | barra de herramientas | barra de herramientas de la aplicación |
29-34 | Barra de progreso | panel de carga | Imagen de marcador de posición 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 {
// swipe control
private boolean isSwipeEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// method redefinition
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe authorized?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe authorized?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- La clase [MyPager] extiende la clase estándar de Android [ViewPager]. Utilizamos la clase [MyPager] en lugar de la clase [ViewPager] únicamente porque queremos desactivar el deslizamiento: por defecto, con la clase [ViewPager], se puede cambiar de pestaña deslizando el dedo (hacia la izquierda o hacia la derecha). Aquí no queremos este comportamiento;
- línea 11: el valor booleano que controla el deslizamiento (líneas 26 y 36);
- líneas 44-46: el método que inicializa 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 {
// layer [DAO]
private IDao dao;
// the session
private Session session;
// manufacturer
public MainActivity() {
// parent
super();
// session
session = new Session();
// DAO
dao = new Dao();
}
// getters
public Session getSession() {
return session;
}
// implémentation IDao ----------------------------------------
@Override
public Observable<AleasDaoResponse> getAleas(Request request) {
return dao.getAleas(request);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
dao.setClientTimeouts(connectTimeout, readTimeOut);
}
}
- 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 ignoren la existencia de la capa [DAO]: sus controladores de eventos se comunicarán con la capa [Activity] cuando necesiten interactuar con el servidor.
- Línea 24: una referencia a la capa [DAO] inicializada por el constructor en la línea 35;
- línea 26: una referencia a la sesión compartida por los fragmentos, inicializada por el constructor en la línea 33;
- líneas 46-59: implementación de la interfaz [IDao];
La clase [MainActivity] inicializa los componentes de su vista asociada de la siguiente manera:
// barre d'outils
private Toolbar toolbar;
// gestionnaire de fragments
private MyPager mViewPager;
// conteneur d'onglets
private TabLayout tabLayout;
// image d'attente
private ProgressBar loadingPanel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
// classique
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// session
session.setActivity(this);
// configuration timeouts de la couche [DAO]
setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
// composants
mViewPager = (MyPager) findViewById(R.id.container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
tabLayout = (TabLayout) findViewById(R.id.tabs);
// toolbar
setSupportActionBar(toolbar);
// au départ on n'a qu'un seul onglet
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Request");
tabLayout.addTab(tab);
// gestionnaire d'évt
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
int position = tab.getPosition();
if (position == 0) {
// onglet requête
showView(0);
} else {
// onglet réponse - dépend de l'exemple choisi
showView(session.getExamplePosition());
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// création des fragments des réponses
createResponseFragments();
// gestion image d'attente
loadingPanel.setVisibility(View.INVISIBLE);
}
Este código es bastante habitual en una actividad. Veamos 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: Creamos la primera pestaña con el título [Request]. En algún momento, tendremos lo siguiente en memoria:
- el fragmento [Request];
- n fragmentos de tipo [ExampleXXFragment];
La primera pestaña siempre mostrará el fragmento [Request]. La segunda pestaña mostrará el fragmento [ExampleXXFragment] correspondiente al ejemplo elegido por el usuario. Por lo tanto, el fragmento 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 actualmente en uso (mostrado). Su número se recupera de la sesión;
- línea 62: crea los fragmentos para todos los ejemplos presentes en el selector de ejemplos de la vista [RequestFragment] (primera pestaña);
- línea 65: la imagen de carga está oculta actualmente;
Para comprender el método [showView] (líneas 43, 46) y el método [createResponseFragments], primero debemos presentar el gestor de fragmentos en memoria (clase incluida en el archivo Java MainActivity):
// fragment manager - must define getItem, getCount methods
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// managed fragments
private MyFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
super(fm);
this.fragments = fragments;
}
// must render fragment no. position
@Override
public MyFragment getItem(int position) {
// the fragment
return fragments[position];
}
// makes the number of fragments to manage
@Override
public int getCount() {
// no. of fragments
return fragments.length;
}
}
}
- La clase [SectionsPagerAdapter] extiende la clase [FragmentPagerAdapter] de Android. Sobrescribe 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. Tenga en cuenta que son de tipo [MyFragment], tal y como se describe en la sección 9.3.7.1;
- línea 8: para construirse a sí misma, 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() {
// spinner examples
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// put the adapter in the session so that the [Request] view can retrieve it
session.setSpinnerExemplesAdapter(adapter);
...
}
- Línea 3: Creamos un adaptador para el spinner de ejemplos, en este caso una lista de cadenas que representan los nombres de los ejemplos. Estos nombres se encuentran en el archivo [layout/exemples.xml]:
![]() |
El archivo [examples.xml] contiene el siguiente código:
<!-- exemples -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
</string-array>
</resources>
Línea 1: Este archivo es el segundo parámetro del método [createFromResource]. En [R.array.examples], [examples] es el nombre de la matriz (véase la línea 3 anterior), no el nombre del archivo.
- Línea 5: Asociamos un diseño (gestor de visualización) al adaptador. Ahora el adaptador tiene tanto los datos como su modo de visualización;
- Línea 7: Añadimos el adaptador a la sesión. Aquí es donde el [RequestFragment] que lo necesita lo recuperará;
Continuemos con el código del método [createResponseFragments]:
private void createResponseFragments() {
// spinner examples
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// put the adapter in the session so that the [Request] view can retrieve it
session.setSpinnerExemplesAdapter(adapter);
// create fragment table (1 query, n responses)
MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
// query fragment
tFragments[0] = new RequestFragment();
// answer fragments
for (int i = 1; i < tFragments.length; i++) {
// we construct the name of the fragment to be instantiated, corresponding to the example chosen by the user
// this name must be the full name with its package - here it is directly associated with the example number in the spinner
String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
// instantiate the fragment associated with the example
MyFragment fragment;
try {
// class instantiation
fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
} catch (Exception e) {
e.printStackTrace();
return;
}
// the fragment has been created - we put it in the table
tFragments[i] = fragment;
}
// instantiation of the fragment manager with these new fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// Set up the ViewPager with the sections adapter.
mViewPager.setAdapter(mSectionsPagerAdapter);
// page navigation - this instruction is important
// here we say that on both sides of the displayed view, we must keep [tFragments.length] views initialized
// this means that all fragments used by the application are in memory and initialized
// if you don't do this then the default [OffscreenPageLimit] is 1
// so if the fragment displayed is no. 3, only fragments 2 and 4 will be initialized
// this is done by calling the [onCreateView] method of these 2 fragments - this means that in this method, you must plan to
// regenerate the visual appearance of the fragment the last time it was used
// there's code that can't stand being run twice - it creates a huge mess and is complex to manage
// here we've chosen to avoid these difficulties - in the logs, we can see that when the application starts, all fragments are created
// and their method [onCreateView] executed - it's never executed again -
mViewPager.setOffscreenPageLimit(tFragments.length);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
}
- 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 fragmento de consulta;
- líneas 13-28: crearemos tantos fragmentos como ejemplos haya. Todos estos fragmentos heredan del fragmento de respuesta [ResponseFragment] e implementan únicamente lo específico del ejemplo: la creación de los valores observados. Estos valores difieren de un ejemplo a otro;
- línea 16: un fragmento de 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 de fragmento del ejemplo en el gestor de fragmentos;
- línea 21: instanciación del fragmento del ejemplo n.º i del spinner:
- Class.forName(exampleName): carga el fragmento en la memoria;
- Class.forName(exampleName).getConstructors()[0]: obtiene una referencia al primer constructor de la clase. La clase ExampleXXFragment solo tiene un constructor. Por lo tanto, se obtendrá una referencia a este constructor;
- Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instancia un objeto de tipo ExampleXXFragment utilizando el constructor del paso anterior. new Object[]{} representa los parámetros pasados a este constructor. Dado que el constructor de la clase ExampleXXFragment no espera ningún parámetro, se pasa una matriz vacía de objetos;
- línea 27: este fragmento se añade a la matriz de fragmentos;
- línea 30: vimos que el constructor del gestor de fragmentos [SectionsPagerAdapter] esperaba como parámetro la matriz de fragmentos que debía gestionar. Ahora se le pasa al constructor;
- línea 22: el contenedor de fragmentos [mViewPager] de la vista asociada a la actividad [MainActivity] se vincula aquí al gestor de fragmentos: el contenedor de fragmentos [mViewPager] muestra los fragmentos del gestor de fragmentos;
- línea 43: lee los comentarios; la instrucción establece esencialmente que todos los fragmentos deben permanecer en el estado en el que los coloca el código, independientemente de qué fragmento se muestre actualmente. Así, cuando volvemos a él, lo encontramos en el estado en el que lo dejamos;
- línea 45: el contenedor de fragmentos [mViewPager] es de tipo [MyPager], lo que deshabilita el deslizamiento;
El método [MainActivity.showView] es el siguiente:
// display view n° [position]
private void showView(int position) {
// refresh the fragment before displaying it
mSectionsPagerAdapter.getItem(position).onRefresh();
// displays the requested view - goes directly to the view (second parameter set to false)
// without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
mViewPager.setCurrentItem(position, false);
}
- línea 3: queremos mostrar el fragmento n.º position;
- línea 4: este fragmento se solicita al gestor de fragmentos y, a continuación, se actualiza. Desde la última vez que se mostró, la sesión puede haber cambiado. Por lo tanto, el fragmento debe inspeccionar la sesión para ver si necesita actualizarse;
- línea 7: el fragmento se muestra mediante el [ViewPager]. Dado que este se ha asociado con el gestor de fragmentos, se mostrará el fragmento n.º [posición], el que acabamos de actualizar en la línea 4;
Terminemos con los dos métodos para gestionar la espera:
public void beginWaiting() {
// gestion image d'attente
loadingPanel.setVisibility(View.VISIBLE);
}
public void cancelWaiting() {
// gestion image d'attente
loadingPanel.setVisibility(View.INVISIBLE);
// fin exécution
session.setOnAir(false);
session.setOperationStarted(false);
}
9.3.7.5. 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 of the web service
private EditText edtUrlServiceRest;
private TextView txtMsgErreurUrlServiceWeb;
// number of requests
private EditText edtNbRequests;
private TextView txtErrorRequests;
// generation interval
private EditText edtA;
private EditText edtB;
private TextView txtErrorIntervalle;
// delay
private EditText edtMinDelay;
private EditText edtMaxDelay;
private TextView txtErrorDelay;
// number of values generated
private EditText edtMinCount;
private EditText edtMaxCount;
private TextView txtErrorCount;
// button
private Button btnExecuter;
// list of answers
private ListView listReponses;
private TextView infoReponses;
// spinner examples
private Spinner spinnerExemples;
// seizures
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// manufacturer
public RequestFragment() {
super();
Log.d("rxjava", "RequestFragment constructor");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d("rxjava", "RequestFragment onCreateView");
// recover activity and session
activity = (MainActivity) getActivity();
session = activity.getSession();
// create the fragment view from its definition XML
View view = inflater.inflate(R.layout.request, container, false);
// components
edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
edtA = (EditText) view.findViewById(R.id.edt_a);
edtB = (EditText) view.findViewById(R.id.edt_b);
txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);
// execute] button
btnExecuter.setVisibility(View.VISIBLE);
btnExecuter.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doExecuter();
}
});
// initially no error messages
txtErrorRequests.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
txtErrorCount.setVisibility(View.INVISIBLE);
txtErrorDelay.setVisibility(View.INVISIBLE);
// spinner examples
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// result
return view;
}
...
}
- Línea 16: La clase [RequestFragment] hereda de la clase [MyFragment] (véase la sección 9.3.7.1);
- líneas 18–42: los componentes visuales del fragmento (véase la sección 9.3.7.2);
- líneas 45–52: entrada del 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. Esto ocurre solo una vez;
- línea 61: el código del método [onCreateView] es estándar. Obsérvese en la línea 102 que el adaptador Spinner de los ejemplos se recupera de la sesión. Obsérvese también en la línea 91 que el clic en el botón [Execute] se gestiona mediante el método [doExecute];
- Líneas 64-65: Los campos [activity] y [session] pertenecen a la clase padre [MyFragment];
El método [doExecute] es el siguiente:
// seizures
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
...
private void doExecuter() {
// valid entries?
if (isPageValid()) {
// we put info in session
session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
// store the URL of the web service
activity.setUrlServiceWebJson(session.getUrlWebJson());
Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
// action in progress
session.setOnAir(true);
// but not started
session.setOperationStarted(false);
// the answer fragment is displayed
activity.selectTab(Constants.VUE_RESPONSE);
// we start waiting
beginWaiting();
}
}
- línea 15: No vamos a comentar el método [ispageValid]. Comprueba la validez de las entradas y devuelve «true» solo si todas son válidas. En este caso, se utilizan para inicializar los campos de las líneas 2 a 9;
- Línea 17: Las distintas entradas se guardan en la sesión:
- [spinnerExemples.getSelectedItem().toString()] es el nombre del ejemplo seleccionado por el usuario y se almacena en [session.exampleName];
- [spinnerExemples.getSelectedItemPosition() + 1] es el ID del fragmento asociado al ejemplo, que ha sido almacenado (el fragmento) por el gestor de fragmentos. Este ID se almacena en [session.examplePosition];
- línea 19: la URL del servicio web / JSON se pasa a la actividad, que a su vez la pasa a la capa [DAO];
- líneas 21–24: ten en cuenta que está a punto de iniciarse una operación;
- línea 26: se mostrará la pestaña de respuesta. Para entender lo que va a pasar, recordemos el código [MainActivity.selectTab]:
// sélection d'un onglet
public void selectTab(int position) {
// il y a au plus 2 onglets
// au départ il n'y en a qu'un, celui de la requête
// si l'onglet demandé est le n° 1 et que celui-ci n'existe pas encore, alors il faut le créer
if (position == 1 && tabLayout.getTabCount() == 1) {
// 1 onglet de +
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Response");
tabLayout.addTab(tab);
}
// on sélectionne par programme l'onglet, ce qui va déclencher l'événement [onTabSelected]
// qui va associer la bonne vue à cet onglet
tabLayout.getTabAt(position).select();
}
- Inicialmente, la actividad solo había creado la pestaña de solicitud (pestaña n.º 0);
- líneas 6-11: creamos la pestaña de respuesta (pestaña n.º 1) si aún no se ha creado;
- línea 14: seleccionamos la posición del número de pestaña (0 o 1). Esto coloca el evento [onTabSelected] en la cola del bucle de eventos de la aplicación de Android;
El controlador del evento [onTabSelected] en [MainActivity] es el siguiente:
@Override
public void onTabSelected(TabLayout.Tab tab) {
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
int position = tab.getPosition();
if (position == 0) {
// onglet requête
showView(0);
} else {
// onglet réponse - dépend de l'exemple choisi
showView(session.getExamplePosition());
}
}
En el caso de la pestaña [Respuesta], se ejecuta la línea 9. Se mostrará el fragmento con el ID [session.getExamplePosition()]. Por ejemplo, para el ejemplo [example-03], el ID almacenado en [session.examplePosition] es 3. A continuación, la línea 10 muestra el fragmento con ID 3. La matriz de fragmentos creada inicialmente por la actividad es [RequestFragment, Example01Fragment, Example02Fragment, Example03Fragment,..]. Por lo tanto, es efectivamente el [Example03Fragment] el que se mostrará. Se muestra mediante el siguiente código:
// display view n° [position]
private void showView(int position) {
// refresh the fragment before displaying it
mSectionsPagerAdapter.getItem(position).onRefresh();
// displays the requested view - goes directly to the view (second parameter set to false)
// without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
mViewPager.setCurrentItem(position, false);
}
Podemos ver 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 {
// list of answers
private ListView listReponses;
private TextView infoReponses;
// button
private Button btnAnnuler;
// mapper jSON
private ObjectMapper mapper;
protected ResponseFragment() {
super();
Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
mapper = new ObjectMapper();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// recover activity and session
activity = (MainActivity) getActivity();
session = activity.getSession();
Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
// create the fragment view from its definition XML
View view = inflater.inflate(R.layout.response, container, false);
// components
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
// cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnAnnuler.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doAnnuler();
}
});
// result
return view;
}
...
// method to be executed (by explicit code) before each fragment display
public void onRefresh() {
...
}
}
- línea 21: la clase [ResponseFragment] hereda de la clase [MyFragment];
- líneas 23–27: los componentes del fragmento;
- líneas 32–36: el constructor se ejecuta solo una vez, durante la creación inicial de los fragmentos de ejemplo por parte de la actividad. Esto se debe a que todos los fragmentos de ejemplo heredan del fragmento [ResponseFragment]. Cuando se instancian, se llama al constructor de su clase padre [ResponseFragment];
- línea 35: inicializa el mapeador JSON de la línea 30 utilizado para mostrar la cadena JSON de una pila de excepciones;
- líneas 38–59: el método [onCreateView] se ejecuta solo una vez, durante la creación inicial de los fragmentos de ejemplo por parte de la actividad. Contiene código estándar que se encuentra en una aplicación de Android;
- líneas 52-56: el método que se ejecuta al pulsar el botón [Cancel] es el método [doCancel];
- líneas 62-64: el método [onRefresh] se ejecuta cada vez que se muestra la pestaña [Respuesta];
Gracias a los diversos registros colocados en métodos clave, podemos ver qué ocurre cuando se inicia la aplicación:
- línea 1: construcción del fragmento [RequestFragment];
- líneas 2–9: construcción de los fragmentos para los 4 ejemplos de la aplicación;
- línea 10: inicialización del fragmento [RequestFragment];
- líneas 11–14: inicialización de los fragmentos para los 4 ejemplos de la aplicación;
Después de eso, no volvemos a ver llamadas a estos métodos.
El método [ResponseFragment.onRefresh] es el siguiente:
// méthode à exécuter (par code explicite) avant chaque visualisation du fragment
public void onRefresh() {
Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
// exécution en cours ?
if (session.isOnAir() && !session.isOperationStarted()) {
// exécution requête
session.setOperationStarted(true);
doExecuter();
}
}
- Línea 5: Comprobamos si el [RequestFragment] ha realizado una solicitud (session.isOnAir) y si se ha iniciado (isOperationStarted). Si el [RequestFragment] ha realizado una solicitud y aún no se está ejecutando, se inicia la operación (líneas 7–8);
- una vez iniciada la operación, dado que es asíncrona, el usuario puede navegar entre las dos pestañas. Si el usuario 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 [doExecute] 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()));
// start waiting
beginWaiting();
// preparation execution
subscriptions.clear();
reponses.clear();
nbInfos = 0;
// create and execute observables for the chosen example
createAndExecuteObservables();
}
// method implemented by child classes
protected abstract void createAndExecuteObservables();
- 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 borra la lista de suscripciones;
- línea 7: se borra la lista que muestra las respuestas;
- línea 8: cuenta el número de respuestas recibidas;
Las clases hijas [ExampleXXFragment] confían al siguiente método [showAlea] la tarea de mostrar los elementos que observan:
protected void showAlea(String data) {
// one more piece of information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// 1 more answer
reponses.add(0, data);
Log.d("rxjava", data);
// maj of UI
listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
- Línea 1: Vemos que el elemento observado llega como una cadena. En realidad, se trata de la cadena JSON del elemento observado. Esto nos permite disponer de un único método para mostrar el elemento observado, independientemente de su tipo Java exacto;
- línea 6: el elemento [data] observado se añade a la primera posición de la lista de respuestas. De este modo, el usuario ve las respuestas más recientes en la parte superior de la lista;
La espera se gestiona mediante los siguientes métodos [beginWaiting] y [cancelWaiting]:
private void beginWaiting() {
// we set the hourglass
activity.beginWaiting();
// the [Cancel] button is displayed
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// end of wait
activity.cancelWaiting();
// the [Cancel] button is hidden
btnAnnuler.setVisibility(View.INVISIBLE);
}
Llaman a los métodos con los mismos nombres en la actividad y simplemente muestran u ocultan el botón [Cancelar].
Al hacer clic en el botón [Cancelar] se ejecuta el siguiente código:
protected void doAnnuler() {
// on annule tous les abonnements
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// fin de l'attente
cancelWaiting();
}
- líneas 3–7: cancelar todas las suscripciones una por una;
9.3.8. Ejemplos de observables
9.3.8.1. Ejemplo-01
Las clases [ExampleXXFragment] están diseñadas para crear, ejecutar y observar observables. Los valores observados se muestran mediante 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 {
// mappers jSON
private ObjectMapper mapperAleasUiResponse;
// manufacturer
public Example01Fragment() {
super();
Log.d("rxjava", "Example01Fragment constructor");
// filters jSON
mapperAleasUiResponse = new ObjectMapper();
}
@Override
public void createAndExecuteObservables() {
Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// observable configuration n° i
// request to server
Request request = session.getRequest();
request.setId(i);
// observable executed on computation thread
observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
}
// observation on event loop thread;
observable = observable.observeOn(AndroidSchedulers.mainThread());
// we execute all these observables
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
...
}, new Action0() {
...
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// extract the information to be displayed
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
- 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 subproceso del programador [Schedulers.io()]. La solicitud HTTP al servidor se ejecutará en este subproceso;
- línea 46: el observable final se observa en el hilo del bucle de eventos;
- líneas 48–57: ejecución de observables, es decir, solicitudes al servidor de números aleatorios. Android aún no es compatible con Java 8 y sus lambdas. Por lo tanto, aquí se utilizan clases anónimas para instanciar las interfaces funcionales de RxJava;
- líneas 49–52: acción ejecutada cuando el observador recibe un nuevo elemento de tipo [AleasDaoResponse] del observable (véase la sección 9.3.6.1);
- línea 51: llamada al método [showAlea] de la clase padre. Recordemos que espera una cadena. Esta es proporcionada por el método [getDataFrom] en las líneas 59–68;
- línea 63: devolvemos la cadena JSON de 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 {
// answer [DAO]
private AleasDaoResponse aleasDaoResponse;
// observation thread
private String observedOn;
// observation time
private String observedAt;
// manufacturers
public AleasUiResponse() {
observedOn = Thread.currentThread().getName();
observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
this.aleasDaoResponse = aleasDaoResponse;
this.observedOn = on;
this.observedAt = at;
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
this();
this.aleasDaoResponse = aleasDaoResponse;
}
// getters and setters
...
}
- A la respuesta de la capa [DAO] (línea 11), añadimos dos datos:
- línea 13: el hilo de observación;
- línea 15: la hora de la observación;
Volvamos al código de suscripción:
@Override
public void createAndExecuteObservables() {
...
// we execute all these observables
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// exception is displayed
showAlea(getMessagesFromThrowable(th));
// after receiving an exception, the observable receives neither onNext nor onCompleted
// forced to cancel the subscription by hand
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}));
}
- líneas 11–18: caso en el que el observador recibe una excepción;
- línea 14: volvemos 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 genera una cadena a partir de una excepción:
// messages d'une exception
protected String getMessagesFromThrowable(Throwable ex) {
// on crée une liste avec les msg d'erreur de la pile d'exceptions
List<String> messages = new ArrayList<String>();
Throwable th = ex;
while (th != null) {
messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
th = th.getCause();
}
try {
return mapper.writeValueAsString(messages);
} catch (IOException e) {
return e.getMessage();
}
}
- línea 11: devuelve la cadena JSON de una lista de mensajes de error (línea 4);
Volvamos al código de suscripción observable:
- líneas 19-25: el código que se ejecuta cuando el observador recibe la notificación de fin de emisión. A continuación, cancelamos la espera (línea 23), lo que actualiza la interfaz gráfica de usuario;
Al ejecutar el ejemplo 01 se obtiene un resultado 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 solicitud (se puede observar que las respuestas llegaron en un orden aleatorio);
- on: el hilo de ejecución del observable que emitió este valor;
- requestAt: hora de la solicitud del cliente;
- responseAt: hora de la respuesta del servidor;
- delay: retraso observado por el servidor;
- error: código de error devuelto por el servidor (0 = sin error);
- message: mensaje de error devuelto por el servidor (null = sin error);
- observadoA: momento en que se observó el valor observado;
- observadoEn: subproceso que observa el valor observado;
9.3.8.2. Ejemplo-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 {
// mappers jSON
private ObjectMapper mapperAleasUiResponse;
// manufacturer
public Example02Fragment() {
super();
Log.d("rxjava", "Example02Fragment constructor");
// filter jSON
mapperAleasUiResponse = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// only observables with an even customer number are kept
observable = observable
.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
})
// execution on I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}));
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// extract the information to be displayed
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
Este ejemplo es similar al anterior (línea 38). Sin embargo, de las observables obtenidas en el ejemplo anterior, solo conservamos aquellas con un número de cliente par (líneas 42-46), utilizando el método [filter] (línea 41).
Los resultados obtenidos son los siguientes (para 10 solicitudes):

9.3.8.3. Ejemplo-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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example03Fragment() {
super();
Log.d("rxjava", "Example03Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<List<Integer>> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).map(new Func1<AleasDaoResponse, List<Integer>>() {
@Override
public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getAleas();
}
})
// execution on I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
subscriptions.add(observable
.subscribe(new Action1<List<Integer>>() {
@Override
public void call(List<Integer> aleas) {
showAlea(getDataFrom(aleas));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}
));
}
private String getDataFrom(List<Integer> aleas) {
// extract the information to be displayed
String data;
try {
data = mapper.writeValueAsString(aleas);
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
Este ejemplo es similar al Ejemplo-02:
- línea 40: definimos los mismos observables que en el Ejemplo-02;
- línea 45: cada valor emitido por los observables anteriores se transforma, utilizando el método [map], en un tipo List<Integer>, que es la lista de números aleatorios generados por el servidor;
- línea 58: el valor observado es ahora de tipo List<Integer>;
El resultado obtenido para 10 solicitudes es el siguiente:

9.3.8.4. Ejemplo-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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example04Fragment() {
super();
Log.d("rxjava", "Example04Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
})
// execution on an I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}
));
}
}
Este ejemplo es similar al Ejemplo-03, salvo que, en lugar de utilizar el método [map] en la línea 42, utilizamos el método [flatMap].
- línea 55: fíjate en que el tipo del valor observado es ahora Integer;
Para 10 solicitudes, obtenemos los siguientes resultados:

En esta ocasión, hay más valores observados que solicitudes.
9.3.8.5. Ejemplo-05
A continuación, describiremos el procedimiento para añadir un nuevo ejemplo de observables a la aplicación.
Supongamos que queremos reproducir el ejemplo [Ejemplo 22h] de la sección 7.6.4:
package dvp.rxjava.observables.exemples;
import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;
public class Exemple22h {
public static void main(String[] args) throws InterruptedException {
// process
Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
// subscriptions
ProcessUtils.subscribe(1, process);
}
}
- 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, a continuación, se combinan en un único observable mediante el método [concatMap] (línea 12);
Paso 1
Creamos un nuevo ejemplo en el archivo [examples.xml]:
![]() |
<!-- exemples -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
<item>Exemple-05</item>
</string-array>
</resources>
Arriba se ha añadido la línea 8. El nombre que se le dé al ejemplo puede ser cualquiera.
Paso 2
Duplica la clase [Example04Fragment] en [Example05Fragment]. En este caso, el nombre es fijo.
Paso 3
Modifica 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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example05Fragment() {
super();
Log.d("rxjava", "Example05Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
// instantiations of functional interfaces
// filter
Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
};
// flatMap
Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
};
// groupBy
Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer integer) {
return integer % 2 == 0;
}
};
// concatMap
Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
return integerIntegerGroupedObservable.asObservable();
}
};
// we ask for the random numbers
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
.groupBy(groupBy).concatMap(concatMap)
// execution on an I/O thread
.subscribeOn(Schedulers.io());
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}
));
}
}
- línea 67: representa el observable del Ejemplo 04: un flujo de enteros;
- línea 68: agrupamos este flujo de enteros según un criterio booleano que 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] producirá elementos de tipo Integer a partir de elementos de tipo GroupedObservable<Boolean, Integer>;
- líneas 32–59: para que la creación del observable en las líneas 67–69 resulte más legible, hemos aislado las instancias de las interfaces funcionales requeridas por 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. Dado un elemento T, la instancia Func1<T,K> se encarga de generar la clave de agrupación K para ese 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. El resultado son dos grupos: el grupo de elementos pares con clave true y el grupo de elementos impares con clave false;
- líneas 53–59: el método [concatMap] espera un parámetro de tipo Func1<T, Observable<R>> y produce un observable de elementos de tipo R. El tipo T aquí es el tipo emitido por el operador [groupBy], en este caso un GroupedObservable<Boolean, Integer>;
- línea 57: a partir del elemento de tipo [GroupedObservable<Boolean, Integer>], generamos un tipo Observable<Integer>. Dado que el operador [groupBy] generó dos grupos, el operador [concatMap] generará dos observables de tipo [Observable<Integer>]. Al igual que [flatMap], los aplanará en un único observable. Pero, a diferencia de [flatMap], no mezcla los elementos de los observables aplanados. Por lo tanto, deberíamos observar dos grupos separados: los números aleatorios pares y los impares.
Paso 4
Ejecutamos la aplicación:

y obtenemos los siguientes resultados:

- en [1], los números aleatorios pares; en [2], los impares;
9.3.8.6. Para continuar
Se invita ahora al lector a crear sus propios ejemplos y también a experimentar con diversos valores para las entradas en el formulario que configura las solicitudes enviadas al servidor de números aleatorios.
9.3.9. Conclusión
Hemos creado la siguiente arquitectura en el entorno Android:
El cliente Android:
![]() |
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:
![]() |
La capa [DAO] realizó n solicitudes HTTP al servidor de números aleatorios, y la capa [swing] esperó de forma asíncrona los resultados de estas solicitudes para mostrarlos. Estas n solicitudes HTTP se realizaron al mismo servidor, que devolvió los mismos tipos de respuestas. Esto nos permitió fusionar las respuestas en un único observable.
En realidad, las aplicaciones de Android se comunican con diferentes servidores, y es probable que no fusionemos sus respuestas. Las solicitudes HTTP a estos servidores se gestionarán de forma independiente entre sí, y sus resultados se observarán utilizando métodos separados.

















































