Skip to content

9. RxJava no ambiente Android

9.1. Introduction

Aqui, retomaremos um aplicativo já abordado em vários documentos:

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

Trata-se de um aplicativo cliente/servidor em que o servidor fornece, de forma assíncrona, números aleatórios que o cliente Android exibe:

  • 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 do documento 2, mas simplificada pelo uso das anotações da biblioteca Android Annotations;

O cliente Android é o seguinte:

A camada [DAO] se comunica com o servidor que gera os números aleatórios exibidos pelo tablet Android. Esse servidor possui a seguinte arquitetura de duas camadas:

Os clientes consultam certos 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

  • sua camada [métier];
  • seu serviço [web / JSON] implementado com Spring MVC;

O cliente Android

  • sua camada [DAO];
  • sua atividade;
  • suas visualizações;

9.2. O serviço web / jSON

Observação: o serviço web / jSON é implementado pela tecnologia Spring MVC. O leitor que não estiver familiarizado com essa tecnologia pode:

  • limitar-se a ler o parágrafo 9.2.1, que explica como iniciar o servidor e como consultá-lo;
  • consultar o documento [Spring MVC et Thymeleaf par l'exemple], especialmente 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 possui a seguinte arquitetura:

Essa arquitetura é implementada pelo seguinte projeto do IntelliJ IDEA: [1]:

O servidor é iniciado pelo [2-3]. Em seguida, são exibidos registros no console:

2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • 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 por meio de uma operação HTTP GET. Seus parâmetros são os seguintes:
    • [a,b]: intervalo de geração de números aleatórios;
    • [minCount, maxCount]: count números aleatórios são gerados, sendo que count é um número aleatório no intervalo [minCount, maxCount];
    • [minDelay, maxDelay]: o serviço aguarda delay milissegundos antes de retornar os números solicitados, sendo que delay é um número aleatório no intervalo [minDelay, maxDelay];

Em um navegador, vamos solicitar esse URL:

 

Foi solicitado:

  • 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 registrou;
  • 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 arquivo [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 indicando como este arquivo de configuração foi gerado;
  • linhas 4 e 10: uma dependência do framework [Spring Boot], um ramo do ecossistema Spring. Esse framework [http://projects.spring.io/spring-boot/] permite uma configuração mínima do Spring. De acordo com os arquivos presentes no Classpath do projeto, o [Spring Boot] infere uma configuração plausível ou provável para o projeto. Assim, se o Hibernate estiver no Classpath do projeto, o [Spring Boot] inferirá que a implementação JPA utilizada será o Hibernate e configurará o Spring nesse sentido. O desenvolvedor não precisa mais fazer isso. Resta a ele, então, apenas realizar as configurações que o [Spring Boot] não fez por padrão ou aquelas que o [Spring Boot] fez por padrão, mas que precisam ser especificadas. Em todos os casos, é a configuração feita pelo desenvolvedor que prevalece;
  • linhas 14-15: dois plug-ins do Gradle necessários para utilizar o conteúdo deste arquivo Gradle;
  • linhas 17-20: definem as características do arquivo gerado para este projeto;
  • linhas 22-23: para compatibilidade com Java 8;
  • linhas 25-27: as dependências serão buscadas no repositório global do Maven ou no repositório local da máquina;
  • linha 30: define uma dependência do artefato [spring-boot-starter-web]. Esse artefato traz consigo todos os arquivos necessários para um projeto Spring MVC. Entre eles está o arquivo de um servidor Tomcat. É ele que será utilizado para implantar a aplicação web. Observe-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]:

Vamos examinar as dependências trazidas pelo arquivo [build.gradle] no [4]:

 

São muitas. O Spring Boot para web incluiu as dependências de que uma aplicação web Spring MVC provavelmente precisará. Isso significa que algumas podem ser desnecessárias. O Spring Boot é ideal para um tutorial:

  • ele traz as dependências das quais provavelmente precisaremos;
  • veremos que ele simplifica consideravelmente a configuração do projeto Spring MVC;
  • ele traz um servidor Tomcat embutido [1], o que nos poupa de ter que implantar a aplicação em um servidor web externo;
  • permite gerar um JAR executável que inclui todas as dependências acima. Esse JAR pode ser transferido de uma plataforma para outra sem necessidade de reconfiguração.

É possível encontrar diversos 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 para o 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 que n é, por sua vez, um número aleatório no intervalo [minCount, maxCount]
  // os números são gerados após um intervalo de espera 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);
}

Essa interface é praticamente idêntica àquela analisada no ambiente Swing no parágrafo 8.4. Na linha 8, o método [getAleas] retorna 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]
    // n números são gerados, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
    // os números são gerados após um intervalo de espera 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 faremos comentários sobre a classe: ela é análoga à encontrada no ambiente Swing no parágrafo 8.4. Destacamos apenas os seguintes pontos:

  • linha 10: a anotação Spring [@Service], que fará com que o Spring instancie a classe em uma única instância (singleton) e disponibilize sua referência para outros componentes do Spring. Outras anotações do Spring poderiam ter sido utilizadas aqui para o mesmo efeito;
  • linhas 13-14: injeta-se um mapeador jSON. O Spring é um contêiner de objetos. Esse contêiner é instanciado no início da aplicação web e os objetos definidos por um arquivo de configuração são então instanciados, por padrão, em uma única instância (singleton). Um singleton do Spring pode conter referências a outros objetos do Spring. É o que ocorre aqui: o singleton [metier] (linhas 10-11) terá uma referência ao singleton [mapper] (linhas 13-14). Isso é chamado de injeção de dependência. Existem duas maneiras de injetar um singleton em outro singleton:
    • pelo tipo: isso é possível se o singleton a ser injetado for o único objeto Spring com esse tipo. É o caso aqui para a injeção nas linhas 13-14 (tipo ObjectMapper);
    • pelo nome, caso vários objetos Spring tenham 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 – Visão – Controlador) da seguinte maneira:

O processamento de uma solicitação de um cliente ocorre da seguinte maneira:

  1. solicitação — as URL solicitadas têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa as URL recebidas. Ela “encaminha” o URL para a ação que deve processá-lo. Essas ações são métodos de classes específicas chamadas [Contrôleurs]. O C de MVC é, neste caso, a string [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);
  1. processamento
  • a ação selecionada pode utilizar os parâmetros parami que o servlet [Dispatcher Servlet] lhe transmitiu. Esses parâmetros podem provir de várias fontes:
    • do caminho [/param1/param2/...] do URL,
    • dos parâmetros [p1=v1&p2=v2] do URL,
    • de parâmetros enviados pelo navegador junto com sua solicitação;
  • no processamento da solicitação do usuário, a ação pode precisar das camadas [metier] e [2b]. Uma vez processada a solicitação do cliente, ela pode gerar diversas respostas. Um exemplo clássico é:
    • uma página de erro, caso a solicitação não tenha sido processada corretamente
    • uma página de confirmação, caso contrário
  • a ação solicita que uma determinada vista seja exibida: [3]. Essa vista exibirá dados chamados de modelo da vista. Esse é o M de MVC. A ação criará esse modelo M [2c] e solicitará que uma vista V seja exibida [3];
  1. resposta — a vista V selecionada utiliza o modelo M criado pela ação para inicializar as partes dinâmicas da resposta HTML que ela deve enviar ao cliente e, em seguida, envia essa resposta.

Para um serviço web / JSON, a arquitetura anterior é ligeiramente modificada:

  • em [4a], o modelo, que é uma classe Java, é transformado em uma string JSON por uma biblioteca JSON;
  • em [4b], essa sequência JSON é enviada ao navegador;

Voltemos à camada [web] do nosso aplicativo:

Em nosso aplicativo, há apenas um controlador:

  

O serviço web /JSON enviará aos seus clientes uma resposta do tipo [AleasResponse] da seguinte forma:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // 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, portanto, 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ócios
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // números aleatórios em [a,b]
    // n números são gerados, sendo n um número no intervalo [minCount, maxCount]
    // os números são gerados após um intervalo de espera 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ócios 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());
        }
        // retornando a resposta jSON
        return mapper.writeValueAsString(response);
    }
}
  • linha 16: a anotação [@Controller] transforma a classe [AleasController] em um singleton do Spring. Além disso, ela indica que a classe contém métodos que processarão solicitações para determinadas URL da aplicação web. Aqui, há apenas uma na linha 29;
  • linhas 20-21: a anotação [@Autowired] solicita que o Spring injete no campo um componente do tipo [IMetier]. Essa será a classe [Metier] mencionada anteriormente. É porque colocamos nessa classe a anotação [@Service] que ela é tratada como um componente do Spring;
  • linhas 22-23: a anotação [@Autowired] solicita que o Spring injete no campo um componente do tipo [ObjectMapper]. Definiremos esse componente em breve;
  • linha 31: o método [getAleas] gera os números aleatórios. Seu nome não tem importância. Quando ele é executado, os parâmetros das linhas 31-33 já foram inicializados pelo Spring MVC. Veremos como isso ocorre. Além disso, se ele for executado, é porque o servidor web recebeu uma solicitação 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á ao cliente. Aqui, vamos enviar a ele uma sequência de caracteres que será a sequência jSON de um tipo [AleasResponse];
  • linha 29: o URL processado tem o formato /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, onde {x} representa uma variável. Essas diferentes variáveis são atribuídas aos parâmetros do método nas linhas 32-33. Isso é feito por meio da anotação @PathVariable("x"). Observe-se que os valores {x} são componentes de um URL e, portanto, são do tipo String. A conversão de String para o tipo dos parâmetros do método pode falhar. Nesse caso, o Spring MVC lança uma exceção. Resumindo: se, em um 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. Vale lembrar 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] é retornada na forma de uma string jSON;

9.2.5. Configuração do projeto Spring

  

Existem várias maneiras de configurar o Spring:

  • com arquivos XML;
  • com código Java;
  • com uma combinação dos dois;

Optamos por configurar nossa aplicação web com código Java. É a classe [Config] acima que realiza essa 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: informamos ao Spring em quais pacotes ele encontrará objetos para instanciar. Ele encontrará dois:
    • a classe [Metier] anotada por [@Service];
    • a classe [AleasController], anotada por [@Controller];
  • linha 16: a anotação [@EnableWebMvc] gera configurações automáticas para o framework Spring MVC;
  • linhas 19-20: injeção do contexto Spring (contêiner de objetos Spring). Essa injeção é necessária porque o objeto das linhas 22-26 precisa dela;
  • o arquivo de configuração do Spring pode definir novos objetos Spring por meio de 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 esse servlet processa todas as URL;
  • linhas 33-36: é a presença desse bean que ativará o servidor Tomcat presente nos arquivos do projeto. Ele aguardará as solicitações na porta 8080;
  • linhas 39-42: um mapeador jSON. Foi ele 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 do aplicativo
    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 iniciará o aplicativo. Seu primeiro parâmetro é a classe Java que configura o projeto. Neste caso, a classe [Config] que acabamos de descrever. O segundo parâmetro é o array de argumentos passado ao método [main] (linha 7). Aqui, não haverá argumentos;

Para a execução propriamente dita, o leitor deve consultar o parágrafo 9.2.1.

9.3. O cliente Android

Observação: o projeto Android a seguir é bastante complexo. Ele requer bons conhecimentos sobre Android, que podem ser encontrados, por exemplo, em [Introduction à la programmation de tablettes Android avec Android Studio ].

Atividade

Visualizações

Camada

[DAO]

Usuário

Servidor

O cliente terá dois componentes:

  1. uma camada [Présentation] (visualizações + atividade);
  2. uma camada [DAO] que se comunica com o serviço [web / JSON], que analisamos anteriormente.

9.3.1. RxAndroid

Para se comunicar de forma assíncrona com o servidor de números aleatórios, o cliente Android utilizará a biblioteca RxAndroid. Essa biblioteca estende a RxJava ao ambiente Android. Assim como foi feito para o aplicativo Swing, utilizaremos apenas uma única extensão fornecida pela RxAndroid: a do agendador [AndroidSchedulers.mainThread()]. Uma interface gráfica do Android segue as mesmas regras que uma interface Swing:

  • os eventos são processados em uma única thread chamada event loop ou thread da UI;
  • quando um evento aciona ações assíncronas, os resultados dessas ações devem ser recuperados na thread da UI caso sejam necessários para atualizar a UI;

O cliente Android:

  • enviará várias solicitações assíncronas ao servidor de números aleatórios. Essas solicitações serão executadas no lado do cliente com os threads do agendador [Schedulers.io()];
  • essas solicitações assíncronas retornarão observáveis que serão mesclados em um único (merge);
  • esse observável será monitorado no lado do cliente no agendador [AndroidSchedulers.mainThread()], fornecido pelo RxAndroid;

9.3.2. O projeto IntelliJ IDEA

O projeto Android se chama [client]:

Ele será executado por meio do [2].

Observação: a execução depende muito da configuração do IntelliJ IDEA utilizado. É provável que a execução do [2] acima não funcione à primeira tentativa em um computador diferente do meu. Configurar corretamente o IntelliJ IDEA para executar este projeto pode ser uma tarefa desafiadora para iniciantes. Aqui estão alguns pontos a serem observados:

  • no [3], acesse a estrutura do projeto;
  • no [4-5], o JDK e os SDK do Android instalados no meu computador. Observe que o JDK 1.8 não é indispensável. O Android não suporta certas funcionalidades do Java 8, incluindo as lambdas. Portanto, para instanciar interfaces funcionais, usaremos 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 arquivo [build.gradle] [6] que configura o projeto Android é o seguinte:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // substitua 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 arquivos SDK do Android presentes, as versões nas linhas 8, 24-25 e 29 podem precisar ser alteradas.

Para instalar novos SDK Android, use o SDK Manager da seguinte maneira [1]:

O projeto foi configurado para:

  • o SDK API 23 [2];
  • o SDK Ferramentas de compilação 23.0.3 [3];
  • o SDK Tool 25.1.3 [4]

Por fim, verifique o caminho do SDK Android no arquivo [local.properties] [4], linha 11 abaixo:


## Este arquivo é gerado automaticamente pelo Android Studio.
# Não modifique este arquivo -- YOUR CHANGES WILL BE ERASED!
#
# Este arquivo deve *NOT* ser registrado nos sistemas de controle de versão,
# pois contém informações específicas da sua configuração local.
#
# Localização do SDK. Isso é usado apenas pelo Gradle.
# Para personalização ao usar um sistema de controle de versão, 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

Depois que um ambiente adequado para o projeto for criado, ele poderá ser executado da seguinte maneira:

  • no [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 nomeada [app];
  • em [2], ela corresponde à execução do módulo denominado [app];
  • em [4], solicita-se que, durante a execução, o IDE sugira um dispositivo de execução. Aqui, será sempre o emulador Genymotion;
  • no [5], indica-se que esse dispositivo seja mantido para todas as execuções da configuração;

A execução do projeto no emulador Genymotion começa com a seguinte linha inicial:

Image

Para saber o que inserir em [1], abra uma janela de comando 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 você tiver um firewall do Windows, provavelmente precisará desativá-lo para que o emulador do Android consiga acessar o servidor de números aleatórios.

A execução das consultas assíncronas com as informações acima resulta nos seguintes resultados:

Image

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 no lado do cliente;
  • requestAt: hora da solicitação;
  • responseAt: hora de recebimento da resposta;
  • delay: o tempo de espera que o servidor observou antes de enviar 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 usuário;

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 precisa de dependências que registramos no arquivo [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 e 3 são dependências padrão de um projeto Android com o SDK 23;
  • A dependência da linha 5 traz o objeto Spring [RestTemplate], que gerencia a comunicação da camada [DAO] com o servidor;
  • a dependência da linha 6 inclui a biblioteca JSON e [Jackson], utilizadas pelo aplicativo;
  • a dependência da linha 7 traz a biblioteca RxAndroid (e, com ela, a biblioteca RxJava), que a camada Ui utiliza para se comunicar com a camada [DAO];

9.3.5. O manifesto do aplicativo 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 que n é, por sua vez, um número aleatório no intervalo [minCount, maxCount]
  // os números são gerados após um intervalo de espera 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 máximo de espera (ms) para a resposta do servidor a uma solicitação de conexão
  // tempo máximo de espera (ms) para a resposta do servidor a uma solicitação
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • linha 12: o método da camada [DAO] que gera números aleatórios de forma assíncrona;
  • linha 15: para 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, a fim de evitar um tempo de espera excessivamente longo quando o servidor não responde;

O método [getAleas] recebe todos os seus parâmetros no objeto [Request] a seguir:


package android.aleas.fragments;

public class Request {

  // n.º da solicitação
  int id;
  // entradas do usuário
  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 ser consultado.

O método [getAleas] retorna um tipo Observable<AleasDaoResponse>, em que a classe [AleasDaoResponse] é a seguinte:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // 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 da thread de execução
  private String on;
  // hora da solicitação
  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 da solicitação;
  • linha 15: hora da resposta;
  • linha 17: número da solicitação;

Os campos [on, requestAt, idClient] são inicializados pelo cliente no início da solicitação. O campo [responseAt] é inicializado quando o cliente recebe a resposta do servidor.

9.3.6.2. Implementação da camada [DAO]

  

A interface [IDao] é implementada com a seguinte classe [Dao]:


package android.aleas.dao;

import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class Dao implements IDao {

  // 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 garantirá 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 string jSON enviada pelo servidor de números aleatórios;
  • linhas 30-33: o construtor da classe;
  • linha 32: o mapeador jSON da linha 27 é criado;

O método [setClientTimeouts] é o seguinte:


  // 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 inicializamos. É o método [setClientTimeouts] que faz isso;
  • linha 8: a classe [HttpComponentsClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Ela nos permitirá 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 para a comunicação com o serviço web. Passamos a ele, como parâmetro, o objeto [factory] que acabou de ser criado;
  • linha 12: o diálogo cliente/servidor pode assumir diversas formas. As trocas ocorrem por meio de linhas de texto e precisamos indicar ao objeto do tipo [RestTemplate] o que ele deve fazer com essa linha de texto. Para isso, fornecemos a ele conversores, ou seja, classes capazes de processar as linhas de texto. A escolha do conversor geralmente é feita por meio dos cabeçalhos HTTP que acompanham a linha de texto. De acordo com esses cabeçalhos, o objeto [RestTemplate] selecionará, entre 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 nenhuma transformação.

O método [getAleas] é o mais complexo:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // 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());
          // solicitação HTTP síncrona
          String response = executeRestService("get", urlService, null);
          // desserializaçã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 {
            // registra-se a hora de recebimento
            clientState.setResponseAt();
            // o resultado é encaminhado ao assinante
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // encaminha-se a exceção ao assinante
          subscriber.onError(ex);
        } finally {
          // é sinalizado o fim do observável
          // na execução, percebe-se que esse método não tem efeito algum se o método [onError] tiver sido chamado     anteriormente — conforme a teoria —, portanto, essa instrução poderia ser colocada apenas no try
          subscriber.onCompleted();
        }
      }
    });
  }
  • linha 2: é preciso lembrar que devemos gerar um tipo [Observable<AleasResponse>];
  • linha 3: uma linha de log no console do Android;
  • linha 5: o objeto [RestTemplate] garante um diálogo síncrono com o servidor. Isso significa que a thread de execução que faz a solicitação fica bloqueada até o recebimento da resposta. No exemplo do Swing, vimos como transformar uma ação síncrona em uma ação assíncrona por meio do método [Observable.create]. É esse mesmo caminho que seguimos aqui;
  • linha 7: o método [call] da interface [Observable.OnSubscribe<AleasDaoResponse>] da linha 5. É esse método que é chamado quando um observador se inscreve no observável;
  • linhas 10-12: construção do URL do serviço de números aleatórios;
  • linha 14: inicialização do objeto [ClientState]. Aqui, registra-se a hora da solicitação;
  • linha 16: solicitação síncrona HTTP. Obtém-se uma resposta jSON. O método [executeRestService] espera três parâmetros:
      1. o método HTTP a ser utilizado para consultar o serviço;
      2. o URL do serviço;
      3. o objeto a ser enviado do tipo Object, null caso o método HTTP não seja POST;
  • 18-21: deserialização da cadeia jSON recebida em um tipo [AleasServerResponse]. Esse 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, uma exceção é encaminhada ao assinante;
  • linha 29: atualiza-se [clientState], que fará parte da resposta enviada ao assinante;
  • linhas 31-33: envio da resposta ao assinante. Ela 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 de fim de transmissão;

9.3.7. As visualizações do aplicativo

  

O aplicativo apresenta as duas visualizações a seguir:

A visualização da consulta

Image

A visualização da resposta

Image

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 única atividade do aplicativo;
  • linha 11: para se comunicarem entre si, os fragmentos utilizam uma sessão;
  • linha 13: antes de exibir um fragmento, ele será solicitado a atualizar-se com o conteúdo da sessão. Esse método é declarado abstrato, pois é implementado pelas classes derivadas. Por esse motivo, a própria classe é declarada abstrata (linha 7);

A classe [Session] contém os dados que os diferentes fragmentos do aplicativo compartilham. Seu código é o seguinte:

  

package android.aleas.activity;

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

public class Session {

  // atividade do aplicativo
  private MainActivity activity;
  // número de solicitações
  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 usuário na lista de exemplos
  private String exampleName;
  // seu número na lista de fragmentos
  private int examplePosition;
  // o adaptador do spinner dos exemplos na visualização 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 usuário na visualização da consulta:

  

package android.aleas.fragments;

public class Request {

  // nº da consulta
  int id;
  // entradas do usuário
  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 possui os seguintes componentes:

Image

O aplicativo possui uma única visualização, que é composta por duas abas:

  • [1]: a aba da consulta;
  • [2]: a aba da resposta;

Os componentes do fragmento [RequestFragment] são os seguintes:

n.º
Tipo
Nome
Função
3
EditText
edtNbRequests
número de solicitações a serem feitas 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;

Erros de digitação são sinalizados:

Image

Os componentes 1 a 6 são componentes [TextView] com os seguintes nomes (na ordem): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. O fragmento [ResponseFragment] da resposta

O fragmento da resposta possui os seguintes componentes:

Image

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 as solicitações ao servidor

9.3.7.4. A atividade do Android [MainActivity]

  

A classe [MainActivity] exibe a seguinte visualização []:


<?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 aplicativos -->
  <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>

    <!-- contêiner de abas -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- contêiner de visualizações -->
  <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 visualização são os seguintes:

linhas
Tipo
Nome
Função
20-34
Barra de ferramentas
barra de ferramentas
barra de ferramentas do aplicativo
29-34
ProgressBar
loadingPanel
imagem de espera exibida enquanto a solicitação do usuário está sendo processada
37-40
TabLayout
abas
a barra de abas do aplicativo
44-51
MyPager
contêiner
o contêiner no qual são exibidos os diferentes fragmentos do aplicativo

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 {

  // controle de 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) {
    // deslize permitido?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // deslize 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]. Utilizamos a classe [MyPager] em vez da classe [ViewPager] apenas porque queremos desativar o deslize: por padrão, com a classe [ViewPager], é possível alternar entre as abas com um deslize (deslizando para a esquerda ou para a direita). Aqui, não queremos esse comportamento;
  • linha 11: a variável booleana que 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 do aplicativo:

o fato de a atividade implementar a interface da camada [DAO] permite que as visualizações não tenham conhecimento da camada [DAO]: seus gerenciadores de eventos se comunicarão com a camada [activité] quando quiserem interagir com o servidor.

  • linha 24: uma referência à camada [DAO] inicializada pelo construtor da linha 35;
  • linha 26: uma referência à sessão compartilhada pelos fragmentos, inicializada pelo construtor da linha 33;
  • linhas 46-59: implementação da interface [IDao];

A classe [MainActivity] inicializa os componentes da visualização a ela associada da seguinte maneira:


  // barra de ferramentas
  private Toolbar toolbar;
  // gerenciador de fragmentos
  private MyPager mViewPager;
  // contêiner de abas
  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, há apenas uma aba
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gerenciador de eventos
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // uma aba foi selecionada — alteramos o fragmento exibido pelo contêiner de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // aba de consulta
          showView(0);
        } else {
          // aba de resposta — 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();

    // gerenciamento da imagem de espera
    loadingPanel.setVisibility(View.INVISIBLE);
}

Esse código é bastante comum em uma 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 a primeira aba com o título [Request]. Em determinado momento, 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 usuário. O fragmento exibido pela segunda aba muda, portanto, com o passar do tempo;

  • linhas 37-48: o código executado quando o usuário clica em uma das abas;
  • linha 43: exibe-se o fragmento nº 0;
  • linha 46: exibe-se o fragmento atualmente em uso (visualizado). Seu número é encontrado na sessão;
  • linha 62: criam-se os fragmentos de todos os exemplos presentes no spinner de exemplos na visualização [RequestFragment] (primeira aba);
  • linha 65: a imagem de espera está oculta no momento;

Para compreender o método [showView] (linhas 43, 46) e o método [createResponseFragments], precisamos, antes de tudo, apresentar o gerenciador de fragmentos em memória (classe incluída no arquivo Java de MainActivity):


  // o gerenciador de fragmentos — deve definir os métodos getItem, getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmentos gerenciados
    private MyFragment[] fragments;

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

    // deve retornar o fragmento na posição n.º
    @Override
    public MyFragment getItem(int position) {
      // o fragmento
      return fragments[position];
    }

    // retorna o número de fragmentos a serem gerenciados
    @Override
    public int getCount() {
      // número de fragmentos
      return fragments.length;
    }
  }
}
  • a classe [SectionsPagerAdapter] estende a classe Android [FragmentPagerAdapter]. Ela redefine dois métodos de sua classe pai:
    • o método [getItem], linha 15;
    • o método [getCount], linha 22;
  • A classe [SectionsPagerAdapter] contém todos os fragmentos do aplicativo. Estes são armazenados na linha 5. Observe-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 gerenciar;
  • linhas 14-18: o método [getItem] retorna o fragmento na posição [position];
  • linhas 21-25: o método [getCount] retorna o número total de fragmentos;

O método [createResponseFragments] cria todos os fragmentos de que o aplicativo precisa:


private void createResponseFragments() {
    // spinner de exemplos
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Especifique o layout a ser usado quando a lista de opções for exibida
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // coloca-se o adaptador na sessão para que a visualização [Request] o recupere
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • linha 3: cria-se um adaptador para o spinner dos exemplos, neste caso uma lista de String representando os nomes dos exemplos. Esses nomes estão presentes no arquivo [layout/exemples.xml]:
  

O arquivo [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>

Linha 1: este arquivo é o segundo parâmetro do método [createFromResource]. No [R.array.exemples], [exemples] é o nome da tabela (linha 3 acima), e não o nome do arquivo.

  • linha 5: associa-se um layout (gerenciador de exibição) ao adaptador. Agora, o adaptador possui tanto os dados quanto o modo de exibição deles;
  • linha 7: colocamos o adaptador em sessão. É daí que o fragmento [RequestFragment], que precisa dele, irá recuperá-lo;

Vamos continuar 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);
    // Especifique o layout a ser usado quando a lista de opções for exibida
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // colocamos o adaptador na sessão para que a visualização [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++) {
      // construímos o nome do fragmento a ser instanciado, correspondente ao exemplo escolhido pelo usuário
      // esse nome deve ser o nome completo com seu pacote — aqui, ele 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 — ele é inserido na matriz
      tFragments[i] = fragment;
    }
    // instanciação do gerenciador de fragmentos com esses novos fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Configurar o ViewPager com o adaptador de seções.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navegação entre páginas — esta instrução é importante
    // aqui se diz que, em ambos os lados da visualização exibida, é preciso manter as visualizações inicializadas [tFragments.length]
    // isso implica que, neste caso, todos os fragmentos utilizados pelo aplicativo estejam na memória e inicializados
    // se isso não for feito, por padrão, o [OffscreenPageLimit] será igual a 1
    // assim, se o fragmento visualizado for o nº 3, apenas os fragmentos 2 e 4 serão inicializados
    // isso ocorre por meio da chamada do método [onCreateView] desses dois fragmentos — o que significa que, nesse método, é preciso prever
    // regenerar a aparência que o fragmento tinha na última vez em que foi utilizado — além disso, não deve haver, nesse método,
    // haja código que não suportaria ser executado duas vezes — isso cria uma bagunça enorme e é complexo de gerenciar
    // aqui preferimos evitar essas dificuldades — nos logs, vemos que, ao iniciar o aplicativo, todos os fragmentos são criados
    // e seu método [onCreateView] é executado — ele nunca mais é executado depois disso —
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // desativamos o deslize entre fragmentos
    mViewPager.setSwipeEnabled(false);
  }
  • linha 9: criação da matriz que conterá todos os fragmentos do aplicativo;
  • linha 11: o primeiro fragmento é o da solicitação;
  • linhas 13-28: vamos criar tantos fragmentos quantos forem os exemplos. Todos esses fragmentos estendem o fragmento da resposta [ResponseFragment] e implementam apenas o que é específico do exemplo: a criação dos valores observados. Esses valores, de fato, diferem de um exemplo para outro;
  • linha 16: o fragmento de um exemplo possui um nome padrão: ExampleXXFragment, em que XX é sua posição no spinner de exemplos mais 1. XX é também o número do fragmento do exemplo no gerenciador 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 possui apenas um construtor. Portanto, será obtida uma referência a ele;
    • 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 uma matriz de objetos vazia;
  • linha 27: esse fragmento é adicionado à matriz de fragmentos;
  • linha 30: vimos que o construtor do gerenciador de fragmentos [SectionsPagerAdapter] esperava, em seus parâmetros, a matriz de fragmentos que deveria gerenciar. É agora que a passamos a ele;
  • linha 22: o contêiner de fragmentos [mViewPager] da visualização associada à atividade [MainActivity] é aqui associado ao gerenciador de fragmentos: o contêiner de fragmentos [mViewPager] exibe os fragmentos do gerenciador de fragmentos;
  • linha 43: vamos ler os comentários — a instrução significa que todos os fragmentos devem permanecer no estado em que o código os coloca, independentemente do fragmento exibido no momento. Assim, quando voltamos a ele, o encontramos no estado em que o deixamos;
  • linha 45: o contêiner de fragmentos [mViewPager] é do tipo [MyPager], que permite inibir o deslize;

O método [MainActivity.showView] é o seguinte:


  // exibição da visualização n.º [position]
  private void showView(int position) {
    // atualiza-se o fragmento antes de sua exibição
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // exibe-se a visualização solicitada — vai-se diretamente para a visualização (segundo parâmetro definido como false)
    // sem esse parâmetro, acessa-se por padrão a visualização desejada, exibindo rapidamente as visualizações intermediárias — comportamento indesejável
    mViewPager.setCurrentItem(position, false);
}
  • linha 3: deseja-se exibir o fragmento na posição n°;
  • linha 4: esse fragmento é solicitado ao gerenciador de fragmentos e, em seguida, atualizado. De fato, desde a última vez em que foi exibido, a sessão pode ter mudado. O fragmento deve, então, inspecionar a sessão para verificar se precisa ser atualizado;
  • linha 7: o fragmento é exibido pelo [ViewPager]. Como este foi associado ao gerenciador de fragmentos, será exibido o fragmento nº [position], aquele que acabamos de atualizar na linha 4;

Vamos concluir com os dois métodos de gerenciamento da espera:


  public void beginWaiting() {
    // gerenciamento da imagem de espera
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // gerenciamento 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 solicitações
  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;
  // spinner 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 visualização do fragmento a partir de 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, sem 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: as entradas feitas pelo usuário no formulário;
  • o construtor (linhas 55-58) e o método [onCreateView] são executados quando a atividade [MainActivity] cria todos os fragmentos do aplicativo. Isso ocorre apenas uma vez;
  • linha 61: o código do método [onCreateView] é padrão. Observe, na linha 102, que o adaptador do spinner dos exemplos é obtido na sessão. Observe também a linha 91: o clique no botão [Exécuter] é tratado 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()) {
      // as informações são armazenadas na sessão
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // armazenamos 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 andamento
      session.setOnAir(true);
      // mas ainda não iniciada
      session.setOperationStarted(false);
      // exibindo o fragmento da resposta
      activity.selectTab(Constants.VUE_RESPONSE);
      // inicia-se a espera
      beginWaiting();
    }
}
  • linha 15: não comentaremos o método [ispageValid]. Ele verifica a validade das entradas e retorna “true” somente se todas forem válidas. Nesse caso, elas são utilizadas para inicializar os campos das linhas 2 a 9;
  • linha 17: as diferentes entradas são inseridas na sessão:
    • [spinnerExemples.getSelectedItem().toString()] é o nome do exemplo selecionado pelo usuário e é armazenado em [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] é o número do fragmento associado ao exemplo e que foi armazenado (o fragmento) pelo gerenciador de fragmentos. Esse número é armazenado 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: observa-se que uma operação está prestes a ser iniciada;
  • linha 26: a aba da resposta será exibida. Para entender o que vai acontecer, é preciso lembrar-se do código [MainActivity.selectTab]:

  // seleção de uma aba
  public void selectTab(int position) {
    // há no máximo 2 abas
    // inicialmente, há apenas uma, a da consulta
    // se a aba solicitada for a nº 1 e ela ainda não existir, é preciso criá-la
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 guia a mais
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // seleciona-se a aba por programa, o que acionará o evento [onTabSelected]
    // que associará a visualização correta a essa aba
    tabLayout.getTabAt(position).select();
}
  • inicialmente, a atividade havia criado apenas a aba da solicitação (aba nº 0);
  • linhas 6-11: cria-se a aba da resposta (aba nº 1) caso ela ainda não tenha sido criada;
  • linha 14: seleciona-se a aba nº position (0 ou 1). Isso coloca o evento [onTabSelected] na fila do loop de eventos do aplicativo Android;

O manipulador do evento [onTabSelected] em [MainActivity] é o seguinte:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // uma aba foi selecionada — altera-se o fragmento exibido pelo contêiner de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // aba de consulta
          showView(0);
        } else {
          // aba de resposta — depende do exemplo escolhido
          showView(session.getExamplePosition());
        }
}

No caso da guia [Response], é a linha 9 que é executada. O fragmento nº [session.getExamplePosition()] será exibido. Por exemplo, para o exemplo [exemple-03], o número registrado em [session.examplePosition] é 3. A linha 10 exibe, então, o fragmento nº 3. A tabela de fragmentos criada inicialmente pela atividade é [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Portanto, é o fragmento [Exemple03Fragment] que será exibido. Isso é feito pelo código a seguir:


  // exibição da visualização n.º [position]
  private void showView(int position) {
    // atualiza-se o fragmento antes de sua exibição
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // exibe-se a visualização solicitada — vai-se diretamente para a visualização (segundo parâmetro definido como false)
    // sem esse parâmetro, acessa-se por padrão a visualização desejada, exibindo rapidamente as visualizações intermediárias — comportamento indesejável
    mViewPager.setCurrentItem(position, false);
}

Vemos que o fragmento será atualizado (linha 4) antes de ser exibido (linha 7).

9.3.7.6. O fragmento [ResponseFragment]

A classe [ResponseFragment] exibe as respostas do servidor. 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 visualização do fragmento a partir de 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 ser executado (por código explícito) antes de cada exibiçã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. De fato, todos os fragmentos dos exemplos estendem o fragmento [ResponseFragment]. Durante sua instanciação, o construtor de sua classe pai [ResponseFragment] é chamado;
  • 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. Nele, encontramos código clássico de um aplicativo 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 a aba [Response] é exibida;

Graças aos diversos registros de log inseridos nos métodos importantes, é possível observar o que ocorre ao iniciar o aplicativo:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • linha 1: construção do fragmento [RequestFragment];
  • linhas 2-9: construção dos fragmentos dos 4 exemplos do aplicativo;
  • linha 10: inicialização do fragmento [RequestFragment];
  • linhas 11 a 14: inicialização dos fragmentos dos quatro exemplos do aplicativo;

Depois disso, não há mais nenhuma chamada a esses métodos.

O método [ResponseFragment.onRefresh] é o seguinte:


  // método a ser executado (por código explícito) antes de cada exibiçã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 andamento?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // execução da solicitação
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • linha 5: verifica-se se o fragmento [RequestFragment] fez uma solicitação (session.isOnAir) e se esta já foi iniciada (isOperationStarted). Se o fragmento [RequestFragment] tiver feito 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 ela é assíncrona, o usuário pode alternar entre as duas abas. Se ele voltar para a aba [Response] e houver uma operação em andamento, as linhas 7-8 não serão executadas;

O método [doExecuter], na linha 8, executa a operação solicitada pelo usuário:


  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;
    // criando e executando 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). Ele será implementado pelos fragmentos [ExampleXXFragment], que estendem a classe [ResponseFragment];
  • linha 6: a lista de assinaturas é esvaziada;
  • linha 7: a lista que exibe as respostas é esvaziada;
  • linha 8: conta o número de respostas recebidas;

As classes filhas [ExampleXXFragment] atribuem ao método [showAlea] a tarefa de exibir os elementos que elas 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: vemos que o elemento observado chega na forma de uma string. Na verdade, será a string jSON do elemento observado. Isso nos permite ter um único método para exibir o elemento observado, independentemente de seu tipo exato em Java;
  • linha 6: o elemento observado [data] é adicionado na primeira posição da lista de respostas. Assim, o usuário vê, no início da lista, as respostas mais recentes;

A espera é gerenciada pelos seguintes métodos [beginWaiting] e [cancelWaiting]:


  private void beginWaiting() {
    // colocamos a ampulheta
    activity.beginWaiting();
    // o botão [Annuler] é exibido
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // fim da espera
    activity.cancelWaiting();
    // o botão [Annuler] fica oculto
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Eles utilizam os métodos com os mesmos nomes da atividade e se limitam a mostrar/ocultar o botão [Annuler].

O clique no botão [Annuler] é gerenciado pelo código a seguir:


  protected void doAnnuler() {
    // todas as assinaturas são canceladas
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fim da espera
    cancelWaiting();
}
  • linhas 3-7: cancelam-se, uma a uma, todas as assinaturas;

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 ser enviada 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 loop de eventos;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // executa-se todos esses 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) {
        // extrai-se a informação a ser exibida
        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 mesclados (linha 43) no observável da linha 36;
  • linha 43: o observável é executado em uma thread do agendador [Schedulers.io()]. A chamada HTTP ao servidor será executada nessa thread;
  • linha 46: o observável final é observado na thread do event loop;
  • linhas 48-57: execução dos observáveis e, portanto, das solicitações ao servidor de números aleatórios. O Android ainda não oferece suporte ao Java 8 e suas lambdas. Portanto, utilizamos 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. Vale lembrar que ele espera uma sequência de caracteres. Essa sequência é fornecida pelo método [getDataFrom] das linhas 59-68;
  • linha 63: retorna-se a string jSON do tipo [AleasUiResponse] da seguinte forma:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // resposta [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // thread de observação
  private String observedOn;
  // hora da 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), são adicionadas duas informações:
    • linha 13: o thread de observação;
    • linha 15: a hora da observação;

Voltemos ao código de assinatura:


    @Override
    public void createAndExecuteObservables() {
...
        // executamos todos esses 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) {
                // exibimos a exceção
                showAlea(getMessagesFromThrowable(th));
                // após receber uma exceção, o observável não recebe nem onNext, nem onCompleted
                // é necessário cancelar a assinatura 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: utiliza-se novamente o método [showAlea] da classe pai para exibir a exceção. O método [getMessagesFromThrowable] é um método da classe pai [ResponseFragment] que, a partir de uma exceção, gera uma sequência 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: retorna-se a string jSON de uma lista de mensagens de erro (linha 4);

Voltemos ao código de assinatura do observável:

  • linhas 19-25: o código executado quando o observador recebe a notificação de fim de transmissão. Em seguida, cancela-se a espera (linha 23), o que atualiza a interface gráfica;

A execução do exemplo 01 produz um resultado semelhante ao seguinte:

Image

Cada elemento da lista é a string jSON de um valor observado. Os campos da string 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 observar que as respostas foram recebidas em ordem aleatória);
  • on: o thread de execução do observável que emitiu esse 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 retornado pelo servidor (0 = sem erro);
  • mensagem: mensagem de erro retornada pelo servidor (null = sem erro);
  • observedAt: hora da observação do valor registrado;
  • 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 da consulta
            Request request = session.getRequest();
            request.setId(i);
            // são mantidos apenas os observáveis com 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 na thread de E/S
                            .subscribeOn(Schedulers.io()));
        }
        // observação na thread do loop de eventos
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // executa-se esses 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) {
        // extrai-se a informação a ser exibida
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Este exemplo é semelhante ao anterior (linha 38). Porém, das observáveis obtidas no exemplo anterior, mantêm-se apenas aquelas com número de cliente par (linhas 42-46), por meio do método [filter] (linha 41).

Os resultados obtidos são os seguintes (para 10 consultas):

Image

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 consulta
      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 na thread de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observação na thread do loop de eventos
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executando esses 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) {
    // extrai-se a informação a ser exibida
    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 do Exemplo-02;
  • linha 45: cada um dos valores emitidos pelas observáveis anteriores é transformado, pelo método [map], em um tipo List<Integer>, que é a lista de números aleatórios gerados pelo servidor;
  • linha 58: agora, o valor observado é do tipo List<Integer>;

O resultado obtido para 10 solicitações é o seguinte:

Image

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 em um thread de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observação em thread do loop de eventos
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executando esses 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: observe-se que, a partir de agora, o tipo do valor observado é Integer;

Para 10 consultas, obtêm-se os seguintes resultados:

Image

Desta vez, temos mais valores observados do que consultas.

9.3.8.5. Exemple-05

Apresentamos agora o procedimento a ser seguido para adicionar um novo exemplo de observáveis ao aplicativo.

Suponhamos que queiramos 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()));
        // assinaturas
        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 em um único observável pelo método [concatMap] (linha 12);

etapa 1

Cria-se um novo exemplo no arquivo [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, a linha 8 foi adicionada. O nome dado ao exemplo pode ser qualquer um.

Etapa 2

Duplicamos a classe [Example04Fragment] em [Example05Fragment]. Nesse caso, o nome é predefinido.

Etapa 3

Alteramos o código de [Example05Fragment] da seguinte maneira:


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();
      }
    };
    // solicita-se a geração de números aleatórios
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparação da consulta
      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 em um thread de E/S
        .subscribeOn(Schedulers.io());
    }
    // observação em thread do loop de eventos
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executamos esses 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 esse fluxo de inteiros de acordo com um critério booleano que definiremos. Obteremos um observável do tipo Observable<GroupedObservable<Boolean, Integer>> que, portanto, emite elementos do tipo GroupedObservable<Boolean, Integer>;
  • linha 68: o método [concatMap] produzirá elementos do tipo Integer a partir 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, isolamos as instâncias das interfaces funcionais necessárias aos diferentes operadores [filter, flatMap, groupBy, concatMap];
  • 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 K de agrupamento do elemento;
  • linhas 48-51: os elementos do tipo Integer serão agrupados por paridade. A instância Func1<Integer,Boolean> gera a chave true ou false, dependendo de o elemento dever ser colocado em um grupo ou no outro. Na saída, temos 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], ou seja, um tipo GroupedObservable<Boolean, Integer>;
  • linha 57: a partir do elemento do tipo [GroupedObservable<Boolean, Integer>], é gerado um tipo Observable<Integer>. Como o operador [groupBy] produziu dois grupos, o operador [concatMap] produzirá dois observáveis do tipo [Observable<Integer>]. Assim como o [flatMap], ele os unificará em um único observável. Mas, ao contrário do [flatMap], ele não mistura os elementos dos observáveis unificados. Portanto, devemos observar dois grupos isolados: os números aleatórios pares e os demais ímpares.

Etapa 4

Executamos o aplicativo:

Image

e obtemos os seguintes resultados:

Image

  • em [1], os números aleatórios pares; em [2], os ímpares;

9.3.8.6. Para continuar

O leitor é agora convidado a criar seus próprios exemplos e também a experimentar diversos valores para os campos do formulário que configuram as solicitações enviadas ao servidor de números aleatórios.

9.3.9. Conclusão

Criamos no ambiente Android a seguinte arquitetura:

O cliente Android:

A camada [DAO] se comunica com o servidor que gera os números aleatórios exibidos pelo tablet Android. Esse servidor possui a seguinte arquitetura de duas camadas:

A camada [DAO] fazia n solicitações HTTP ao servidor de números aleatórios, e a camada [swing] aguardava de forma assíncrona os resultados dessas solicitações para exibi-los. Essas n solicitações HTTP eram feitas ao mesmo servidor, que fornecia os mesmos tipos de respostas. Isso nos permitiu mesclar (mergeWith) as respostas em um único observável.

Na prática, os aplicativos Android se comunicam com servidores diferentes e provavelmente não se fará a fusão de suas respostas. As solicitações HTTP a esses servidores serão gerenciadas independentemente umas das outras e seus resultados observados por métodos separados.