9. RxJava no ambiente Android
9.1. Introduction
Vamos agora retomar uma aplicação já abordada em vários documentos:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (capítulo 4);
- [Introduction à la programmation de tablettes Android par l'exemple] (capítulo 9);
- [Introduction à la programmation de tablettes Android par l'exemple - version 2] (parágrafo 1.11);
Trata-se de 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 uma 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 que no documento 2, mas simplificada através da utilização das 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 tem a seguinte arquitetura de duas camadas:
![]() |
Os clientes consultam determinados URL da 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 [métier];
- o seu serviço [web / JSON] implementado com o 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 pela tecnologia Spring MVC. O leitor que não esteja familiarizado com esta tecnologia pode:
- limitar-se a ler o parágrafo 9.2.1, que explica como iniciar o servidor e como o consultar;
- consultar o documento [Spring MVC et Thymeleaf par l'exemple], nomeadamente 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 pelo [2-3]. São então apresentados registos na consola:
- linha 12: indica que o serviço está disponível na porta 8080;
- linha 10: o único URL 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 de geração de números aleatórios;
- [minCount, maxCount]: são gerados count números aleatórios, em que count é um número aleatório no intervalo [minCount, maxCount];
- [minDelay, maxDelay]: o serviço aguarda delay milissegundos antes de devolver os números solicitados, sendo que delay é um número aleatório no intervalo [minDelay, maxDelay];
Num navegador, vamos solicitar este URL:
![]() |
Foram solicitados:
- números aleatórios no intervalo [100, 200];
- n números aleatórios com n no intervalo [10, 20];
- um tempo de espera de x milissegundos com x no intervalo [300, 400];
Na resposta:
- aleas: lista dos números aleatórios gerados;
- delay: o tempo de espera em milissegundos que o servidor registou;
- erro: um código de erro — 0 se não houver erro;
- mensagem: uma mensagem de erro — null se não houver erro;
9.2.2. As dependências Gradle do projeto
![]() |
O projeto [serveur] é um projeto Gradle configurado pelo seguinte ficheiro [build.gradle] [1]:
// gerado por http://start.spring.io/ (maio de 2016)
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'spring-boot'
jar {
baseName = 'serveur'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
}
- linha 1: um comentário para indicar como este ficheiro de configuração foi gerado;
- linhas 4 e 10: uma dependência do framework [Spring Boot], um ramo do ecossistema Spring. Este framework [http://projects.spring.io/spring-boot/] permite uma configuração mínima do Spring. De acordo com os ficheiros presentes no Classpath do projeto, o [Spring Boot] deduz uma configuração plausível ou provável para o mesmo. Assim, se o Hibernate estiver no Classpath do projeto, então o [Spring Boot] deduzirá que a implementação JPA utilizada será o Hibernate e configurará o Spring nesse sentido. O programador já não precisa de o fazer. Basta-lhe então efetuar apenas as configurações que o [Spring Boot] não definiu por predefinição ou aquelas que o [Spring Boot] definiu por predefinição, mas que têm de ser especificadas. Em todos os casos, é a configuração feita pelo programador que prevalece;
- 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 o Java 8;
- linhas 25-27: as dependências serão procuradas no repositório global do Maven ou no repositório local da 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 encontra-se o arquivo de um servidor Tomcat. É este que será utilizado para implementar a aplicação web. Note-se que a versão da dependência não foi mencionada. Será utilizada a versão indicada no projeto importado [spring-boot];
Para atualizar o projeto, é necessário forçar o download das dependências [1-3]:
![]() | ![]() ![]() |
Vejamos as dependências introduzidas pelo ficheiro [build.gradle] no [4]:
![]() |
São muitas. O Spring Boot para a Web incluiu 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:
- fornece as dependências de que provavelmente iremos precisar;
- vamos ver que simplifica consideravelmente 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 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.
É possível encontrar inúmeros 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 [métier]
![]() |
![]() |
A camada [métier] terá a seguinte interface [IMetier]:
package dvp.rxjava.server.metier;
public interface IMetier {
// números aleatórios no intervalo [a,b]
// são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
// os números são gerados após um atraso de delay milissegundos,
// onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}
Esta interface é praticamente idêntica à analisada no ambiente Swing no parágrafo 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 {
// campos
private int delay;
private List<Integer> aleas;
// construtores
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 e 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) {
// números aleatórios no intervalo [a,b]
// são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
// os números são gerados após um atraso de delay milissegundos,
// onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]
// algumas verificações
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;
}
// erros?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// gerador de números aleatórios
Random random = new Random();
// em espera?
int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
String message = null;
try {
message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
} catch (JsonProcessingException e1) {
throw new AleasException(e1,512);
}
throw new AleasException(message, 1024);
}
}
// geração do resultado
int count = minCount + random.nextInt(maxCount - minCount + 1);
List<Integer> nombres = new ArrayList<Integer>();
for (int i = 0; i < count; i++) {
nombres.add(a + random.nextInt(b - a + 1));
}
// retorno do resultado
return new AleasMetier(delay,nombres);
}
}
Não comentamos a classe: é análoga à encontrada no ambiente Swing no parágrafo 8.4. Salientamos apenas os seguintes pontos:
- linha 10: a anotação Spring [@Service], que fará com que o Spring instancie a classe numa única instância (singleton) e disponibilize a sua referência para outros componentes do Spring. Outras anotações Spring poderiam ter sido utilizadas aqui com o mesmo efeito;
- linhas 13-14: é injetado um mapeador jSON. O Spring é um contentor de objetos. Este contentor é instanciado no arranque da aplicação web e os objetos definidos por um ficheiro de configuração são então instanciados, por predefinição, numa única instância (singleton). Um singleton do Spring pode conter referências a outros objetos do Spring. É o que acontece aqui: o singleton [metier] (linhas 10-11) terá uma referência ao singleton [mapper] (linhas 13-14). A isto chama-se injeção de dependências. Existem duas formas de injetar um singleton noutro singleton:
- pelo seu tipo: isto é possível se o singleton a injetar for o único objeto Spring com esse tipo. É o que acontece aqui na injeção das linhas 13-14 (tipo ObjectMapper);
- pelo seu nome, se vários objetos Spring tiverem o mesmo tipo. Nesse caso, é necessário adicionar a anotação @Qualifier(«nomDuSingleton») 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 {
// código de erro
private int code;
// construtores
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 e 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 controlada (não é obrigatório tratá-la com um try/catch);
- linha 6: adiciona-se 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 modelo de arquitetura denominado MVC (Modelo – Vista – Controlador) da seguinte forma:
![]() |
O processamento de um pedido de um cliente decorre da seguinte forma:
- pedido — os URL solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa os URL recebidos. Esta «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas denominadas [Contrôleurs]. O C de MVC é, neste caso, a cadeia [Dispatcher Servlet, Contrôleur, Action]. Se nenhuma ação tiver sido configurada para processar o URL recebido, o servlet [Dispatcher Servlet] responderá que o URL solicitado não foi encontrado (erro 404 NOT FOUND);
- processamento
- a ação selecionada pode utilizar os parâmetros parami que a servlet [Dispatcher Servlet] lhe transmitiu. Estes podem provir de várias fontes:
- do caminho [/param1/param2/...] do URL,
- dos parâmetros [p1=v1&p2=v2] do URL,
- dos parâmetros enviados pelo navegador juntamente com o seu pedido;
- no processamento do pedido do utilizador, a ação pode necessitar da camada [metier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
- uma página de erro, caso a solicitação não tenha podido ser processada corretamente
- uma página de confirmação, caso contrário
- a ação solicita que uma determinada vista seja apresentada: [3]. Esta vista irá apresentar dados a que se chama o modelo da vista. É o M de MVC. A ação irá criar este modelo M [2c] e solicitar que uma vista V seja apresentada [3];
- resposta — a vista V selecionada utiliza o modelo M criado 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 alterada:
![]() |
- em [4a], o modelo, que é uma classe Java, é transformado 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] com o seguinte conteúdo:
package dvp.rxjava.server.web;
import dvp.rxjava.server.metier.AleasMetier;
public class AleasResponse extends AleasMetier {
// código de erro
private int erreur;
// mensagem de erro
private String message;
// construtores
public AleasResponse() {
}
public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
super(aleasMetier);
this.erreur = erreur;
this.message = message;
}
// getters e setters
public void setAleasMetier(AleasMetier aleasMetier) {
this.setDelay(aleasMetier.getDelay());
this.setAleas(aleasMetier.getAleas());
}
...
}
- linha 5: a classe [AleasResponse] estende a classe [AleasMetier] e, por isso, herda todos os seus atributos (aleas, delay);
- linha 8: um código de erro (0 se não houver erro);
- linha 10: se for erreur!=0, uma mensagem de erro; se não houver erro, null;
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 {
// camada de negócio
@Autowired
private IMetier metier;
@Autowired
private ObjectMapper mapper;
// números aleatórios em [a,b]
// são gerados n números, sendo n no intervalo [minCount, maxCount]
// os números são gerados após um atraso de delay milissegundos,
// onde [delay] é um número aleatório no intervalo [minDelay, maxDelay]
@RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
@PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
@PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {
// prepara-se a resposta
AleasResponse response = new AleasResponse();
// utiliza-se a camada de negócio para gerar os números aleatórios
try {
response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
} catch (AleasException e) {
// caso de erro (código e mensagem)
response.setErreur(e.getCode());
response.setMessage(e.getMessage());
}
// envia-se a resposta jSON
return mapper.writeValueAsString(response);
}
}
- linha 16: a anotação [@Controller] transforma a classe [AleasController] num singleton Spring. Além disso, indica que a classe contém métodos que irão processar pedidos para determinados URL da aplicação web. Neste caso, existe apenas uma na linha 29;
- linhas 20-21: a anotação [@Autowired] solicita ao Spring que injete no campo um componente do tipo [IMetier]. Será a classe [Metier] anterior. É porque colocámos nesta a anotação [@Service] que ela é tratada como um componente Spring;
- linhas 22-23: a anotação [@Autowired] solicita ao Spring que injete no campo um componente do tipo [ObjectMapper]. Iremos defini-lo em breve;
- linha 31: o método [getAleas] gera os números aleatórios. O seu nome não tem importância. Quando é executado, os parâmetros das linhas 31-33 foram inicializados pelo Spring MVC. Veremos como. Além disso, se este método for executado, é porque o servidor web recebeu um pedido HTTP GET para o URL da 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, vamos enviar-lhe uma cadeia de caracteres que será a cadeia jSON de um tipo [AleasResponse];
- linha 29: o URL processado tem o formato /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, em que {x} representa uma variável. Estas diferentes 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-se que os valores {x} são componentes de um URL e são, portanto, do tipo String. A conversão de String para o tipo dos parâmetros do método pode falhar. O Spring MVC lança então uma exceção. Resumindo: se, num navegador, eu solicitar o URL /100/200/10/20/300/400, o método getAleas da 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: solicita-se à camada [métier] uma lista de números aleatórios. Recorde-se que o método [metier].getAleas pode lançar uma exceção;
- linhas 42-43: caso de erro;
- linha 46: a resposta do tipo [AleasResponse] é devolvida sob a forma de uma cadeia jSON;
9.2.5. Configuração do projeto Spring
![]() |
Existem várias formas de configurar o Spring:
- com ficheiros XML;
- com código Java;
- com uma combinação de ambos;
Optamos por configurar a nossa aplicação web com código Java. É a classe [Config] acima referida que assegura esta 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 {
// -------------------------------- configuração da camada [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mapeador jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- linha 15: indicamos ao Spring em que pacotes irá encontrar os objetos a instanciar. Encontrará dois:
- a classe [Metier] anotada por [@Service];
- a classe [AleasController] anotada por [@Controller];
- linha 16: a anotação [@EnableWebMvc] induz configurações automáticas para o framework Spring MVC;
- linhas 19-20: injeção do contexto Spring (recipiente dos objetos Spring). Esta injeção é necessária porque o objeto das linhas 22-26 necessita dela;
- 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 as solicitações HTTP para o controlador e o método corretos. [DispatcherServlet] é uma classe do Spring;
- linhas 28-31: indica-se que este servlet processa todas as URL;
- linhas 33-36: é a presença deste bean que irá ativar o servidor Tomcat presente nos arquivos do projeto. Este aguardará as solicitações na porta 8080;
- linhas 39-42: um mapeador jSON. Foi este que foi injetado nos objetos Spring [Metier] e [AleasController];
9.2.6. Execução do 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) {
// execução da aplicação
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 de [spring Boot] (linha 4) que irá iniciar a aplicação. O seu primeiro parâmetro é a classe Java que configura o projeto. Neste caso, a classe [Config] que acabámos de descrever. O segundo parâmetro é o tabuleiro de argumentos passado ao método [main] (linha 7). Neste caso, não haverá argumentos;
Para a execução propriamente dita, convidamos o leitor a consultar novamente o parágrafo 9.2.1.
9.3. O cliente Android
Nota: o projeto Android que se segue é bastante complexo. Requer bons conhecimentos de Android, que podem ser encontrados, por exemplo, em [Introduction à la programmation de tablettes Android avec Android Studio ].
Atividade
Visualizações
Camada
[DAO]
Utilizador
Servidor
O cliente terá dois componentes:
- uma camada [Présentation] (visualizações + atividade);
- uma camada [DAO] que se dirige ao serviço [web / JSON] que analisá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 a RxJava ao ambiente Android. Tal como foi feito para a aplicação Swing, utilizaremos apenas uma única extensão introduzida pela RxAndroid, a do agendador [AndroidSchedulers.mainThread()]. Uma interface gráfica Android obedece às mesmas regras que uma interface Swing:
- os eventos são processados num único thread denominado «event loop» ou «thread da UI»;
- quando um evento desencadeia ações assíncronas, os resultados dessas ações devem ser recuperados na thread da UI, caso devam ser utilizados para atualizar a UI;
O cliente Android:
- enviará várias solicitações assíncronas para o servidor de números aleatórios. Estas solicitações serão executadas do lado do cliente com os threads do agendador [Schedulers.io()];
- estas solicitações assíncronas irão devolver observáveis que serão fundidos num único (merge);
- este observável será observado do 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 do [2].
Nota: a execução depende em grande medida da configuração do IntelliJ IDEA utilizado. É provável que a execução do [2] acima referida não funcione à primeira tentativa noutro computador que não o meu. Configurar corretamente o IntelliJ IDEA para executar este projeto pode ser uma tarefa complicada para principiantes. Aqui estão alguns pontos a ter em conta:
- no [3], aceder à estrutura do projeto;
![]() | ![]() |
- no [4-5], o JDK e os SDK Android presentes no meu computador. Note-se que o JDK 1.8 não é indispensável. O Android não suporta certas funcionalidades do Java 8, incluindo as lambdas. Por isso, 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 {
// substituir pela versão atual do plugin do Android
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
jcenter()
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "android.aleas"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
De acordo com os ficheiros SDK Android existentes, as versões nas linhas 8, 24-25 e 29 poderão ter de ser alteradas.
Para instalar novos SDK Android, utilize o SDK Manager da seguinte forma [1]:
![]() ![]() | ![]() |
O projeto foi configurado para:
- o SDK API 23 [2];
- o SDK Build-tools 23.0.3 [3];
- o SDK Tool 25.1.3 [4]
Por fim, verifique o caminho do SDK Android no ficheiro [local.properties] [4], linha 11 abaixo:
## Este ficheiro é gerado automaticamente pelo Android Studio.
# Não altere este ficheiro -- YOUR CHANGES WILL BE ERASED!
#
# Este ficheiro deve *NOT* ser submetido aos sistemas de controlo de versões,
# uma vez que contém informações específicas da sua configuração local.
#
# Localização do ficheiro SDK. Este ficheiro é utilizado apenas pelo Gradle.
# Para personalização ao utilizar um sistema de controlo de versões, leia a
# nota do cabeçalho.
#Qui, 07 de abril, 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. Execução do projeto no IntelliJ IDEA
Quando tiver sido criado um ambiente adequado para o projeto, este pode ser executado da seguinte forma:
![]() | ![]() | ![]() |
- em [1], inicia-se o emulador Android Genymotion;
- em [2], executa-se 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], solicita-se que, durante a execução, o IDE nos sugira um dispositivo de execução. Neste caso, será sempre o emulador Genymotion;
- no [5], indica-se que se mantenha este dispositivo para todas as execuções da configuração;
A execução do projeto no emulador Genymotion começa com a seguinte linha inicial:

Para saber o que inserir em [1], abra uma janela de comandos 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
Digite no [1] um dos endereços IP do seu computador (linhas 20, 28, 32). Se tiver um firewall do Windows, provavelmente terá de o desativar para que o emulador Android consiga aceder ao servidor de números aleatórios.
A execução das requisições assíncronas com as informações acima fornece os seguintes resultados:

Cada solicitação gera uma resposta jSON com os seguintes campos:
- aleas: os números aleatórios gerados pelo servidor;
- idClient: o número da solicitação;
- on: o thread de execução da solicitação do lado do cliente;
- requestAt: hora da solicitação;
- responseAt: hora de receção da resposta;
- delay: o tempo de espera que o servidor observou antes de enviar a sua resposta;
- erro: um código de erro — 0 se não houver erro;
- mensagem: uma mensagem de erro — null se não houver erro;
- observedAt: hora de observação da resposta;
- observedOn: thread de observação da resposta. Aqui será sempre [main], que designa o thread da interface do utilizador;
Como as solicitações são assíncronas e os tempos de espera impostos ao servidor são aleatórios, as respostas chegam em ordem dispersa.
9.3.4. As dependências Gradle do projeto
O projeto necessita de dependências que registamos 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 das linhas 2-3 são dependências padrão de um projeto Android com o SDK 23;
- A dependência da linha 5 inclui o objeto Spring [RestTemplate], que gere a comunicação da camada [DAO] com o servidor;
- a dependência da linha 6 inclui a biblioteca JSON e [Jackson], utilizadas pela aplicação;
- a dependência da linha 7 inclui 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: os acessos à Internet devem ser autorizados;
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 {
// números aleatórios no intervalo [a,b]
// são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
// os números são gerados após um atraso de delay milissegundos,
// onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]
public Observable<AleasDaoResponse> getAleas(final Request request);
// URL do serviço web
public void setUrlServiceWebJson(String url);
// tempo de espera máximo (ms) da resposta do servidor a um pedido de ligação
// tempo de espera (ms) máximo da resposta do servidor a uma solicitação
public void setClientTimeouts(int connectTimeout, int readTimeOut);
}
- linha 12: o método da camada [DAO] que fornece os números aleatórios de forma assíncrona;
- linha 15: para indicar à implementação [DAO] o URL do serviço de geração de números aleatórios;
- linha 19: para definir, na implementação [DAO], os tempos máximos de espera, de modo a evitar um tempo de espera excessivamente longo 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 {
// n.º da solicitação
int id;
// entradas do utilizador
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// construtores
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 e setters
...
}
Reconhecem-se aqui a maioria dos parâmetros do URL do servidor a consultar.
O método [getAleas] devolve um tipo Observable<AleasDaoResponse>, em que a classe [AleasDaoResponse] é a seguinte:
package android.aleas.dao;
import java.util.List;
public class AleasDaoResponse {
// código de erro
private int erreur;
// mensagem de erro
private String message;
// tempo de espera do servidor
private int delay;
// números aleatórios gerados pelo servidor
private List<Integer> aleas;
// estado do cliente
private ClientState clientState;
// construtores
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 e 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 {
// nome do thread de execução
private String on;
// hora do pedido
private String requestAt;
// hora da resposta
private String responseAt;
// ID do cliente
private int idClient;
// construtor
public ClientState() {
on = Thread.currentThread().getName();
requestAt = getTimeStamp();
}
public ClientState(int idClient) {
this();
this.idClient = idClient;
}
// métodos privados
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// getters e setters
...
}
- linha 11: thread de execução da camada [DAO];
- linha 13: hora do pedido;
- linha 15: hora da resposta;
- linha 17: n.º 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 {
// cliente REST
private RestTemplate restTemplate;
// URL serviço
private String urlServiceWebJson;
// mapeador jSON
private ObjectMapper mapper;
// construtores
public Dao() {
// mapeador jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// define-se o URL do serviço REST
this.urlServiceWebJson = urlServiceWebJson;
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
}
}
- linha 22: o objeto [RestTemplate], que assegurará a comunicação com o servidor de números aleatórios;
- linha 24: o URL do serviço de geração — é definido pelo método [setUrlServiceWebJson] da linha 41;
- linha 27: o mapeador jSON, que servirá para deserializar a cadeia jSON enviada pelo servidor de números aleatórios;
- linhas 30-33: o construtor da classe;
- linha 32: é criado o mapeador jSON da linha 27;
O método [setClientTimeouts] é o seguinte:
// cliente REST
private RestTemplate restTemplate;
...
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
// define-se o tempo limite para as solicitações do cliente 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 é assegurada pelo objeto [RestTemplate] da linha 2. Por enquanto, ainda não o inicializámos. É o método [setClientTimeouts] que o faz;
- linha 8: a classe [HttpComponentsClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Esta vai permitir-nos definir os tempos máximos de espera pela resposta do servidor (linhas 9-10);
- linha 11: criamos o objeto do tipo [RestTemplate], que servirá de suporte à comunicação com o serviço web. Passamos-lhe como parâmetro o objeto [factory] que acabou de ser criado;
- linha 12: o diálogo cliente/servidor pode assumir várias formas. As trocas de dados são feitas através de linhas de texto e temos de indicar ao objeto do tipo [RestTemplate] o que deve fazer com essa linha de texto. Para tal, fornecemos-lhe conversores, ou seja, classes capazes de processar as linhas de texto. A escolha do conversor é geralmente feita através dos cabeçalhos HTTP que acompanham a linha de texto. De acordo com esses cabeçalhos, o objeto [RestTemplate] irá escolher, entre os seus conversores, aquele que for mais adequado à situação. Neste caso, teremos apenas um único conversor, um conversor String --> String, o que faz com que o tipo String recebido do servidor não sofra qualquer transformação.
O método [getAleas] é o método mais complexo:
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
// execução do serviço
return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
@Override
public void call(Subscriber<? super AleasDaoResponse> subscriber) {
try {
// URL do serviço: /{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());
// informações do cliente
ClientState clientState = new ClientState(request.getId());
// pedido HTTP síncrono
String response = executeRestService("get", urlService, null);
// deserialização da resposta jSON do servidor
AleasServerResponse aleasServerResponse = mapper.readValue(
response,
new TypeReference<AleasServerResponse>() {
});
// erro?
int erreur = aleasServerResponse.getErreur();
if (erreur != 0) {
// a exceção é encaminhada
subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
} else {
// regista-se a hora de receção
clientState.setResponseAt();
// o resultado é encaminhado para o assinante
subscriber.onNext(
new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
}
} catch (Exception ex) {
// a exceção é encaminhada para o assinante
subscriber.onError(ex);
} finally {
// é sinalizado o fim do observável
// durante a execução, verifica-se que este método não tem qualquer efeito se o método [onError] tiver sido chamado anteriormente — conforme a teoria —, pelo que esta instrução poderia ser colocada apenas no bloco «try»
subscriber.onCompleted();
}
}
});
}
- linha 2: é importante lembrar que devemos produzir um tipo [Observable<AleasResponse>];
- linha 3: uma linha de registo na consola Android;
- linha 5: o objeto [RestTemplate] garante uma comunicação síncrona com o servidor. Isto significa que o thread de execução que efetua a solicitação fica bloqueado até receber a resposta. No exemplo do Swing, vimos como transformar uma ação síncrona numa ação assíncrona através do método [Observable.create]. É este mesmo caminho que seguimos aqui;
- linha 7: o método [call] da interface [Observable.OnSubscribe<AleasDaoResponse>] da linha 5. É este método que é chamado quando um observador se subscreve ao observável;
- linhas 10-12: construção do URL do serviço de números aleatórios;
- linha 14: inicialização do objeto [ClientState]. Trata-se aqui de registar a hora do pedido;
- linha 16: pedido síncrono HTTP. Obtém-se uma resposta jSON. O método [executeRestService] espera três parâmetros:
- o método HTTP a utilizar para consultar o serviço;
- o URL do serviço;
- o objeto a enviar do tipo Object, null se o método HTTP não for POST;
- 18-21: deserialização da cadeia jSON recebida num tipo [AleasServerResponse]. Este tipo é o seguinte:
package android.aleas.dao;
import java.util.List;
public class AleasServerResponse {
// código de erro
private int erreur;
// mensagem de erro
private String message;
// tempo de espera do servidor
private int delay;
// números aleatórios
private List<Integer> aleas;
// getters e setters
...
}
- linha 23: recupera-se o código de erro enviado pelo servidor;
- linhas 24-26: em caso de erro, é lançada uma exceção para o assinante;
- linha 29: atualiza-se o [clientState], que fará parte da resposta enviada ao assinante;
- linhas 31-33: envio da resposta ao assinante. A resposta tem o formato [AleasDaoResponse];
- linhas 35-37: tratam todos os casos de erro de forma indiferenciada. O erro mais provável é um erro de rede;
- linha 41: notificação do fim da transmissão;
9.3.7. As vistas da aplicação
![]() |
![]() |
A aplicação apresenta as duas vistas seguintes:
A vista da consulta

A vista da resposta

9.3.7.1. A classe [MyFragment]
Existem dois fragmentos:
- [RequestFragment] para a consulta;
- [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 {
// ------------- dados comuns aos fragmentos
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- linha 7: a classe [MyFragment] estende a classe Android [Fragment];
- linhas 10-11: os dados comuns a todos os fragmentos;
- linha 10: cada fragmento conhece a atividade única da aplicação;
- linha 11: para comunicarem entre si, os fragmentos utilizam uma sessão;
- linha 13: antes de apresentar um fragmento, este será solicitado a atualizar-se com o conteúdo da sessão. Este método é declarado abstrato, uma vez que é implementado pelas classes derivadas. Por este motivo, a própria classe é declarada abstrata (linha 7);
A classe [Session] contém os dados que os diferentes fragmentos da aplicação partilham. O seu código é o seguinte:
![]() |
package android.aleas.activity;
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
public class Session {
// atividade da aplicação
private MainActivity activity;
// número de pedidos
private int nbRequests;
// características das solicitações
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// URL serviço web / jSON
private String urlWebJson;
// operação iniciada
private boolean onAir;
// idem, mas um pouco mais tarde
private boolean operationStarted;
// o nome do exemplo escolhido pelo utilizador na lista de exemplos
private String exampleName;
// o seu n.º na lista de fragmentos
private int examplePosition;
// o adaptador do spinner dos exemplos na vista da consulta
private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
// métodos
public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
this.urlWebJson = urlWebJson;
this.exampleName = exampleName;
this.examplePosition = examplePosition;
}
public Request getRequest() {
return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
}
// getters e setters
...
}
O método da linha 46 permite criar o objeto [Request], que encapsula todas as informações fornecidas pelo utilizador na vista de consulta:
![]() |
package android.aleas.fragments;
public class Request {
// n.º da consulta
int id;
// entradas do utilizador
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// construtores
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 e setters
....
}
9.3.7.2. O fragmento [RequestFragment] da consulta
O fragmento da consulta tem os seguintes componentes:

A aplicação tem uma única vista, que é uma vista com duas separadores:
- [1]: o separador da solicitação;
- [2]: o separador da 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 count números, 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 | URL do serviço de geração de números aleatórios; |
8 | Spinner | spinnerExemples | a lista suspensa de exemplos. Cada exemplo ilustra um método específico da classe [Observable]; |
8 | Botão | btnExecuter | o botão que inicia as chamadas ao serviço de geração de números; |
Os erros de introdução de dados são assinalados:

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

n.º | Tipo | Nome | Função |
1 | TextView | infoReponses | número de respostas recebidas |
2 | ListView | listReponses | lista de canais jSON recebidos do servidor |
3 | Botão | btnAnnuler | para cancelar os 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">
<!-- barra de aplicações -->
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<!-- barra de ferramentas -->
<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">
<!-- imagem de espera -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- recipiente de separadores -->
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<!-- recipiente de vistas -->
<android.aleas.activity.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:layout_marginBottom="100dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
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 | ProgressBar | loadingPanel | imagem de espera exibida enquanto o pedido do utilizador está a ser processado |
37-40 | TabLayout | separadores | a barra de separadores da aplicação |
44-51 | MyPager | container | o contentor no qual são apresentados os diferentes fragmentos da aplicação |
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 {
// controla o deslize
private boolean isSwipeEnabled;
// construtores
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// redefinição de métodos
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// deslizar permitido?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// deslizar autorizado?
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 do Android [ViewPager]. Utiliza-se a classe [MyPager] em vez da classe [ViewPager] apenas porque se pretende inibir o deslize: por predefinição, com a classe [ViewPager], é possível passar de um separador para outro através de um deslize (deslizando para a esquerda ou para a direita). Neste caso, não queremos esse comportamento;
- linha 11: a variável booleana que irá controlar o deslize (linhas 26 e 36);
- linhas 44-46: o método que permite inicializar o campo da linha 11;
A estrutura da atividade Android [MainActivity] é a 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 {
// camada [DAO]
private IDao dao;
// a sessão
private Session session;
// construtor
public MainActivity() {
// pai
super();
// sessão
session = new Session();
// DAO
dao = new Dao();
}
// getters
public Session getSession() {
return session;
}
// implementação 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]. Trata-se, portanto, de 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 não tenham conhecimento da camada [DAO]: os seus gestores de eventos irão recorrer à camada [activité] quando pretenderem comunicar com o servidor.
- linha 24: uma referência à camada [DAO] inicializada pelo construtor da linha 35;
- linha 26: uma referência à sessão partilhada pelos fragmentos, inicializada pelo construtor da linha 33;
- linhas 46-59: implementação da interface [IDao];
A classe [MainActivity] inicializa os componentes da vista que lhe está associada da seguinte forma:
// barra de ferramentas
private Toolbar toolbar;
// gestor de fragmentos
private MyPager mViewPager;
// recipiente de separadores
private TabLayout tabLayout;
// imagem de espera
private ProgressBar loadingPanel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
// clássico
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// sessão
session.setActivity(this);
// configuração de tempos limite da camada [DAO]
setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
// componentes
mViewPager = (MyPager) findViewById(R.id.container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
tabLayout = (TabLayout) findViewById(R.id.tabs);
// barra de ferramentas
setSupportActionBar(toolbar);
// inicialmente, existe apenas um separador
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Request");
tabLayout.addTab(tab);
// gestor de eventos
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// foi selecionado um separador — altera-se o fragmento apresentado pelo contentor de fragmentos
int position = tab.getPosition();
if (position == 0) {
// separador de consulta
showView(0);
} else {
// separador de respostas — depende do exemplo escolhido
showView(session.getExamplePosition());
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// criação dos fragmentos das respostas
createResponseFragments();
// gestão da imagem de espera
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: cria-se o primeiro separador com o título [Request]. A certa altura, teremos na memória:
- o fragmento [Request];
- n fragmentos do tipo [ExampleXXFragment];
A primeira aba exibirá sempre o fragmento [Request]. A segunda aba exibirá o fragmento [ExampleXXFragment] correspondente ao exemplo escolhido pelo utilizador. O fragmento exibido pela segunda aba muda, portanto, ao longo do tempo;
- linhas 37-48: o código executado quando o utilizador clica numa das separadores;
- linha 43: é exibido o fragmento n.º 0;
- linha 46: é exibido o fragmento atualmente em uso (visualizado). O seu n.º é encontrado na sessão;
- linha 62: criam-se os fragmentos de todos os exemplos presentes no spinner de exemplos na vista [RequestFragment] (1.ª aba);
- linha 65: a imagem de espera está, por enquanto, oculta;
Para compreender o método [showView] (linhas 43, 46) e o método [createResponseFragments], temos primeiro de apresentar o gestor de fragmentos em memória (classe incluída no ficheiro Java de MainActivity):
// o gestor de fragmentos - deve definir os métodos getItem, getCount
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragmentos geridos
private MyFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
super(fm);
this.fragments = fragments;
}
// deve apresentar o fragmento na posição n.º
@Override
public MyFragment getItem(int position) {
// o fragmento
return fragments[position];
}
// indica o número de fragmentos a gerir
@Override
public int getCount() {
// n.º de fragmentos
return fragments.length;
}
}
}
- A classe [SectionsPagerAdapter] estende a classe Android [FragmentPagerAdapter]. Ela redefine 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 são armazenados na linha 5. Note-se que são do tipo [MyFragment], apresentado no parágrafo 9.3.7.1;
- linha 8: para ser criada, 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 de que a aplicação necessita:
private void createResponseFragments() {
// spinner de exemplos
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Especificar o layout a utilizar quando a lista de opções for apresentada
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// coloca-se o adaptador na sessão para que a vista [Request] o recupere
session.setSpinnerExemplesAdapter(adapter);
...
}
- linha 3: cria-se um adaptador para o spinner dos exemplos, neste caso uma lista de String que representa os nomes dos exemplos. Estes nomes estão presentes no ficheiro [layout/exemples.xml]:
![]() |
O ficheiro [exemples.xml] contém o seguinte código:
<!-- exemplos -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
</string-array>
</resources>
Na linha 1, este ficheiro é o segundo parâmetro do método [createFromResource]. Em [R.array.exemples], [exemples] é o nome da tabela, na linha 3 acima, e não o nome do ficheiro.
- linha 5: associa-se um layout (gestor de visualização) ao adaptador. Agora, o adaptador dispõe tanto dos dados como do respetivo modo de visualização;
- linha 7: coloca-se o adaptador em sessão. É aí que o fragmento [RequestFragment], que precisa dele, o irá recuperar;
Continuemos com o código do método [createResponseFragments]:
private void createResponseFragments() {
// exemplos de spinner
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Especificar o layout a utilizar quando a lista de opções for apresentada
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// colocamos o adaptador na sessão para que a vista [Request] o recupere
session.setSpinnerExemplesAdapter(adapter);
// criação da tabela de fragmentos (1 consulta, n respostas)
MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
// fragmento da consulta
tFragments[0] = new RequestFragment();
// fragmentos das respostas
for (int i = 1; i < tFragments.length; i++) {
// constrói-se o nome do fragmento a instanciar, correspondente ao exemplo escolhido pelo utilizador
// este nome deve ser o nome completo com o respetivo pacote — neste caso, está diretamente associado ao número do exemplo no spinner
String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
// instancia-se o fragmento associado ao exemplo
MyFragment fragment;
try {
// instanciação da classe
fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
} catch (Exception e) {
e.printStackTrace();
return;
}
// o fragmento foi criado — é adicionado à matriz
tFragments[i] = fragment;
}
// instanciação do gestor de fragmentos com estes novos fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// Configurar o ViewPager com o adaptador de secções.
mViewPager.setAdapter(mSectionsPagerAdapter);
// navegação entre páginas — esta instrução é importante
// aqui diz-se que, de ambos os lados da vista exibida, é necessário manter vistas inicializadas [tFragments.length]
// isto implica que, neste caso, todos os fragmentos utilizados pela aplicação estão na memória e inicializados
// Se não se fizer isso, por predefinição, o [OffscreenPageLimit] é igual a 1
// assim, se o fragmento visualizado for o n.º 3, apenas os fragmentos 2 e 4 serão inicializados
// isto ocorre através da chamada do método [onCreateView] destes dois fragmentos — o que significa que, neste método, é necessário prever
// regenerar o aspeto visual que o fragmento tinha da última vez que foi utilizado — além disso, não pode haver, neste método
// haja código que não suporte ser executado duas vezes — isso cria uma confusão enorme e é complexo de gerir
// aqui optou-se por evitar essas dificuldades — nos registos, verifica-se que, no arranque da aplicação, todos os fragmentos são criados
// e o respetivo método [onCreateView] é executado — e nunca mais volta a ser executado depois disso —
mViewPager.setOffscreenPageLimit(tFragments.length);
// inibimos a transição entre fragmentos
mViewPager.setSwipeEnabled(false);
}
- linha 9: criação da matriz que irá conter todos os fragmentos da aplicação;
- linha 11: o primeiro fragmento é o da consulta;
- linhas 13-28: vamos criar tantos fragmentos quantos forem os exemplos. Todos estes fragmentos estendem o fragmento da resposta [ResponseFragment] e implementam apenas o que é específico do exemplo: a criação dos valores observados. Estes, de facto, diferem de um exemplo para outro;
- linha 16: o fragmento de um exemplo tem um nome padrão: ExampleXXFragment, em que XX é a sua posição no spinner dos exemplos, acrescida de 1. XX é também o n.º do fragmento do exemplo no gestor de fragmentos;
- linha 21: instanciação do fragmento do exemplo n.º i do spinner:
- Class.forName(exampleName): carrega o fragmento na memória;
- Class.forName(exampleName).getConstructors()[0]: obtém a referência ao primeiro construtor da classe. A classe ExampleXXFragment tem apenas um construtor. Por conseguinte, será obtida uma referência a esse 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 esse construtor. Como o construtor da classe ExampleXXFragment não espera parâmetros, passa-se um array de objetos vazio;
- linha 27: este fragmento é adicionado à matriz de fragmentos;
- linha 30: vimos que o construtor do gestor de fragmentos [SectionsPagerAdapter] esperava, nos seus parâmetros, a matriz de fragmentos que deveria gerir. É agora que lha passamos;
- linha 22: o contentor de fragmentos [mViewPager] da vista associada à atividade [MainActivity] é aqui associado ao gestor de fragmentos: o contentor de fragmentos [mViewPager] exibe os fragmentos do gestor de fragmentos;
- linha 43: vamos ler os comentários — a instrução equivale a dizer que todos os fragmentos devem permanecer no estado em que o código os coloca, independentemente do fragmento atualmente exibido. Assim, quando voltamos a ele, encontramo-lo no estado em que o deixámos;
- linha 45: o contentor de fragmentos [mViewPager] é do tipo [MyPager], o que permite inibir o deslize;
O método [MainActivity.showView] é o seguinte:
// visualização da vista n.º [position]
private void showView(int position) {
// atualiza-se o fragmento antes da sua visualização
mSectionsPagerAdapter.getItem(position).onRefresh();
// exibe-se a vista solicitada — passa-se diretamente para a vista (segundo parâmetro definido como «false»)
// sem este parâmetro, acede-se por predefinição à vista pretendida, exibindo rapidamente as vistas intermédias — comportamento indesejável
mViewPager.setCurrentItem(position, false);
}
- linha 3: pretende-se apresentar o fragmento na posição n.º;
- linha 4: este fragmento é solicitado ao gestor de fragmentos e, em seguida, atualizado. Com efeito, desde a última vez que foi apresentado, a sessão pode ter mudado. O fragmento deve, então, inspecionar a sessão para verificar se deve atualizar-se;
- linha 7: o fragmento é apresentado pelo [ViewPager]. Como este foi associado ao gestor de fragmentos, é o fragmento n.º [position] que será apresentado, aquele que acabámos de atualizar na linha 4;
Terminemos com os dois métodos de gestão da espera:
public void beginWaiting() {
// gestão da imagem de espera
loadingPanel.setVisibility(View.VISIBLE);
}
public void cancelWaiting() {
// gestão da imagem de espera
loadingPanel.setVisibility(View.INVISIBLE);
// fim da execução
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 do serviço web
private EditText edtUrlServiceRest;
private TextView txtMsgErreurUrlServiceWeb;
// número de pedidos
private EditText edtNbRequests;
private TextView txtErrorRequests;
// intervalo de geração
private EditText edtA;
private EditText edtB;
private TextView txtErrorIntervalle;
// atraso
private EditText edtMinDelay;
private EditText edtMaxDelay;
private TextView txtErrorDelay;
// número de valores gerados
private EditText edtMinCount;
private EditText edtMaxCount;
private TextView txtErrorCount;
// botão
private Button btnExecuter;
// lista de respostas
private ListView listReponses;
private TextView infoReponses;
// seletor de exemplos
private Spinner spinnerExemples;
// as entradas
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// construtor
public RequestFragment() {
super();
Log.d("rxjava", "RequestFragment constructor");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d("rxjava", "RequestFragment onCreateView");
// recuperamos a atividade e a sessão
activity = (MainActivity) getActivity();
session = activity.getSession();
// cria-se a vista do fragmento a partir da sua definição XML
View view = inflater.inflate(R.layout.request, container, false);
// componentes
edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
edtA = (EditText) view.findViewById(R.id.edt_a);
edtB = (EditText) view.findViewById(R.id.edt_b);
txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);
// botão [Exécuter]
btnExecuter.setVisibility(View.VISIBLE);
btnExecuter.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doExecuter();
}
});
// Inicialmente, não há mensagens de erro
txtErrorRequests.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
txtErrorCount.setVisibility(View.INVISIBLE);
txtErrorDelay.setVisibility(View.INVISIBLE);
// spinner dos exemplos
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// resultado
return view;
}
...
}
- linha 16: a classe [RequestFragment] estende a classe [MyFragment] (ver parágrafo 9.3.7.1);
- linhas 18-42: os componentes visuais do fragmento (ver parágrafo 9.3.7.2);
- linhas 45-52: os dados introduzidos pelo 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 da aplicação. Esta é a única vez;
- linha 61: o código do método [onCreateView] é clássico. Note-se, na linha 102, que o adaptador do spinner dos exemplos é obtido na sessão. Note-se também, na linha 91, que o clique no botão [Exécuter] é gerido pelo método [doExecuter];
- linhas 64-65: os campos [activity] e [session] pertencem à classe pai [MyFragment];
O método [doExecuter] é o seguinte:
// as entradas
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
...
private void doExecuter() {
// entradas válidas?
if (isPageValid()) {
// guardamos as informações na sessão
session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
// memoriza-se o URL do serviço web
activity.setUrlServiceWebJson(session.getUrlWebJson());
Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
// ação em curso
session.setOnAir(true);
// mas ainda não iniciada
session.setOperationStarted(false);
// exibe-se o fragmento da resposta
activity.selectTab(Constants.VUE_RESPONSE);
// inicia-se a espera
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. Nesse caso, são utilizadas para inicializar os campos das linhas 2 a 9;
- linha 17: as diferentes entradas são colocadas na sessão:
- [spinnerExemples.getSelectedItem().toString()] é o nome do exemplo selecionado pelo utilizador e é memorizado em [session.exampleName];
- [spinnerExemples.getSelectedItemPosition() + 1] é o n.º do fragmento associado ao exemplo e que foi guardado (o fragmento) pelo gestor de fragmentos. Este n.º é guardado em [session.examplePosition];
- linha 19: o URL do serviço web / jSON é transmitido à atividade, que, por sua vez, o transmite à camada [DAO];
- linhas 21-24: verifica-se que uma operação está prestes a iniciar;
- linha 26: o separador da resposta vai ser apresentado. Para compreender o que vai acontecer, é necessário recordar o código [MainActivity.selectTab]:
// seleção de um separador
public void selectTab(int position) {
// existem, no máximo, 2 separadores
// inicialmente, existe apenas uma, a da consulta
// se a aba solicitada for a n.º 1 e esta ainda não existir, então é necessário criá-la
if (position == 1 && tabLayout.getTabCount() == 1) {
// mais 1 separador
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Response");
tabLayout.addTab(tab);
}
// seleciona-se a guia por programa, o que irá desencadear o evento [onTabSelected]
// que irá associar a vista correta a essa aba
tabLayout.getTabAt(position).select();
}
- inicialmente, a atividade tinha criado apenas o separador da consulta (separador n.º 0);
- linhas 6-11: cria-se o separador da resposta (separador n.º 1) caso ainda não tenha sido criado;
- linha 14: seleciona-se a guia n.º position (0 ou 1). Isto coloca o evento [onTabSelected] na fila do evento loop da aplicação Android;
O gestor do evento [onTabSelected] em [MainActivity] é o seguinte:
@Override
public void onTabSelected(TabLayout.Tab tab) {
// foi selecionada uma aba — altera-se o fragmento apresentado pelo contentor de fragmentos
int position = tab.getPosition();
if (position == 0) {
// separador de consulta
showView(0);
} else {
// separador «Resposta» — depende do exemplo escolhido
showView(session.getExamplePosition());
}
}
No caso do separador [Response], é a linha 9 que é executada. O fragmento n.º [session.getExamplePosition()] será apresentado. Por exemplo, no caso do exemplo [exemple-03], o n.º que foi registado em [session.examplePosition] é 3. A linha 10 apresenta, então, o fragmento n.º 3. A tabela de fragmentos criada inicialmente pela atividade é [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Por conseguinte, é efetivamente o fragmento [Exemple03Fragment] que será apresentado. Isto é feito através do seguinte código:
// exibição da vista n.º [position]
private void showView(int position) {
// atualiza-se o fragmento antes da sua exibição
mSectionsPagerAdapter.getItem(position).onRefresh();
// exibe-se a vista solicitada - vai-se diretamente para a vista (segundo parâmetro definido como «false»)
// sem este parâmetro, acede-se por predefinição à vista pretendida, exibindo rapidamente as vistas intermédias — comportamento indesejável
mViewPager.setCurrentItem(position, false);
}
Vê-se que o fragmento vai ser atualizado (linha 4) antes de ser apresentado (linha 7).
9.3.7.6. O fragmento [ResponseFragment]
A classe [ResponseFragment] apresenta as 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 {
// lista de respostas
private ListView listReponses;
private TextView infoReponses;
// botão
private Button btnAnnuler;
// mapeador jSON
private ObjectMapper mapper;
protected ResponseFragment() {
super();
Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
mapper = new ObjectMapper();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// recupera-se a atividade e a sessão
activity = (MainActivity) getActivity();
session = activity.getSession();
Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
// cria-se a vista do fragmento a partir da sua definição XML
View view = inflater.inflate(R.layout.response, container, false);
// componentes
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
// botão [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnAnnuler.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doAnnuler();
}
});
// resultado
return view;
}
...
// método a executar (por código explícito) antes de cada visualização do fragmento
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 dos exemplos pela atividade. Com efeito, todos os fragmentos dos exemplos estendem o fragmento [ResponseFragment]. Durante a sua instanciação, é chamado o construtor da sua classe pai [ResponseFragment];
- linha 35: inicializa o mapeador jSON da linha 30, utilizado para exibir 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 dos exemplos pela atividade. Aqui encontra-se código clássico de uma aplicação Android;
- linhas 52-56: o método executado ao clicar no botão [Annuler] é o método [doAnnuler];
- linhas 62-64: o método [onRefresh] é executado sempre que o separador [Response] é apresentado;
Graças aos vários registos inseridos nos métodos importantes, é possível ver o que acontece no arranque da aplicação:
- linha 1: construção do fragmento [RequestFragment];
- linhas 2-9: construção dos fragmentos dos 4 exemplos da aplicação;
- linha 10: inicialização do fragmento [RequestFragment];
- linhas 11-14: inicialização dos fragmentos dos 4 exemplos da aplicação;
Depois disso, nunca mais se voltam a ver chamadas a estes métodos.
O método [ResponseFragment.onRefresh] é o seguinte:
// método a executar (por código explícito) antes de cada visualização do fragmento
public void onRefresh() {
Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
// execução em curso?
if (session.isOnAir() && !session.isOperationStarted()) {
// execução da solicitação
session.setOperationStarted(true);
doExecuter();
}
}
- linha 5: verifica-se se o fragmento [RequestFragment] efetuou um pedido (session.isOnAir) e se este já foi iniciado (isOperationStarted). Se o fragmento [RequestFragment] tiver efetuado uma solicitação e esta ainda não estiver em execução, a operação é iniciada (linhas 7-8);
- uma vez iniciada a operação, como esta é assíncrona, o utilizador pode alternar entre os dois separadores. Se voltar ao separador [Response] e houver uma operação em curso, as linhas 7-8 não são executadas;
O método [doExecuter], 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()));
// início da espera
beginWaiting();
// preparação da execução
subscriptions.clear();
reponses.clear();
nbInfos = 0;
// estão a ser criados e executados os observáveis do exemplo escolhido
createAndExecuteObservables();
}
// método implementado pelas classes filhas
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 é esvaziada;
- linha 7: a lista que apresenta as respostas é esvaziada;
- linha 8: conta o número de respostas recebidas;
As classes filhas [ExampleXXFragment] atribuem ao método [showAlea], a seguir, a tarefa de apresentar os elementos que observam:
protected void showAlea(String data) {
// mais uma informação
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// mais 1 resposta
reponses.add(0, data);
Log.d("rxjava", data);
// atualização do UI
listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
- linha 1: verifica-se que o elemento observado chega sob a forma de uma cadeia de caracteres. Trata-se, na verdade, da cadeia jSON do elemento observado. Isto permite-nos dispor de um método único para apresentar o elemento observado, independentemente do seu tipo exato em Java;
- linha 6: o elemento observado [data] é adicionado na primeira posição da lista de respostas. Assim, o utilizador vê, no início da lista, as respostas mais recentes;
A espera é gerida pelos seguintes métodos [beginWaiting] e [cancelWaiting]:
private void beginWaiting() {
// colocamos a ampulheta
activity.beginWaiting();
// o botão [Annuler] é apresentado
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// fim da espera
activity.cancelWaiting();
// o botão [Annuler] fica oculto
btnAnnuler.setVisibility(View.INVISIBLE);
}
Estes recorrem aos métodos com os mesmos nomes da atividade e limitam-se a mostrar/ocultar o botão [Annuler].
O clique no botão [Annuler] é gerido pelo código seguinte:
protected void doAnnuler() {
// cancelam-se todas as subscrições
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// fim da espera
cancelWaiting();
}
- linhas 3-7: cancelam-se, uma a uma, todas as subscrições;
9.3.8. Exemplos de observáveis
9.3.8.1. Exemple-01
As classes [ExampleXXFragment] têm como funcionalidade criar, executar e observar observáveis. A exibição dos valores observados é feita 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 {
// mapeadores jSON
private ObjectMapper mapperAleasUiResponse;
// construtor
public Example01Fragment() {
super();
Log.d("rxjava", "Example01Fragment constructor");
// filtros jSON
mapperAleasUiResponse = new ObjectMapper();
}
@Override
public void createAndExecuteObservables() {
Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
// solicitação de números aleatórios
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// configuração observável n.º i
// solicitação a enviar ao servidor
Request request = session.getRequest();
request.setId(i);
// observável executado na thread de cálculo
observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
}
// observação na thread do evento loop;
observable = observable.observeOn(AndroidSchedulers.mainThread());
// executam-se todos estes observáveis
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) {
// extraem-se as informações a apresentar
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 diferentes observáveis que são fundidos (linha 43) no observável da linha 36;
- linha 43: o observável é executado num thread do agendador [Schedulers.io()]. A chamada HTTP ao servidor será executada neste thread;
- linha 46: o observável final é observado na thread do evento loop;
- linhas 48-57: execução dos observáveis e, consequentemente, das solicitações ao servidor de números aleatórios. O Android ainda não suporta o Java 8 e as suas lambdas. Por isso, utilizam-se aqui classes anónimas para instanciar as interfaces funcionais de RxJava;
- linhas 49-52: ação executada quando o observador recebe um novo elemento do tipo [AleasDaoResponse] do observável (ver parágrafo 9.3.6.1);
- linha 51: chamada do método [showAlea] da classe pai. Recorde-se que este método espera uma cadeia de caracteres. Esta é fornecida pelo método [getDataFrom] das linhas 59-68;
- linha 63: devolve-se a cadeia 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 {
// resposta [DAO]
private AleasDaoResponse aleasDaoResponse;
// thread de observação
private String observedOn;
// hora de observação
private String observedAt;
// construtores
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 e setters
...
}
- à resposta da camada [DAO] (linha 11), adicionam-se duas informações:
- linha 13: o fio de observação;
- linha 15: a hora da observação;
Voltemos ao código de subscrição:
@Override
public void createAndExecuteObservables() {
...
// executam-se todos estes observáveis
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) {
// exibe-se a exceção
showAlea(getMessagesFromThrowable(th));
// após receber uma exceção, o observável não recebe nem onNext, nem onCompleted
// obrigado a cancelar a subscrição manualmente
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// fim da espera
cancelWaiting();
}
}));
}
- linhas 11-18: caso em que o observador receba uma exceção;
- linha 14: volta-se a utilizar o método [showAlea] da classe pai para apresentar a exceção. O método [getMessagesFromThrowable] é um método da classe pai [ResponseFragment] que, a partir de uma exceção, gera uma cadeia de caracteres:
// mensagens de uma exceção
protected String getMessagesFromThrowable(Throwable ex) {
// é criada uma lista com as mensagens de erro da pilha de exceções
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-se a cadeia jSON de uma lista de mensagens de erro (linha 4);
Voltemos ao código de subscrição do observável:
- linhas 19-25: o código executado quando o observador recebe a notificação de fim de emissão. Cancela-se então a espera (linha 23), o que atualiza a interface gráfica;
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 da solicitação (pode-se ver que as respostas chegaram em 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: tempo de espera observado pelo servidor;
- erro: código de erro devolvido pelo servidor (0 = sem erro);
- mensagem: mensagem de erro devolvida pelo servidor (null = sem erro);
- observedAt: hora em que o valor foi observado;
- observedOn: thread de observação do valor observado;
9.3.8.2. Exemple-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 {
// mapeadores jSON
private ObjectMapper mapperAleasUiResponse;
// construtor
public Example02Fragment() {
super();
Log.d("rxjava", "Example02Fragment constructor");
// filtro jSON
mapperAleasUiResponse = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
// solicitação de números aleatórios
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// preparação do pedido
Request request = session.getRequest();
request.setId(i);
// só se mantêm os observáveis com um número de cliente par
observable = observable
.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
})
// execução no thread de E/S
.subscribeOn(Schedulers.io()));
}
// observação no thread do evento loop
observable = observable.observeOn(AndroidSchedulers.mainThread());
// executam-se estes observáveis
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() {
// fim da espera
cancelWaiting();
}
}));
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// extraímos a informação a apresentar
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 é análogo ao anterior (linha 38). No entanto, das observáveis obtidas no exemplo anterior, apenas se mantêm as observáveis com um número de cliente par (linhas 42-46), graças ao método [filter] (linha 41).
Os resultados obtidos são os seguintes (para 10 pedidos):

9.3.8.3. Exemple-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 {
// mapeadores jSON
private ObjectMapper mapper;
// construtor
public Example03Fragment() {
super();
Log.d("rxjava", "Example03Fragment constructor");
// filtro jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// solicita-se os números aleatórios
Observable<List<Integer>> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// preparação da solicitação
Request request = session.getRequest();
request.setId(i);
// configuração observável
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();
}
})
// execução no thread de E/S
.subscribeOn(Schedulers.io()));
}
// observação no thread do evento loop
observable = observable.observeOn(AndroidSchedulers.mainThread());
// executam-se estes observáveis
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() {
// fim da espera
cancelWaiting();
}
}
));
}
private String getDataFrom(List<Integer> aleas) {
// extraímos a informação a apresentar
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: definem-se os mesmos observáveis que no Exemplo-02;
- linha 45: cada um dos valores emitidos pelos observáveis anteriores é transformado, através do método [map], num tipo List<Integer>, que corresponde à lista de números aleatórios gerados pelo servidor;
- linha 58: agora, o valor observado é do tipo List<Integer>;
O resultado obtido para 10 pedidos é o seguinte:

9.3.8.4. Exemple-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 {
// mapeadores jSON
private ObjectMapper mapper;
// construtor
public Example04Fragment() {
super();
Log.d("rxjava", "Example04Fragment constructor");
// filtro jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// solicita-se os números aleatórios
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// preparação da solicitação
Request request = session.getRequest();
request.setId(i);
// configuração de observáveis
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());
}
})
// execução num thread de E/S
.subscribeOn(Schedulers.io()));
}
// observação num thread do evento loop
observable = observable.observeOn(AndroidSchedulers.mainThread());
// executamos estes observáveis
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() {
// fim da espera
cancelWaiting();
}
}
));
}
}
Este exemplo é semelhante ao Exemplo-03, com a diferença de que, na linha 42, em vez de utilizar o método [map], utiliza-se o método [flatMap].
- linha 55: note-se que, a partir de agora, o tipo do valor observado é Integer;
Para 10 consultas, obtêm-se os seguintes resultados:

Desta vez, temos mais valores observados do que consultas.
9.3.8.5. Exemple-05
Apresentamos agora o procedimento a seguir para adicionar um novo exemplo de observáveis à aplicação.
Suponhamos que se pretenda reproduzir o exemplo [Exemple22h] do parágrafo 7.6.4:
package dvp.rxjava.observables.exemples;
import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;
public class Exemple22h {
public static void main(String[] args) throws InterruptedException {
// processo
Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
// subscrições
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, reunidos num único observável pelo método [concatMap] (linha 12);
passo 1
Cria-se um novo exemplo no ficheiro [exemples.xml]:
![]() |
<!-- exemplos -->
<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
Duplicamos a classe [Example04Fragment] para [Example05Fragment]. Neste caso, o nome é imposto.
Passo 3
Altera-se o código de [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 {
// mapeadores jSON
private ObjectMapper mapper;
// construtor
public Example05Fragment() {
super();
Log.d("rxjava", "Example05Fragment constructor");
// filtro jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
// instâncias das interfaces funcionais
// filtro
Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
};
// flatMap
Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
};
// groupBy
Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer integer) {
return integer % 2 == 0;
}
};
// concatMap
Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
return integerIntegerGroupedObservable.asObservable();
}
};
// solicitam-se os números aleatórios
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// preparação do pedido
Request request = session.getRequest();
request.setId(i);
// configuração observável
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
.groupBy(groupBy).concatMap(concatMap)
// execução num thread de E/S
.subscribeOn(Schedulers.io());
}
// observação num thread do evento loop
observable = observable.observeOn(AndroidSchedulers.mainThread());
// executamos estes observáveis
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() {
// fim da espera
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 assim um observável do tipo Observable<GroupedObservable<Boolean, Integer>> que emite, portanto, elementos do tipo GroupedObservable<Boolean, Integer>;
- linha 68: o método [concatMap] irá produzir elementos do tipo Integer a partir dos elementos do tipo GroupedObservable<Boolean, Integer>;
- linhas 32-59: para tornar mais legível a criação do observável nas linhas 67-69, isolámos as instâncias das interfaces funcionais de que os diferentes operadores [filter, flatMap, groupBy, concatMap] necessitam;
- linhas 47-52: o método [groupBy] espera um parâmetro do tipo Func1<T,K>, em que T é o tipo dos elementos agrupados e K é o tipo do critério de agrupamento. A partir do elemento T, a instância Func1<T,K> é responsável por gerar a chave de agrupamento K do 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, consoante o elemento deva ser colocado num grupo ou noutro. No final, obtêm-se dois grupos: o grupo dos elementos pares com a chave true e o grupo dos 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 será, neste caso, o tipo emitido pelo operador [groupBy], neste caso um tipo GroupedObservable<Boolean, Integer>;
- linha 57: a partir do elemento do tipo [GroupedObservable<Boolean, Integer>], é produzido um tipo Observable<Integer>. Como o operador [groupBy] produziu dois grupos, o operador [concatMap] irá produzir dois observáveis do tipo [Observable<Integer>]. Tal como o [flatMap], irá fundi-los num único observável. Mas, ao contrário do [flatMap], não mistura os elementos dos observáveis fundidos. Por isso, devemos observar dois grupos isolados: os números aleatórios pares e os restantes ímpares.
Etapa 4
Executamos a aplicação:

e obtêm-se 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 os campos do formulário que configura as solicitações enviadas ao servidor de números aleatórios.
9.3.9. Conclusão
Criámos no ambiente Android a seguinte arquitetura:
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] efetuava n pedidos HTTP ao servidor de números aleatórios e a camada [swing] aguardava de forma assíncrona os resultados desses pedidos para os apresentar. Essas n solicitações HTTP eram feitas ao mesmo servidor, que fornecia os mesmos tipos de respostas. Isso permitiu-nos fundir (mergeWith) as respostas num único observável.
Na realidade, as aplicações Android comunicam com servidores diferentes e, provavelmente, não se irão fundir as respetivas respostas. As solicitações HTTP a esses servidores serão geridas de forma independente umas das outras e os seus resultados observados por métodos separados.

















































