9. RxJava no ambiente Android
9.1. Introdução
Aqui, vamos revisitar uma aplicação já discutida em vários documentos:
- [Android para programadores JEE: Um modelo assíncrono para clientes Android] (Capítulo 4);
- [Introdução à Programação de Tablets Android Através de Exemplos] (Capítulo 9);
- [Introdução à programação de tablets Android através de exemplos - Versão 2] (Secção 1.11);
Aborda uma aplicação cliente/servidor em que o servidor fornece de forma assíncrona números aleatórios que o cliente Android apresenta:
- No Documento 1, o cliente Android utiliza tecnologia não padrão;
- no Documento 2, o cliente Android utiliza a tecnologia padrão do Android para operações assíncronas;
- No Documento 3, o cliente Android utiliza a mesma tecnologia do Documento 2, mas simplificada através da utilização de anotações da biblioteca Android Annotations;
O cliente Android é o seguinte:
![]() |
A camada [DAO] comunica com o servidor que gera os números aleatórios apresentados pelo tablet Android. Este servidor possui a seguinte arquitetura de duas camadas:
![]() |
Os clientes consultam determinados URLs na camada [web / JSON] e recebem uma resposta de texto no formato JSON (JavaScript Object Notation).
Vamos dividir a análise da aplicação em duas etapas:
O servidor web/JSON
- a sua camada [de negócios];
- o seu serviço [web / JSON] implementado com Spring MVC;
O cliente Android
- a sua camada [DAO];
- a sua atividade;
- as suas vistas;
9.2. O serviço web / JSON
Nota: O serviço web / JSON é implementado utilizando a tecnologia Spring MVC. Os leitores que não estejam familiarizados com esta tecnologia podem:
- ler simplesmente a secção 9.2.1, que explica como iniciar o servidor e como o consultar;
- consultar o documento [Spring MVC e Thymeleaf por Exemplo], em particular o Capítulo 4, que apresenta as principais anotações utilizadas no código;
9.2.1. O projeto IntelliJ IDEA
O serviço web / JSON tem a seguinte arquitetura:
![]() |
Esta arquitetura é implementada pelo seguinte projeto IntelliJ IDEA [1]:
![]() | ![]() |
O servidor é iniciado através de [2-3]. Em seguida, são apresentados os registos da consola:
- Linha 12: indica que o serviço está disponível na porta 8080;
- linha 10: o URL único do serviço web / JSON disponível através de uma operação HTTP GET. Os seus parâmetros são os seguintes:
- [a,b]: intervalo para a geração de números aleatórios;
- [minCount, maxCount]: contagem de números aleatórios gerados, em que count é um número aleatório no intervalo [minCount, maxCount];
- [minDelay, maxDelay]: o serviço aguarda um atraso de milissegundos antes de devolver os números solicitados, sendo que o atraso é um número aleatório no intervalo [minDelay, maxDelay];
Num navegador, vamos solicitar esta URL:
![]() |
Solicitámos:
- números aleatórios no intervalo [100, 200];
- n números aleatórios, em que n está no intervalo [10, 20];
- um tempo de espera de x milissegundos, em que x está no intervalo [300, 400];
Na resposta:
- aleas: lista de números aleatórios gerados;
- delay: o tempo de espera em milissegundos que o servidor definiu;
- error: um código de erro — 0 se não houver erro;
- message: uma mensagem de erro — null se não houver erro;
9.2.2. As dependências Gradle do projeto
![]() |
O projeto [server] é um projeto Gradle configurado pelo seguinte ficheiro [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')
}
- linha 1: um comentário explicando como este ficheiro de configuração foi gerado;
- linhas 4 e 10: uma dependência da estrutura [Spring Boot], um ramo do ecossistema Spring. Esta estrutura [http://projects.spring.io/spring-boot/] permite uma configuração mínima do Spring. Com base nas bibliotecas presentes no classpath do projeto, o [Spring Boot] infere uma configuração plausível ou provável para o projeto. Assim, se o Hibernate estiver no classpath do projeto, o [Spring Boot] inferirá que a implementação JPA utilizada é o Hibernate e configurará o Spring em conformidade. O programador já não precisa de fazer isto. Tudo o que lhe resta fazer é configurar as definições que o [Spring Boot] não configurou por predefinição, ou aquelas que o [Spring Boot] configurou por predefinição, mas que precisam de ser especificadas. Em qualquer caso, a configuração feita pelo programador tem precedência;
- linhas 14–15: dois plugins Gradle necessários para utilizar o conteúdo deste ficheiro Gradle;
- linhas 17–20: definem as características do arquivo gerado para este projeto;
- linhas 22–23: para compatibilidade com Java 8;
- linhas 25–27: as dependências serão procuradas no repositório global do Maven ou no repositório local na máquina;
- linha 30: define uma dependência do artefacto [spring-boot-starter-web]. Este artefacto inclui todos os arquivos necessários para um projeto Spring MVC. Entre estes está o arquivo do servidor Tomcat. Este é o que será utilizado para implementar a aplicação web. Note-se que a versão da dependência não foi especificada. Será utilizada a versão especificada no projeto [spring-boot] importado;
Para atualizar o projeto, deve forçar o download das dependências [1-3]:
![]() | ![]() ![]() |
Vamos ver [4] as dependências incluídas neste ficheiro [build.gradle]:
![]() |
São muitas. O Spring Boot para a Web inclui as dependências de que uma aplicação Web Spring MVC provavelmente irá necessitar. Isto significa que algumas podem ser desnecessárias. O Spring Boot é ideal para um tutorial:
- inclui as dependências de que provavelmente precisaremos;
- veremos que simplifica bastante a configuração do projeto Spring MVC;
- inclui um servidor Tomcat incorporado [1], o que nos poupa de ter de implementar a aplicação num servidor web externo;
- permite-nos gerar um ficheiro JAR executável que inclui todas as dependências acima referidas. Este ficheiro JAR pode ser transferido de uma plataforma para outra sem necessidade de reconfiguração.
Pode encontrar muitos exemplos que utilizam o Spring Boot no site do ecossistema Spring [http://spring.io/guides]. Agora que conhecemos as dependências do projeto, podemos passar ao código.
9.2.3. A camada [de negócios]
![]() |
![]() |
A camada [de negócios] terá a seguinte interface [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 interface é praticamente idêntica à discutida no ambiente Swing na Secção 8.4. Na linha 8, o método [getAleas] devolve o seguinte 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
...
}
O código da classe [Metier] que implementa a interface [IMetier] é o seguinte:
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);
}
}
Não faremos comentários sobre a classe: ela é semelhante à encontrada no ambiente Swing na Secção 8.4. Limitar-nos-emos a assinalar os seguintes pontos:
- linha 10: a anotação Spring [@Service], que faz com que o Spring instancie a classe como uma instância única (singleton) e disponibilize a sua referência a outros componentes Spring. Outras anotações Spring poderiam ter sido usadas aqui para obter o mesmo efeito;
- linhas 13–14: é injetado um mapeador JSON. O Spring é um contentor de objetos. Este contentor é instanciado quando a aplicação web é iniciada e os objetos definidos por um ficheiro de configuração são então instanciados, por predefinição como uma única instância (singleton). Um singleton do Spring pode conter referências a outros objetos do Spring. É o que acontece aqui: o singleton [business] (linhas 10–11) terá uma referência ao singleton [mapper] (linhas 13–14). Isto é chamado de injeção de dependências. Existem duas formas de injetar um singleton noutro singleton:
- pelo seu tipo: isto é possível se o singleton a ser injetado for o único objeto Spring desse tipo. É o que acontece aqui para a injeção nas linhas 13–14 (tipo ObjectMapper);
- pelo seu nome, se vários objetos Spring tiverem o mesmo tipo. Neste caso, deve adicionar a anotação @Qualifier("singletonName") para especificar o nome do singleton;
A classe [Metier] lança exceções do 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;
}
}
- linha 3: [AleasException] estende a classe [RuntimeException]. Trata-se, portanto, de uma exceção não tratada (não é necessário tratá-la com um try/catch);
- linha 6: é adicionado um código de erro à classe [RuntimeException];
9.2.4. O Serviço Web / JSON
![]() |
![]() |
O serviço web / JSON é implementado pelo Spring MVC. O Spring MVC implementa o padrão arquitetónico MVC (Modelo–Visão–Controlador) da seguinte forma:
![]() |
O processamento de um pedido do cliente decorre da seguinte forma:
- solicitação – os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Dispatcher Servlet] é a classe Spring que lida com os URLs recebidos. Ele «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas chamadas [Controllers]. O C em MVC aqui é a cadeia [Dispatcher Servlet, Controller, Action]. Se nenhuma ação tiver sido configurada para tratar a URL recebida, o [Dispatcher Servlet] responderá que a URL solicitada não foi encontrada (erro 404 NOT FOUND);
- o processamento
- a ação selecionada pode utilizar os parâmetros que o [Servlet Dispatcher] lhe passou. Estes podem provir de várias fontes:
- o caminho [/param1/param2/...] da URL,
- os parâmetros da URL [p1=v1&p2=v2],
- dos parâmetros enviados pelo navegador com o seu pedido;
- ao processar a solicitação do utilizador, a ação pode necessitar da camada [de negócios] [2b]. Uma vez processada a solicitação do cliente, ela pode desencadear várias respostas. Um exemplo clássico é:
- uma página de erro, se a solicitação não puder ser processada corretamente
- uma página de confirmação, caso contrário
- a ação instrui que uma vista específica seja exibida [3]. Esta vista exibirá dados conhecidos como o modelo de vista. Este é o M em MVC. A ação criará este modelo M [2c] e instruirá que uma vista V seja exibida [3];
- resposta - a vista V selecionada utiliza o modelo M construído pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.
Para um serviço web / JSON, a arquitetura anterior é ligeiramente modificada:
![]() |
- em [4a], o modelo, que é uma classe Java, é convertido numa cadeia JSON por uma biblioteca JSON;
- em [4b], esta cadeia JSON é enviada para o navegador;
Voltemos à camada [web] da nossa aplicação:
![]() |
Na nossa aplicação, existe apenas um controlador:
![]() |
O serviço web / JSON enviará aos seus clientes uma resposta do tipo [AleasResponse] da seguinte forma:
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());
}
...
}
- linha 5: a classe [AleasResponse] estende a classe [AleasMetier] e, portanto, herda todos os seus atributos (aleas, delay);
- linha 8: um código de erro (0 se não houver erro);
- linha 10: se error != 0, uma mensagem de erro; null se não houver erro;
O controlador [AleasController] é o seguinte:
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);
}
}
- linha 16: a anotação [@Controller] torna a classe [AleasController] um singleton do Spring. Também indica que a classe contém métodos que irão tratar de pedidos para determinados URLs na aplicação web. Aqui, existe apenas um na linha 29;
- linhas 20–21: a anotação [@Autowired] instrui o Spring a injetar um componente do tipo [IMetier] no campo. Esta será a classe [Metier] anterior. Como adicionámos a anotação [@Service] a ela, é tratada como um componente Spring;
- linhas 22–23: a anotação [@Autowired] instrui o Spring a injetar um componente do tipo [ObjectMapper] no campo. Iremos definir isto em breve;
- linha 31: o método [getAleas] gera números aleatórios. O seu nome é irrelevante. Quando é executado, os parâmetros nas linhas 31–33 já foram inicializados pelo Spring MVC. Veremos como. Além disso, ele é executado porque o servidor web recebeu um pedido HTTP GET para a URL na linha 29 (atributo method);
- linha 30: a anotação [@ResponseBody] indica que o resultado do método deve ser enviado tal como está para o cliente. Aqui, enviar-lhe-emos uma string que será a string JSON do tipo [AleasResponse];
- Linha 29: A URL processada tem o formato /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, onde {x} representa uma variável. Estas várias variáveis são atribuídas aos parâmetros do método nas linhas 32–33. Isto é feito através da anotação @PathVariable("x"). Note que os valores {x} são componentes de uma URL e, portanto, são do tipo String. A conversão de String para o tipo de parâmetro do método pode falhar. O Spring MVC lança então uma exceção. Resumindo: se eu solicitar a URL /100/200/10/20/300/400 num navegador, o método getAleas na linha 31 será executado com os parâmetros a=100 (linha 31), b=200 (linha 31), minCount=10 (linha 31), maxCount=20 (linha 32), minDelay=300 (linha 32), maxDelay=400 (linha 33);
- linha 39: solicitamos uma lista de números aleatórios à camada [business]. Lembre-se de que o método [business].getRandom pode lançar uma exceção;
- linhas 42–43: tratamento de erros;
- linha 46: a resposta [AleasResponse] é devolvida como uma cadeia JSON;
9.2.5. Configuração do projeto Spring
![]() |
Existem várias formas de configurar o Spring:
- usando ficheiros XML;
- com código Java;
- utilizando uma combinação de ambos;
Optamos por configurar a nossa aplicação web utilizando código Java. A classe [Config] acima trata desta configuração:
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();
}
}
- Linha 15: Indicamos ao Spring em que pacotes encontrará os objetos a instanciar. Encontrará dois:
- a classe [Metier] anotada com [@Service];
- a classe [AleasController] anotada com [@Controller];
- linha 16: a anotação [@EnableWebMvc] aciona configurações automáticas para a estrutura Spring MVC;
- linhas 19–20: injeção do contexto Spring (recipiente para objetos Spring). Esta injeção é necessária porque o objeto nas linhas 22–26 a requer;
- o ficheiro de configuração do Spring pode definir novos objetos Spring utilizando métodos anotados com [@Bean]. O resultado do método torna-se então um objeto Spring;
- linhas 22–26: definição do servlet do framework Spring MVC, que encaminha os pedidos HTTP para o controlador e método corretos. [DispatcherServlet] é uma classe Spring;
- linhas 28–31: isto especifica que este servlet lida com todas as URLs;
- linhas 33–36: a presença deste bean ativará o servidor Tomcat incluído nos arquivos do projeto. Ele ficará à escuta de solicitações na porta 8080;
- linhas 39–42: um mapeador JSON. É este que foi injetado nos objetos Spring [Metier] e [AleasController];
9.2.6. Executar o servidor web
![]() |
O projeto é executado a partir da seguinte classe executável [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);
}
}
- linha 6: a classe [Application] é uma classe executável (linhas 7–10);
- linha 9: o método estático [SpringApplication.run] é um método do [Spring Boot] (linha 4) que irá iniciar a aplicação. O seu primeiro parâmetro é a classe Java que configura o projeto. Aqui, trata-se da classe [Config] que acabámos de descrever. O segundo parâmetro é a matriz de argumentos passados ao método [main] (linha 7). Aqui, não haverá argumentos;
Para a execução propriamente dita, convidamos o leitor a consultar a secção 9.2.1.
9.3. O Cliente Android
Nota: O projeto Android a seguir é bastante complexo. Requer um conhecimento sólido de Android, que pode ser encontrado, por exemplo, em [Introdução à Programação de Tablets Android com o Android Studio].
ActivityViewsLayer[DAO]UserServer
O cliente terá dois componentes:
- uma camada [Apresentação] (visualizações + atividade);
- uma camada [DAO] que comunica com o serviço [web / JSON] que estudámos anteriormente.
9.3.1. RxAndroid
Para comunicar de forma assíncrona com o servidor de números aleatórios, o cliente Android utilizará a biblioteca RxAndroid. Esta biblioteca estende o RxJava ao ecossistema Android. Tal como fizemos para a aplicação Swing, utilizaremos apenas uma única extensão fornecida pelo RxAndroid: o agendador [AndroidSchedulers.mainThread()]. Uma GUI Android segue as mesmas regras que uma interface Swing:
- os eventos são processados numa única thread chamada de loop de eventos ou thread da interface do utilizador;
- quando um evento desencadeia ações assíncronas, os resultados dessas ações devem ser recuperados na thread da interface do utilizador, caso devam ser utilizados para atualizar a interface do utilizador;
O cliente Android:
- enviará múltiplas solicitações assíncronas para o servidor de números aleatórios. Estas solicitações serão executadas no lado do cliente utilizando as threads do agendador [Schedulers.io()];
- Estas solicitações assíncronas irão devolver observáveis que serão fundidos num único observável;
- Este observável será observado no lado do cliente no agendador [AndroidSchedulers.mainThread()] fornecido pelo RxAndroid;
9.3.2. O projeto IntelliJ IDEA
O projeto Android chama-se [client]:
![]() | ![]() ![]() |
Será executado através de [2].
Nota: A execução depende em grande medida da configuração do IDE IntelliJ IDEA que estiver a ser utilizado. É provável que a execução [2] acima não funcione à primeira tentativa numa máquina que não seja a minha. Configurar corretamente o IDE IntelliJ IDEA para executar este projeto pode ser uma tarefa intimidante para principiantes. Aqui estão alguns pontos a ter em conta:
- em [3], aceda à estrutura do projeto;
![]() | ![]() |
- em [4-5], o JDK e os SDKs do Android instalados na minha máquina. Note que o JDK 1.8 não é essencial. O Android não suporta certas funcionalidades do Java 8, incluindo lambdas. Portanto, para instanciar interfaces funcionais, utilizaremos classes anónimas. Um JDK 1.6 é, portanto, suficiente. No entanto, o projeto tal como distribuído foi configurado com um JDK 1.8;
O ficheiro [build.gradle] [6] que configura o projeto Android é o seguinte:
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'
}
}
Dependendo dos SDKs do Android disponíveis, poderá ser necessário alterar as versões nas linhas 8, 24–25 e 29.
Para instalar novos SDKs do Android, utilize o SDK Manager da seguinte forma [1]:
![]() ![]() | ![]() |
O projeto foi configurado para:
- SDK API 23 [2];
- Ferramentas de compilação SDK 23.0.3 [3];
- Ferramenta SDK 25.1.3 [4]
Por fim, verifique o caminho do SDK do Android no ficheiro [local.properties] [4], linha 11 abaixo:
## 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. Executar o projeto IntelliJ IDEA
Depois de criado um ambiente adequado para o projeto, este pode ser executado da seguinte forma:
![]() | ![]() | ![]() |
- Em [1], inicie o emulador Android Genymotion;
- em [2], execute a configuração de execução [app];
- em [3], para criar uma configuração de execução;
![]() |
- Em [1, 3], a configuração foi denominada [app];
- em [2], corresponde à execução do módulo denominado [app];
- em [4], especificamos que, durante a execução, o IDE nos deve fornecer um dispositivo de execução. Aqui, este será sempre o emulador Genymotion;
- em [5], especificamos que este dispositivo deve ser utilizado para todas as execuções da configuração;
A execução do projeto no emulador Genymotion começa com o seguinte comando inicial:

Para saber o que introduzir em [1], abra uma janela de comando do DOS e digite o seguinte 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
Introduza [1] para um dos endereços IP do seu computador (linhas 20, 28, 32). Se tiver uma firewall do Windows, provavelmente terá de a desativar para que o emulador Android consiga aceder ao servidor de números aleatórios.
A execução de pedidos assíncronos com as informações acima produz os seguintes resultados:

Cada pedido devolve uma resposta JSON com os seguintes campos:
- aleas: os números aleatórios gerados pelo servidor;
- idClient: o ID da solicitação;
- on: o thread do lado do cliente que executa a solicitação;
- requestAt: hora da solicitação;
- responseAt: hora em que a resposta foi recebida;
- delay: o tempo de espera observado pelo servidor antes de devolver a sua resposta;
- error: um código de erro — 0 se não houver erro;
- message: uma mensagem de erro — nulo se não houver erro;
- observedAt: hora em que a resposta foi observada;
- observadoEm: thread que observou a resposta. Aqui, será sempre [main], que se refere à thread da interface do utilizador;
Uma vez que os pedidos são assíncronos e os tempos de espera impostos ao servidor são aleatórios, as respostas são devolvidas numa ordem dispersa.
9.3.4. As dependências Gradle do projeto
O projeto requer dependências, que especificamos no ficheiro [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'
}
- As dependências nas linhas 2–3 são dependências padrão para um projeto Android que utiliza o SDK 23;
- A dependência na linha 5 inclui o objeto Spring [RestTemplate], que gere a comunicação entre a camada [DAO] e o servidor;
- a dependência na linha 6 traz a biblioteca JSON [Jackson] utilizada pela aplicação;
- A dependência na linha 7 importa a biblioteca RxAndroid (e, com ela, a biblioteca RxJava) que a camada UI utiliza para comunicar com a camada [DAO];
9.3.5. O manifesto da aplicação 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>
- Linha 5: O acesso à Internet deve ser permitido;
9.3.6. A camada [DAO]
![]() |
![]() | ![]() |
9.3.6.1. A interface [IDao] da camada [DAO]
A interface da camada [DAO] será a seguinte:
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);
}
- linha 12: o método da camada [DAO] que gera números aleatórios de forma assíncrona;
- linha 15: para fornecer à implementação [DAO] o URL do serviço de geração de números aleatórios;
- linha 19: definir os tempos de espera máximos para a implementação [DAO], para evitar esperas excessivamente longas quando o servidor não responde;
O método [getAleas] recebe todos os seus parâmetros no seguinte 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
...
}
Aqui, podemos ver a maioria dos parâmetros da URL do servidor que precisam de ser consultados.
O método [getAleas] retorna um tipo Observable<AleasDaoResponse>, em que a classe [AleasDaoResponse] é a seguinte:
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
...
}
O tipo [ClientState] é o seguinte:
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
...
}
- linha 11: thread de execução da camada [DAO];
- linha 13: tempo de solicitação;
- linha 15: tempo de resposta;
- linha 17: número da solicitação;
Os campos [on, requestAt, idClient] são inicializados pelo cliente no início da solicitação. O campo [responseAt] é inicializado quando o cliente recebe a resposta do servidor.
9.3.6.2. Implementação da camada [DAO]
![]() |
A interface [IDao] é implementada com a seguinte classe [Dao]:
package android.aleas.dao;
import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class Dao implements IDao {
// customer REST
private RestTemplate restTemplate;
// URL service
private String urlServiceWebJson;
// mapper jSON
private ObjectMapper mapper;
// manufacturers
public Dao() {
// mapper jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// set the URL of the REST service
this.urlServiceWebJson = urlServiceWebJson;
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
}
}
- linha 22: o objeto [RestTemplate] que irá gerir a comunicação com o servidor de números aleatórios;
- linha 24: a URL do serviço de geração — definida pelo método [setUrlServiceWebJson] na linha 41;
- linha 27: o mapeador JSON utilizado para deserializar a cadeia JSON enviada pelo servidor de números aleatórios;
- linhas 30–33: o construtor da classe;
- linha 32: o mapeador JSON da linha 27 é criado;
O método [setClientTimeouts] é o seguinte:
// 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());
}
- A comunicação do cliente com o servidor web / JSON é gerida pelo objeto [RestTemplate] na linha 2. Ainda não o inicializámos. O método [setClientTimeouts] faz isso;
- Linha 8: A classe [HttpComponentsClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Ela permite definir os tempos máximos de espera pela resposta do servidor (linhas 9–10);
- linha 11: construímos o objeto [RestTemplate], que servirá como canal de comunicação com o serviço web. Passamos o objeto [factory] que acabámos de construir como parâmetro para ele;
- linha 12: o diálogo cliente/servidor pode assumir várias formas. As trocas ocorrem através de linhas de texto, e temos de indicar ao objeto [RestTemplate] o que fazer com essa linha de texto. Para tal, fornecemos-lhe conversores — classes capazes de processar linhas de texto. A escolha do conversor é geralmente feita através dos cabeçalhos HTTP que acompanham a linha de texto. Com base nestes cabeçalhos, o objeto [RestTemplate] selecionará, entre os seus conversores, aquele mais adequado à situação. Aqui, teremos apenas um único conversor, um conversor String --> String, o que significa que o tipo String recebido do servidor não sofrerá qualquer transformação.
O método [getAleas] é o mais complexo:
@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();
}
}
});
}
- linha 2: lembre-se de que devemos devolver um tipo [Observable<AleasResponse>];
- linha 3: uma linha de registo na consola do Android;
- linha 5: o objeto [RestTemplate] garante a comunicação síncrona com o servidor. Isto significa que o segmento de execução que faz a solicitação fica bloqueado até que a resposta seja recebida. No exemplo do Swing, vimos como transformar uma ação síncrona numa assíncrona usando o método [Observable.create]. Estamos a seguir a mesma abordagem aqui;
- linha 7: o método [call] da interface [Observable.OnSubscribe<AleasDaoResponse>] da linha 5. Este método é chamado quando um observador subscreve o observável;
- linhas 10–12: construção do URL para o serviço de números aleatórios;
- linha 14: inicialização do objeto [ClientState]. Aqui, registamos a hora do pedido;
- linha 16: pedido HTTP síncrono. É devolvida uma resposta JSON. O método [executeRestService] espera três parâmetros:
- o método HTTP a utilizar para consultar o serviço;
- a URL do serviço;
- o objeto a ser enviado (tipo Object), nulo se o método HTTP não for POST;
- 18-21: Deserialização da cadeia JSON recebida para um tipo [AleasServerResponse]. Este tipo é o seguinte:
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
...
}
- linha 23: recuperar o código de erro enviado pelo servidor;
- linhas 24–26: se ocorrer um erro, é enviada uma exceção ao assinante;
- linha 29: atualizamos [clientState], que fará parte da resposta enviada ao assinante;
- linhas 31–33: enviar a resposta ao assinante. É do tipo [AleasDaoResponse];
- linhas 35–37: tratar todos os casos de erro sem distinção. O erro mais provável é um erro de rede;
- linha 41: notificação do fim da transmissão;
9.3.7. Visualizações da aplicação
![]() |
![]() |
A aplicação tem as duas vistas seguintes:
A vista de solicitação

A vista de resposta

9.3.7.1. A classe [MyFragment]
Existem dois fragmentos:
- [RequestFragment] para o pedido;
- [ResponseFragment] para a resposta;
Ambos os fragmentos estendem a seguinte classe [MyFragment]:
package android.aleas.fragments;
import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;
public abstract class MyFragment extends Fragment {
// ------------- data common to all fragments
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- linha 7: a classe [MyFragment] estende a classe [Fragment] do Android;
- linhas 10–11: dados partilhados por todos os fragmentos;
- linha 10: cada fragmento conhece a única atividade da aplicação;
- linha 11: para comunicarem entre si, os fragmentos utilizam uma sessão;
- linha 13: antes de exibir um fragmento, este será solicitado a atualizar-se com o conteúdo da sessão. Este método é declarado abstrato porque é implementado pelas classes filhas. Por este motivo, a própria classe é declarada abstrata (linha 7);
A classe [Session] contém os dados partilhados pelos vários fragmentos da aplicação. O seu código é o seguinte:
![]() |
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
...
}
O método na linha 46 cria o objeto [Request], que encapsula todas as informações fornecidas pelo utilizador na vista de pedido:
![]() |
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. O fragmento [RequestFragment] da solicitação
O fragmento da solicitação possui os seguintes componentes:

A aplicação tem uma única vista, que é uma vista com dois separadores:
- [1]: o separador de pedido;
- [2]: o separador de resposta;
Os componentes do fragmento [RequestFragment] são os seguintes:
N.º | Tipo | Nome | Função |
3 | EditText | edtNbRequests | número de pedidos a enviar ao serviço de geração de números aleatórios |
4 | EditText | edtA, edtB | os limites [a,b] do intervalo de geração de números; |
5 | EditText | edtMinCount, edtMaxCount | o serviço gera números count, em que count é um número aleatório no intervalo [minCount, maxCount] |
6 | EditText | edtMinDelay, edtMaxDelay | O serviço aguarda delay milissegundos antes de gerar os números, em que delay é um número aleatório no intervalo [minDelay, maxDelay] |
7 | EditText | edtUrlServiceRest | a URL do serviço de geração de números aleatórios; |
8 | Spinner | spinnerExamples | a lista suspensa de exemplos. Cada exemplo ilustra um método específico da classe [Observable]; |
8 | Botão | btnExecute | o botão que aciona chamadas para o serviço de geração de números; |
Os erros de entrada são comunicados:

Os componentes 1 a 6 são componentes [TextView] com os seguintes nomes (por ordem): txtErrorRequests, txtErrorInterval, txtErrorCount, txtErrorDelay, txtWebServiceErrorMessage.
9.3.7.3. O fragmento [ResponseFragment] da resposta
O fragmento de resposta tem os seguintes componentes:

N.º | Tipo | Nome | Função |
1 | TextView | infoRespostas | número de respostas recebidas |
2 | ListView | listResponses | lista de cadeias JSON recebidas do servidor |
3 | Botão | btnCancel | para cancelar pedidos ao servidor |
9.3.7.4. A atividade Android [MainActivity]
![]() |
![]() |
A classe [MainActivity] apresenta a seguinte 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>
Os componentes desta vista são os seguintes:
linhas | Tipo | Nome | Função |
20-34 | Barra de ferramentas | barra de ferramentas | barra de ferramentas da aplicação |
29-34 | Barra de Progresso | painel de carregamento | imagem de preenchimento exibida enquanto o pedido do utilizador está a ser processado |
37-40 | TabLayout | separadores | a barra de separadores da aplicação |
44-51 | MyPager | recipiente | o contentor no qual os vários fragmentos da aplicação são apresentados |
A classe [MyPager] é a seguinte:
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;
}
}
- A classe [MyPager] estende a classe padrão [ViewPager] do Android. Utilizamos a classe [MyPager] em vez da classe [ViewPager] apenas porque queremos desativar o deslizar: por predefinição, com a classe [ViewPager], é possível alternar entre separadores deslizando (para a esquerda ou para a direita). Aqui, não queremos este comportamento;
- linha 11: o booleano que controla o deslizar (linhas 26 e 36);
- linhas 44–46: o método que inicializa o campo na linha 11;
O esqueleto da atividade Android [MainActivity] é o seguinte:
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);
}
}
- Linha 21: A classe [MainActivity] estende a classe padrão do Android [AppCompatActivity]. É, portanto, uma atividade padrão do Android;
- linha 21: a classe [MainActivity] implementa a interface [IDao];
Voltando à arquitetura da aplicação:
![]() |
o facto de a atividade implementar a interface da camada [DAO] permite que as vistas permaneçam alheias à camada [DAO]: os seus manipuladores de eventos comunicarão com a camada [Activity] quando precisarem de interagir com o servidor.
- Linha 24: uma referência à camada [DAO] inicializada pelo construtor na linha 35;
- linha 26: uma referência à sessão partilhada pelos fragmentos, inicializada pelo construtor na linha 33;
- linhas 46–59: implementação da interface [IDao];
A classe [MainActivity] inicializa os componentes da sua vista associada da seguinte forma:
// 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 é bastante comum numa atividade. Vamos explicar alguns pontos:
- A linha 19 faz referência à seguinte classe [Constants]:
package android.aleas.activity;
abstract public class Constants {
final static public int VUE_REQUEST = 0;
final static public int VUE_RESPONSE = 1;
final static public int CONNECT_TIMEOUT = 1000;
final static public int READ_TIMEOUT = 6000;
final static public int DELAY_MAX = 5000;
final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
- Linhas 31–33: Criamos o primeiro separador com o título [Request]. A certa altura, teremos o seguinte na memória:
- o fragmento [Request];
- n fragmentos do tipo [ExampleXXFragment];
A primeira guia exibirá sempre o fragmento [Request]. A segunda guia exibirá o fragmento [ExampleXXFragment] correspondente ao exemplo escolhido pelo utilizador. O fragmento exibido pela segunda guia muda, portanto, ao longo do tempo;
- linhas 37–48: o código executado quando o utilizador clica numa das guias;
- linha 43: o fragmento n.º 0 é apresentado;
- linha 46: o fragmento atualmente em uso (exibido) é mostrado. O seu número é recuperado da sessão;
- linha 62: cria os fragmentos para todos os exemplos presentes no seletor de exemplos na vista [RequestFragment] (1.ª guia);
- linha 65: a imagem de carregamento está atualmente oculta;
Para compreender o método [showView] (linhas 43, 46) e o método [createResponseFragments], devemos primeiro apresentar o gestor de fragmentos na memória (classe incluída no ficheiro 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;
}
}
}
- A classe [SectionsPagerAdapter] estende a classe [FragmentPagerAdapter] do Android. Ela substitui dois métodos da sua classe pai:
- o método [getItem], linha 15;
- o método [getCount], linha 22;
- A classe [SectionsPagerAdapter] contém todos os fragmentos da aplicação. Estes estão armazenados na linha 5. Note-se que são do tipo [MyFragment], conforme descrito na secção 9.3.7.1;
- linha 8: para se construir, a classe [SectionsPagerAdapter] recebe os fragmentos que deve gerir;
- linhas 14–18: o método [getItem] devolve o fragmento na posição [position];
- linhas 21–25: o método [getCount] devolve o número total de fragmentos;
O método [createResponseFragments] cria todos os fragmentos necessários à aplicação:
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);
...
}
- Linha 3: Criamos um adaptador para o spinner de exemplos, neste caso uma lista de Strings que representam os nomes dos exemplos. Estes nomes estão presentes no ficheiro [layout/exemples.xml]:
![]() |
O ficheiro [examples.xml] contém o seguinte 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>
Linha 1: Este ficheiro é o segundo parâmetro do método [createFromResource]. Em [R.array.examples], [examples] é o nome da matriz (ver linha 3 acima), não o nome do ficheiro.
- Linha 5: Associamos um layout (gestor de visualização) ao adaptador. Agora, o adaptador tem tanto os dados como o seu modo de visualização;
- Linha 7: Adicionamos o adaptador à sessão. É aqui que o [RequestFragment] que dele necessita irá recuperá-lo;
Vamos continuar com o código do 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);
}
- linha 9: criação da matriz que irá conter todos os fragmentos da aplicação;
- linha 11: o primeiro fragmento é o fragmento de consulta;
- linhas 13–28: vamos criar tantos fragmentos quantos forem os exemplos. Todos estes fragmentos estendem o fragmento de resposta [ResponseFragment] e implementam apenas o que é específico do exemplo: a criação dos valores observados. Estes valores diferem de um exemplo para outro;
- linha 16: um fragmento de exemplo tem um nome padrão: ExampleXXFragment, em que XX é a sua posição no spinner de exemplos mais 1. XX é também o número do fragmento do exemplo no gestor de fragmentos;
- linha 21: instanciação do fragmento do exemplo #i a partir do spinner:
- Class.forName(exampleName): carrega o fragmento na memória;
- Class.forName(exampleName).getConstructors()[0]: obtém uma referência ao primeiro construtor da classe. A classe ExampleXXFragment tem apenas um construtor. Portanto, será obtida uma referência a este construtor;
- Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instancia um objeto do tipo ExampleXXFragment utilizando o construtor da etapa anterior. new Object[]{} representa os parâmetros passados a este construtor. Uma vez que o construtor da classe ExampleXXFragment não espera quaisquer parâmetros, é passado um array vazio de objetos;
- linha 27: este fragmento é adicionado à matriz de fragmentos;
- linha 30: vimos que o construtor do gestor de fragmentos [SectionsPagerAdapter] esperava a matriz de fragmentos que iria gerir como parâmetro. Passamo-la agora para o construtor;
- linha 22: o contentor de fragmentos [mViewPager] da vista associada à atividade [MainActivity] é ligado aqui ao gestor de fragmentos: o contentor de fragmentos [mViewPager] exibe os fragmentos do gestor de fragmentos;
- linha 43: leia os comentários — a instrução basicamente afirma que todos os fragmentos devem permanecer no estado em que o código os coloca, independentemente de qual fragmento esteja atualmente a ser exibido. Assim, quando voltamos a ele, encontramo-lo no estado em que o deixámos;
- linha 45: o contêiner de fragmentos [mViewPager] é do tipo [MyPager], o que desativa o deslizar;
O método [MainActivity.showView] é o seguinte:
// 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);
}
- linha 3: queremos exibir o fragmento #position;
- linha 4: este fragmento é solicitado ao gestor de fragmentos e, em seguida, atualizado. Desde a última vez que foi exibido, a sessão pode ter mudado. O fragmento deve, portanto, inspecionar a sessão para verificar se precisa de ser atualizado;
- linha 7: o fragmento é exibido pelo [ViewPager]. Uma vez que este foi associado ao gestor de fragmentos, o fragmento #[posição] será exibido — aquele que acabámos de atualizar na linha 4;
Vamos terminar com os dois métodos para gerir a 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. O fragmento [RequestFragment]
A classe [RequestFragment] é a seguinte:
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;
}
...
}
- Linha 16: A classe [RequestFragment] estende a classe [MyFragment] (ver Secção 9.3.7.1);
- linhas 18–42: os componentes visuais do fragmento (ver secção 9.3.7.2);
- linhas 45–52: entrada do utilizador no formulário;
- o construtor (linhas 55–58) e o método [onCreateView] são executados quando a atividade [MainActivity] cria todos os fragmentos na aplicação. Isto acontece apenas uma vez;
- linha 61: o código do método [onCreateView] é padrão. Repare que, na linha 102, o adaptador spinner dos exemplos é recuperado da sessão. Repare também que, na linha 91, o clique no botão [Execute] é tratado pelo método [doExecute];
- Linhas 64–65: Os campos [activity] e [session] pertencem à classe pai [MyFragment];
O método [doExecute] é o seguinte:
// 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();
}
}
- linha 15: Não iremos comentar o método [ispageValid]. Este verifica a validade das entradas e retorna true apenas se todas forem válidas. Neste caso, são utilizadas para inicializar os campos nas linhas 2–9;
- Linha 17: As várias entradas são guardadas na sessão:
- [spinnerExemples.getSelectedItem().toString()] é o nome do exemplo selecionado pelo utilizador e é armazenado em [session.exampleName];
- [spinnerExemples.getSelectedItemPosition() + 1] é o ID do fragmento associado ao exemplo, que foi armazenado (o fragmento) pelo gestor de fragmentos. Este ID é armazenado em [session.examplePosition];
- linha 19: o URL do serviço web / JSON é passado para a atividade, que, por sua vez, o passa para a camada [DAO];
- linhas 21–24: note que uma operação está prestes a começar;
- linha 26: o separador de resposta será exibido. Para compreender o que irá acontecer, recorde o 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, a atividade tinha criado apenas a guia de solicitação (guia n.º 0);
- linhas 6–11: criam a guia de resposta (guia n.º 1) caso ainda não tenha sido criada;
- linha 14: selecionamos a posição do número da guia (0 ou 1). Isto coloca o evento [onTabSelected] na fila do ciclo de eventos da aplicação Android;
O manipulador do evento [onTabSelected] em [MainActivity] é o seguinte:
@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());
}
}
No caso do separador [Resposta], é executada a linha 9. Será apresentado o fragmento com o ID [session.getExamplePosition()]. Por exemplo, no caso do [example-03], o ID armazenado em [session.examplePosition] é 3. A linha 10 exibe então o fragmento com ID 3. A matriz de fragmentos inicialmente criada pela atividade é [RequestFragment, Example01Fragment, Example02Fragment, Example03Fragment,..]. Portanto, é de facto o [Example03Fragment] que será exibido. É exibido pelo seguinte 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 o fragmento será atualizado (linha 4) antes de ser exibido (linha 7).
9.3.7.6. O fragmento [ResponseFragment]
A classe [ResponseFragment] apresenta respostas do servidor. O seu código é o seguinte:
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() {
...
}
}
- linha 21: a classe [ResponseFragment] estende a classe [MyFragment];
- linhas 23–27: os componentes do fragmento;
- linhas 32–36: o construtor é executado apenas uma vez, durante a criação inicial dos fragmentos de exemplo pela atividade. Isto porque todos os fragmentos de exemplo estendem o fragmento [ResponseFragment]. Quando são instanciados, o construtor da sua classe pai [ResponseFragment] é chamado;
- linha 35: inicializa o mapeador JSON da linha 30, utilizado para apresentar a cadeia JSON de uma pilha de exceções;
- linhas 38–59: o método [onCreateView] é executado apenas uma vez, durante a criação inicial dos fragmentos de exemplo pela atividade. Contém código padrão encontrado numa aplicação Android;
- linhas 52–56: o método executado quando o botão [Cancel] é clicado é o método [doCancel];
- linhas 62–64: o método [onRefresh] é executado sempre que o separador [Response] é apresentado;
Graças aos vários registos colocados em métodos-chave, podemos ver o que acontece quando a aplicação é iniciada:
- linha 1: construção do fragmento [RequestFragment];
- linhas 2–9: construção dos fragmentos para os 4 exemplos na aplicação;
- linha 10: inicialização do fragmento [RequestFragment];
- linhas 11–14: inicialização dos fragmentos para os 4 exemplos na aplicação;
Depois disso, nunca mais vemos chamadas a estes métodos.
O método [ResponseFragment.onRefresh] é o seguinte:
// 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();
}
}
- Linha 5: Verificamos se o [RequestFragment] efetuou um pedido (session.isOnAir) e se este já foi iniciado (isOperationStarted). Se o [RequestFragment] tiver efetuado um pedido e este ainda não estiver a ser executado, a operação é iniciada (linhas 7–8);
- assim que a operação é iniciada, uma vez que é assíncrona, o utilizador pode navegar entre os dois separadores. Se o utilizador voltar ao separador [Response] e houver uma operação em curso, as linhas 7–8 não são executadas;
O método [doExecute] na linha 8 executa a operação solicitada pelo utilizador:
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();
- linha 10: cria, executa e observa observáveis. Estes são diferentes para cada exemplo. É por isso que o método [createAndExecuteObservables] é abstrato (linha 14). Será implementado pelos fragmentos [ExampleXXFragment] que estendem a classe [ResponseFragment];
- linha 6: a lista de subscrições é limpa;
- linha 7: a lista que exibe as respostas é limpa;
- linha 8: conta o número de respostas recebidas;
As classes filhas [ExampleXXFragment] confiam ao método [showAlea] a tarefa de exibir os elementos que observam:
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));
}
- Linha 1: Vemos que o elemento observado chega como uma string. Na verdade, trata-se da string JSON do elemento observado. Isto permite-nos ter um único método para apresentar o elemento observado, independentemente do seu tipo Java exato;
- linha 6: o elemento [data] observado é adicionado à primeira posição da lista de respostas. O utilizador vê, assim, as respostas mais recentes no topo da lista;
A espera é gerida pelos seguintes métodos [beginWaiting] e [cancelWaiting]:
private void beginWaiting() {
// we set the hourglass
activity.beginWaiting();
// the [Cancel] button is displayed
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// end of wait
activity.cancelWaiting();
// the [Cancel] button is hidden
btnAnnuler.setVisibility(View.INVISIBLE);
}
Chamam os métodos com os mesmos nomes na atividade e simplesmente mostram ou ocultam o botão [Cancelar].
O clique no botão [Cancelar] é tratado pelo seguinte código:
protected void doAnnuler() {
// on annule tous les abonnements
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// fin de l'attente
cancelWaiting();
}
- linhas 3–7: cancelar todas as subscrições uma a uma;
9.3.8. Exemplos de observáveis
9.3.8.1. Exemplo-01
As classes [ExampleXXFragment] foram concebidas para criar, executar e observar observáveis. Os valores observados são apresentados pela classe pai [ResponseFragment].
A classe [Example01Fragment] é a seguinte:
![]() |
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;
}
}
- linha 36: o único observável que será gerado;
- linhas 37–44: geração e configuração dos vários observáveis que são mesclados (linha 43) no observável da linha 36;
- linha 43: o observável é executado numa thread do agendador [Schedulers.io()]. O pedido HTTP ao servidor será executado nesta thread;
- linha 46: o observável final é observado na thread do ciclo de eventos;
- linhas 48–57: execução de observáveis, ou seja, pedidos ao servidor de números aleatórios. O Android ainda não suporta o Java 8 e as suas lambdas. Por isso, são utilizadas aqui classes anónimas para instanciar as interfaces funcionais do RxJava;
- linhas 49–52: ação executada quando o observador recebe um novo elemento do tipo [AleasDaoResponse] do observável (ver secção 9.3.6.1);
- linha 51: chamada ao método [showAlea] da classe pai. Recorde-se que este espera uma string. Esta é fornecida pelo método [getDataFrom] nas linhas 59–68;
- linha 63: devolvemos a string JSON do tipo [AleasUiResponse] da seguinte forma:
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
...
}
- À resposta da camada [DAO] (linha 11), adicionamos duas informações:
- linha 13: o segmento de observação;
- linha 15: o tempo de observação;
Voltemos ao código de subscrição:
@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();
}
}));
}
- linhas 11–18: caso em que o observador recebe uma exceção;
- linha 14: usamos novamente o método [showAlea] da classe pai para exibir a exceção. O método [getMessagesFromThrowable] é um método da classe pai [ResponseFragment] que gera uma string a partir de uma exceção:
// 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();
}
}
- linha 11: devolve a cadeia JSON de uma lista de mensagens de erro (linha 4);
Voltemos ao código de subscrição observável:
- linhas 19–25: o código executado quando o observador recebe a notificação de fim de emissão. Em seguida, cancelamos a espera (linha 23), o que atualiza a GUI;
A execução do Exemplo 01 produz um resultado semelhante ao seguinte:

Cada elemento da lista é a cadeia JSON de um valor observado. Os campos da cadeia JSON são os seguintes:
- aleas: a lista de números aleatórios fornecida pelo servidor;
- idClient: o número do pedido (pode ver-se que as respostas chegaram numa ordem aleatória);
- on: o thread de execução do observável que emitiu este valor;
- requestAt: hora da solicitação do cliente;
- responseAt: hora da resposta do servidor;
- delay: atraso observado pelo servidor;
- error: código de erro devolvido pelo servidor (0 = sem erro);
- message: mensagem de erro devolvida pelo servidor (null = sem erro);
- observedAt: momento em que o valor observado foi registado;
- observadoEm: thread que observou o valor observado;
9.3.8.2. Exemplo-02
A classe [Example02Fragment] é a seguinte:
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 exemplo é semelhante ao anterior (linha 38). No entanto, das observáveis obtidas no exemplo anterior, mantemos apenas aquelas com um número de cliente par (linhas 42–46), utilizando o método [filter] (linha 41).
Os resultados obtidos são os seguintes (para 10 pedidos):

9.3.8.3. Exemplo-03
A classe [Example03Fragment] é a seguinte:
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 exemplo é semelhante ao Exemplo-02:
- linha 40: definimos os mesmos observáveis que no Exemplo-02;
- linha 45: cada valor emitido pelos observáveis anteriores é transformado, utilizando o método [map], num tipo List<Integer>, que é a lista de números aleatórios gerados pelo servidor;
- linha 58: o valor observado é agora do tipo List<Integer>;
O resultado obtido para 10 pedidos é o seguinte:

9.3.8.4. Exemplo-04
A classe [Example04Fragment] é a seguinte:
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 exemplo é semelhante ao Exemplo-03, exceto que, em vez de utilizar o método [map] na linha 42, utilizamos o método [flatMap].
- linha 55: note que o tipo do valor observado é agora Integer;
Para 10 pedidos, obtemos os seguintes resultados:

Desta vez, há mais valores observados do que solicitações.
9.3.8.5. Exemplo-05
Vamos agora descrever o procedimento para adicionar um novo exemplo de observáveis à aplicação.
Suponhamos que queremos reproduzir o exemplo [Exemplo22h] da Secção 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);
}
}
- Os valores do observável [Observable.range(1, 10)] são primeiro agrupados em valores pares e ímpares pelo método [groupBy] (linha 11) e, em seguida, combinados num único observável pelo método [concatMap] (linha 12);
Passo 1
Criamos um novo exemplo no ficheiro [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>
Acima, foi adicionada a linha 8. O nome dado ao exemplo pode ser qualquer um.
Passo 2
Duplique a classe [Example04Fragment] para [Example05Fragment]. Aqui, o nome é fixo.
Passo 3
Modifique o código em [Example05Fragment] da seguinte forma:
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();
}
}
));
}
}
- linha 67: representa o observável do Exemplo 04: um fluxo de inteiros;
- linha 68: agrupamos este fluxo de inteiros de acordo com um critério booleano que iremos definir. Obteremos um observável do tipo Observable<GroupedObservable<Boolean, Integer>>, que, portanto, emite elementos do tipo GroupedObservable<Boolean, Integer>;
- linha 68: o método [concatMap] produzirá elementos do tipo Integer a partir de elementos do tipo GroupedObservable<Boolean, Integer>;
- linhas 32–59: para tornar a criação do observável nas linhas 67–69 mais legível, isolámos as instâncias das interfaces funcionais exigidas pelos vários operadores [filter, flatMap, groupBy, concatMap];
- linhas 47–52: O método [groupBy] espera um parâmetro do tipo Func1<T,K>, onde T é o tipo dos elementos agrupados e K é o tipo do critério de agrupamento. Dado um elemento T, a instância Func1<T,K> é responsável por produzir a chave de agrupamento K para esse elemento;
- linhas 48–51: Os elementos do tipo Integer serão agrupados por paridade. A instância Func1<Integer,Boolean> produz a chave true ou false, dependendo se o elemento deve ser colocado num grupo ou noutro. O resultado são dois grupos: o grupo de elementos pares com a chave true e o grupo de elementos ímpares com a chave false;
- linhas 53–59: o método [concatMap] espera um parâmetro do tipo Func1<T, Observable<R>> e produz um observável de elementos do tipo R. O tipo T aqui é o tipo emitido pelo operador [groupBy], neste caso um GroupedObservable<Boolean, Integer>;
- linha 57: a partir do elemento do tipo [GroupedObservable<Boolean, Integer>], produzimos um Observable<Integer>. Uma vez que o operador [groupBy] produziu dois grupos, o operador [concatMap] produzirá dois observáveis do tipo [Observable<Integer>]. Tal como o [flatMap], irá achatá-los num único observável. Mas, ao contrário do [flatMap], não mistura os elementos dos observáveis achatados. Devemos, portanto, observar dois grupos separados: os números aleatórios pares e os ímpares.
Passo 4
Executamos a aplicação:

e obtemos os seguintes resultados:

- em [1], os números aleatórios pares; em [2], os ímpares;
9.3.8.6. Para continuar
Convidamos agora o leitor a criar os seus próprios exemplos e também a experimentar vários valores para as entradas no formulário que configura os pedidos enviados ao servidor de números aleatórios.
9.3.9. Conclusão
Criámos a seguinte arquitetura no ambiente Android:
O cliente Android:
![]() |
A camada [DAO] comunica com o servidor que gera os números aleatórios apresentados pelo tablet Android. Este servidor possui a seguinte arquitetura de duas camadas:
![]() |
A camada [DAO] efetuou n pedidos HTTP ao servidor de números aleatórios, e a camada [swing] aguardou de forma assíncrona os resultados desses pedidos para os apresentar. Estes n pedidos HTTP foram efetuados ao mesmo servidor, que devolveu os mesmos tipos de respostas. Isto permitiu-nos fundir as respostas num único observável.
Na realidade, as aplicações Android comunicam com servidores diferentes e, provavelmente, não iremos fundir as suas respostas. As solicitações HTTP a estes servidores serão tratadas independentemente umas das outras e os seus resultados serão observados utilizando métodos separados.

















































