Skip to content

9. RxJava no ambiente Android

9.1. Introduction

Vamos agora retomar uma aplicação já abordada 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 uma aplicação cliente/servidor em que o servidor fornece, de forma assíncrona, números aleatórios que o cliente Android apresenta:

  • no documento 1, o cliente Android utiliza uma tecnologia não padrão;
  • no documento 2, o cliente Android utiliza a tecnologia padrão do Android para operações assíncronas;
  • no documento 3, o cliente Android utiliza a mesma tecnologia que no documento 2, mas simplificada através da utilização das anotações da biblioteca Android Annotations;

O cliente Android é o seguinte:

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

Os clientes consultam determinados URL da camada [web / JSON] e recebem uma resposta de texto no formato JSON (JavaScript Object Notation).

Vamos dividir a análise da aplicação em duas etapas:

O servidor web / jSON

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

O cliente Android

  • a sua camada [DAO];
  • a sua atividade;
  • as suas vistas;

9.2. O serviço web / jSON

Nota: o serviço web / jSON é implementado pela tecnologia Spring MVC. O leitor que não esteja familiarizado com esta tecnologia pode:

  • limitar-se a ler o parágrafo 9.2.1, que explica como iniciar o servidor e como o consultar;
  • consultar o documento [Spring MVC et Thymeleaf par l'exemple], nomeadamente o capítulo 4, que apresenta as principais anotações utilizadas no código;

9.2.1. O projeto IntelliJ IDEA

O serviço web / jSON tem a seguinte arquitetura:

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

O servidor é iniciado pelo [2-3]. São então apresentados registos na consola:

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 através de uma operação HTTP GET. Os seus parâmetros são os seguintes:
    • [a,b]: intervalo de geração de números aleatórios;
    • [minCount, maxCount]: são gerados count números aleatórios, em que count é um número aleatório no intervalo [minCount, maxCount];
    • [minDelay, maxDelay]: o serviço aguarda delay milissegundos antes de devolver os números solicitados, sendo que delay é um número aleatório no intervalo [minDelay, maxDelay];

Num navegador, vamos solicitar este URL:

 

Foram solicitados:

  • números aleatórios no intervalo [100, 200];
  • n números aleatórios com n no intervalo [10, 20];
  • um tempo de espera de x milissegundos com x no intervalo [300, 400];

Na resposta:

  • aleas: lista dos números aleatórios gerados;
  • delay: o tempo de espera em milissegundos que o servidor registou;
  • erro: um código de erro — 0 se não houver erro;
  • mensagem: uma mensagem de erro — null se não houver erro;

9.2.2. As dependências Gradle do projeto

  

O projeto [serveur] é um projeto Gradle configurado pelo seguinte ficheiro [build.gradle] [1]:


// gerado por http://start.spring.io/ (maio de 2016)
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

jar {
  baseName = 'serveur'
  version = '0.0.1-SNAPSHOT'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
}
  • linha 1: um comentário para indicar como este ficheiro de configuração foi gerado;
  • linhas 4 e 10: uma dependência do framework [Spring Boot], um ramo do ecossistema Spring. Este framework [http://projects.spring.io/spring-boot/] permite uma configuração mínima do Spring. De acordo com os ficheiros presentes no Classpath do projeto, o [Spring Boot] deduz uma configuração plausível ou provável para o mesmo. Assim, se o Hibernate estiver no Classpath do projeto, então o [Spring Boot] deduzirá que a implementação JPA utilizada será o Hibernate e configurará o Spring nesse sentido. O programador já não precisa de o fazer. Basta-lhe então efetuar apenas as configurações que o [Spring Boot] não definiu por predefinição ou aquelas que o [Spring Boot] definiu por predefinição, mas que têm de ser especificadas. Em todos os casos, é a configuração feita pelo programador que prevalece;
  • linhas 14-15: dois plugins Gradle necessários para utilizar o conteúdo deste ficheiro Gradle;
  • linhas 17-20: definem as características do arquivo gerado para este projeto;
  • linhas 22-23: para compatibilidade com o Java 8;
  • linhas 25-27: as dependências serão procuradas no repositório global do Maven ou no repositório local da máquina;
  • linha 30: define uma dependência do artefacto [spring-boot-starter-web]. Este artefacto inclui todos os arquivos necessários para um projeto Spring MVC. Entre estes encontra-se o arquivo de um servidor Tomcat. É este que será utilizado para implementar a aplicação web. Note-se que a versão da dependência não foi mencionada. Será utilizada a versão indicada no projeto importado [spring-boot];

Para atualizar o projeto, é necessário forçar o download das dependências [1-3]:

Vejamos as dependências introduzidas pelo ficheiro [build.gradle] no [4]:

 

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

  • fornece as dependências de que provavelmente iremos precisar;
  • vamos ver que simplifica consideravelmente a configuração do projeto Spring MVC;
  • inclui um servidor Tomcat incorporado [1], o que nos poupa de ter de implementar a aplicação num servidor web externo;
  • permite gerar um ficheiro JAR executável que inclui todas as dependências acima referidas. Este ficheiro JAR pode ser transferido de uma plataforma para outra sem necessidade de reconfiguração.

É possível encontrar inúmeros exemplos que utilizam o Spring Boot no site do ecossistema Spring [http://spring.io/guides]. Agora que conhecemos as dependências do projeto, podemos passar ao código.

9.2.3. A camada [métier]

  

A camada [métier] terá a seguinte interface [IMetier]:


package dvp.rxjava.server.metier;

public interface IMetier {
  // números aleatórios no intervalo [a,b]
  // são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
  // os números são gerados após um atraso de delay milissegundos,
  // onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Esta interface é praticamente idêntica à analisada no ambiente Swing no parágrafo 8.4. Na linha 8, o método [getAleas] devolve o seguinte tipo [AleasMetier]:


package dvp.rxjava.server.metier;

import java.util.List;

public class AleasMetier {
  // campos
  private int delay;
  private List<Integer> aleas;

  // construtores
  public AleasMetier(){

  }

  public AleasMetier(int delay, List<Integer> aleas){
    this.delay=delay;
    this.aleas=aleas;
  }

  public AleasMetier(AleasMetier aleasMetier){
    this.delay=aleasMetier.delay;
    this.aleas=aleasMetier.aleas;
  }

  // getters e setters
...
}

O código da classe [Metier] que implementa a interface [IMetier] é o seguinte:


package dvp.rxjava.server.metier;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class Metier implements IMetier {

  @Autowired
  private ObjectMapper mapper;

  @Override
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // números aleatórios no intervalo [a,b]
    // são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
    // os números são gerados após um atraso de delay milissegundos,
    // onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]

    // algumas verificações
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // erros?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // gerador de números aleatórios
    Random random = new Random();
    // em espera?
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        String message = null;
        try {
          message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
        } catch (JsonProcessingException e1) {
          throw new AleasException(e1,512);
        }
        throw new AleasException(message, 1024);
      }
    }
    // geração do resultado
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // retorno do resultado
    return new AleasMetier(delay,nombres);
  }

}

Não comentamos a classe: é análoga à encontrada no ambiente Swing no parágrafo 8.4. Salientamos apenas os seguintes pontos:

  • linha 10: a anotação Spring [@Service], que fará com que o Spring instancie a classe numa única instância (singleton) e disponibilize a sua referência para outros componentes do Spring. Outras anotações Spring poderiam ter sido utilizadas aqui com o mesmo efeito;
  • linhas 13-14: é injetado um mapeador jSON. O Spring é um contentor de objetos. Este contentor é instanciado no arranque da aplicação web e os objetos definidos por um ficheiro de configuração são então instanciados, por predefinição, numa única instância (singleton). Um singleton do Spring pode conter referências a outros objetos do Spring. É o que acontece aqui: o singleton [metier] (linhas 10-11) terá uma referência ao singleton [mapper] (linhas 13-14). A isto chama-se injeção de dependências. Existem duas formas de injetar um singleton noutro singleton:
    • pelo seu tipo: isto é possível se o singleton a injetar for o único objeto Spring com esse tipo. É o que acontece aqui na injeção das linhas 13-14 (tipo ObjectMapper);
    • pelo seu nome, se vários objetos Spring tiverem o mesmo tipo. Nesse caso, é necessário adicionar a anotação @Qualifier(«nomDuSingleton») para especificar o nome do singleton;

A classe [Metier] lança exceções do tipo [AleaException]:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // código de erro
  private int code;

  // construtores
  public AleaException() {
  }

  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }

  // getters e setters

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • linha 3: [AleasException] estende a classe [RuntimeException]. Trata-se, portanto, de uma exceção não controlada (não é obrigatório tratá-la com um try/catch);
  • linha 6: adiciona-se um código de erro à classe [RuntimeException];

9.2.4. O serviço web / JSON

  

O serviço web / JSON é implementado pelo Spring MVC. O Spring MVC implementa o modelo de arquitetura denominado MVC (Modelo – Vista – Controlador) da seguinte forma:

O processamento de um pedido de um cliente decorre da seguinte forma:

  1. pedido — os URL solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa os URL recebidos. Esta «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas denominadas [Contrôleurs]. O C de MVC é, neste caso, a cadeia [Dispatcher Servlet, Contrôleur, Action]. Se nenhuma ação tiver sido configurada para processar o URL recebido, o servlet [Dispatcher Servlet] responderá que o URL solicitado não foi encontrado (erro 404 NOT FOUND);
  1. processamento
  • a ação selecionada pode utilizar os parâmetros parami que a servlet [Dispatcher Servlet] lhe transmitiu. Estes podem provir de várias fontes:
    • do caminho [/param1/param2/...] do URL,
    • dos parâmetros [p1=v1&p2=v2] do URL,
    • dos parâmetros enviados pelo navegador juntamente com o seu pedido;
  • no processamento do pedido do utilizador, a ação pode necessitar da camada [metier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
    • uma página de erro, caso a solicitação não tenha podido ser processada corretamente
    • uma página de confirmação, caso contrário
  • a ação solicita que uma determinada vista seja apresentada: [3]. Esta vista irá apresentar dados a que se chama o modelo da vista. É o M de MVC. A ação irá criar este modelo M [2c] e solicitar que uma vista V seja apresentada [3];
  1. resposta — a vista V selecionada utiliza o modelo M criado pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.

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

  • em [4a], o modelo, que é uma classe Java, é transformado numa cadeia JSON por uma biblioteca JSON;
  • em [4b], esta cadeia JSON é enviada para o navegador;

Voltemos à camada [web] da nossa aplicação:

Na nossa aplicação, existe apenas um controlador:

  

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


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // código de erro
  private int erreur;
  // mensagem de erro
  private String message;

  // construtores
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getters e setters

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • linha 5: a classe [AleasResponse] estende a classe [AleasMetier] e, por isso, herda todos os seus atributos (aleas, delay);
  • linha 8: um código de erro (0 se não houver erro);
  • linha 10: se for erreur!=0, uma mensagem de erro; se não houver erro, null;

O controlador [AleasController] é o seguinte:


package dvp.rxjava.server.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;

@Controller
public class AleasController {

    // camada de negócio
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // números aleatórios em [a,b]
    // são gerados n números, sendo n no intervalo [minCount, maxCount]
    // os números são gerados após um atraso de delay milissegundos,
    // onde [delay] é um número aleatório no intervalo [minDelay, maxDelay]
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
            @PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
            @PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {

        // prepara-se a resposta
        AleasResponse response = new AleasResponse();
        // utiliza-se a camada de negócio para gerar os números aleatórios
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // caso de erro (código e mensagem)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // envia-se a resposta jSON
        return mapper.writeValueAsString(response);
    }
}
  • linha 16: a anotação [@Controller] transforma a classe [AleasController] num singleton Spring. Além disso, indica que a classe contém métodos que irão processar pedidos para determinados URL da aplicação web. Neste caso, existe apenas uma na linha 29;
  • linhas 20-21: a anotação [@Autowired] solicita ao Spring que injete no campo um componente do tipo [IMetier]. Será a classe [Metier] anterior. É porque colocámos nesta a anotação [@Service] que ela é tratada como um componente Spring;
  • linhas 22-23: a anotação [@Autowired] solicita ao Spring que injete no campo um componente do tipo [ObjectMapper]. Iremos defini-lo em breve;
  • linha 31: o método [getAleas] gera os números aleatórios. O seu nome não tem importância. Quando é executado, os parâmetros das linhas 31-33 foram inicializados pelo Spring MVC. Veremos como. Além disso, se este método for executado, é porque o servidor web recebeu um pedido HTTP GET para o URL da linha 29 (atributo method);
  • linha 30: a anotação [@ResponseBody] indica que o resultado do método deve ser enviado tal como está para o cliente. Aqui, vamos enviar-lhe uma cadeia de caracteres que será a cadeia jSON de um tipo [AleasResponse];
  • linha 29: o URL processado tem o formato /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, em que {x} representa uma variável. Estas diferentes variáveis são atribuídas aos parâmetros do método nas linhas 32-33. Isto é feito através da anotação @PathVariable("x"). Note-se que os valores {x} são componentes de um URL e são, portanto, do tipo String. A conversão de String para o tipo dos parâmetros do método pode falhar. O Spring MVC lança então uma exceção. Resumindo: se, num navegador, eu solicitar o URL /100/200/10/20/300/400, o método getAleas da linha 31 será executado com os parâmetros a=100 (linha 31), b=200 (linha 31), minCount=10 (linha 31), maxCount=20 (linha 32), minDelay=300 (linha 32), maxDelay=400 (linha 33);
  • linha 39: solicita-se à camada [métier] uma lista de números aleatórios. Recorde-se que o método [metier].getAleas pode lançar uma exceção;
  • linhas 42-43: caso de erro;
  • linha 46: a resposta do tipo [AleasResponse] é devolvida sob a forma de uma cadeia jSON;

9.2.5. Configuração do projeto Spring

  

Existem várias formas de configurar o Spring:

  • com ficheiros XML;
  • com código Java;
  • com uma combinação de ambos;

Optamos por configurar a nossa aplicação web com código Java. É a classe [Config] acima referida que assegura esta configuração:


package dvp.rxjava.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
  // -------------------------------- configuração da camada [web]
  @Autowired
  private ApplicationContext context;

  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // mapeador jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • linha 15: indicamos ao Spring em que pacotes irá encontrar os objetos a instanciar. Encontrará dois:
    • a classe [Metier] anotada por [@Service];
    • a classe [AleasController] anotada por [@Controller];
  • linha 16: a anotação [@EnableWebMvc] induz configurações automáticas para o framework Spring MVC;
  • linhas 19-20: injeção do contexto Spring (recipiente dos objetos Spring). Esta injeção é necessária porque o objeto das linhas 22-26 necessita dela;
  • o ficheiro de configuração do Spring pode definir novos objetos Spring utilizando métodos anotados com [@Bean]. O resultado do método torna-se, então, um objeto Spring;
  • linhas 22-26: definição do servlet do framework Spring MVC, que encaminha as solicitações HTTP para o controlador e o método corretos. [DispatcherServlet] é uma classe do Spring;
  • linhas 28-31: indica-se que este servlet processa todas as URL;
  • linhas 33-36: é a presença deste bean que irá ativar o servidor Tomcat presente nos arquivos do projeto. Este aguardará as solicitações na porta 8080;
  • linhas 39-42: um mapeador jSON. Foi este que foi injetado nos objetos Spring [Metier] e [AleasController];

9.2.6. Execução do servidor web

  

O projeto é executado a partir da seguinte classe executável [Application]:


package android.exemples.server.boot;

import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;

public class Application {
  public static void main(String[] args) {
    // execução da aplicação
    SpringApplication.run(Config.class, args);
  }

}
  • linha 6: a classe [Application] é uma classe executável (linhas 7-10);
  • linha 9: o método estático [SpringApplication.run] é um método de [spring Boot] (linha 4) que irá iniciar a aplicação. O seu primeiro parâmetro é a classe Java que configura o projeto. Neste caso, a classe [Config] que acabámos de descrever. O segundo parâmetro é o tabuleiro de argumentos passado ao método [main] (linha 7). Neste caso, não haverá argumentos;

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

9.3. O cliente Android

Nota: o projeto Android que se segue é bastante complexo. Requer bons conhecimentos de Android, que podem ser encontrados, por exemplo, em [Introduction à la programmation de tablettes Android avec Android Studio ].

Atividade

Visualizações

Camada

[DAO]

Utilizador

Servidor

O cliente terá dois componentes:

  1. uma camada [Présentation] (visualizações + atividade);
  2. uma camada [DAO] que se dirige ao serviço [web / JSON] que analisámos anteriormente.

9.3.1. RxAndroid

Para comunicar de forma assíncrona com o servidor de números aleatórios, o cliente Android utilizará a biblioteca RxAndroid. Esta biblioteca estende a RxJava ao ambiente Android. Tal como foi feito para a aplicação Swing, utilizaremos apenas uma única extensão introduzida pela RxAndroid, a do agendador [AndroidSchedulers.mainThread()]. Uma interface gráfica Android obedece às mesmas regras que uma interface Swing:

  • os eventos são processados num único thread denominado «event loop» ou «thread da UI»;
  • quando um evento desencadeia ações assíncronas, os resultados dessas ações devem ser recuperados na thread da UI, caso devam ser utilizados para atualizar a UI;

O cliente Android:

  • enviará várias solicitações assíncronas para o servidor de números aleatórios. Estas solicitações serão executadas do lado do cliente com os threads do agendador [Schedulers.io()];
  • estas solicitações assíncronas irão devolver observáveis que serão fundidos num único (merge);
  • este observável será observado do lado do cliente no agendador [AndroidSchedulers.mainThread()], fornecido pelo RxAndroid;

9.3.2. O projeto IntelliJ IDEA

O projeto Android chama-se [client]:

Será executado através do [2].

Nota: a execução depende em grande medida da configuração do IntelliJ IDEA utilizado. É provável que a execução do [2] acima referida não funcione à primeira tentativa noutro computador que não o meu. Configurar corretamente o IntelliJ IDEA para executar este projeto pode ser uma tarefa complicada para principiantes. Aqui estão alguns pontos a ter em conta:

  • no [3], aceder à estrutura do projeto;
  • no [4-5], o JDK e os SDK Android presentes no meu computador. Note-se que o JDK 1.8 não é indispensável. O Android não suporta certas funcionalidades do Java 8, incluindo as lambdas. Por isso, para instanciar interfaces funcionais, utilizaremos classes anónimas. Um JDK 1.6 é, portanto, suficiente. No entanto, o projeto, tal como é distribuído, foi configurado com um JDK 1.8;

O ficheiro [build.gradle] [6] que configura o projeto Android é o seguinte:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // substituir pela versão atual do plugin do Android
    classpath 'com.android.tools.build:gradle:1.5.0'
  }
}
apply plugin: 'com.android.application'
dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
  jcenter()
}
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "android.aleas"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_6
    targetCompatibility JavaVersion.VERSION_1_6
  }
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

De acordo com os ficheiros SDK Android existentes, as versões nas linhas 8, 24-25 e 29 poderão ter de ser alteradas.

Para instalar novos SDK Android, utilize o SDK Manager da seguinte forma [1]:

O projeto foi configurado para:

  • o SDK API 23 [2];
  • o SDK Build-tools 23.0.3 [3];
  • o SDK Tool 25.1.3 [4]

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


## Este ficheiro é gerado automaticamente pelo Android Studio.
# Não altere este ficheiro -- YOUR CHANGES WILL BE ERASED!
#
# Este ficheiro deve *NOT* ser submetido aos sistemas de controlo de versões,
# uma vez que contém informações específicas da sua configuração local.
#
# Localização do ficheiro SDK. Este ficheiro é utilizado apenas pelo Gradle.
# Para personalização ao utilizar um sistema de controlo de versões, leia a
# nota do cabeçalho.
#Qui, 07 de abril, 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Execução do projeto no IntelliJ IDEA

Quando tiver sido criado um ambiente adequado para o projeto, este pode ser executado da seguinte forma:

  • em [1], inicia-se o emulador Android Genymotion;
  • em [2], executa-se a configuração de execução [app];
  • em [3], para criar uma configuração de execução;
 
  • em [1, 3], a configuração foi denominada [app];
  • em [2], corresponde à execução do módulo denominado [app];
  • em [4], solicita-se que, durante a execução, o IDE nos sugira um dispositivo de execução. Neste caso, será sempre o emulador Genymotion;
  • no [5], indica-se que se mantenha este dispositivo para todas as execuções da configuração;

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

Image

Para saber o que inserir em [1], abra uma janela de comandos DOS e digite o seguinte comando [ipconfig]:


C:\Program Files\Console2>ipconfig

Configuration IP de Windows


Carte Ethernet Ethernet :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr

Carte réseau sans fil Connexion au réseau local* 3 :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Carte Ethernet VirtualBox Host-Only Network :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte Ethernet Ethernet 2 :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1


Digite no [1] um dos endereços IP do seu computador (linhas 20, 28, 32). Se tiver um firewall do Windows, provavelmente terá de o desativar para que o emulador Android consiga aceder ao servidor de números aleatórios.

A execução das requisições assíncronas com as informações acima fornece os seguintes resultados:

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 do lado do cliente;
  • requestAt: hora da solicitação;
  • responseAt: hora de receção da resposta;
  • delay: o tempo de espera que o servidor observou antes de enviar a sua resposta;
  • erro: um código de erro — 0 se não houver erro;
  • mensagem: uma mensagem de erro — null se não houver erro;
  • observedAt: hora de observação da resposta;
  • observedOn: thread de observação da resposta. Aqui será sempre [main], que designa o thread da interface do utilizador;

Como as solicitações são assíncronas e os tempos de espera impostos ao servidor são aleatórios, as respostas chegam em ordem dispersa.

9.3.4. As dependências Gradle do projeto

O projeto necessita de dependências que registamos no ficheiro [app / build.gradle]:

  

dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
  • as dependências das linhas 2-3 são dependências padrão de um projeto Android com o SDK 23;
  • A dependência da linha 5 inclui o objeto Spring [RestTemplate], que gere a comunicação da camada [DAO] com o servidor;
  • a dependência da linha 6 inclui a biblioteca JSON e [Jackson], utilizadas pela aplicação;
  • a dependência da linha 7 inclui a biblioteca RxAndroid (e, com ela, a biblioteca RxJava) que a camada Ui utiliza para comunicar com a camada [DAO];

9.3.5. O manifesto da aplicação Android

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="android.aleas">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name="android.aleas.activity.MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • linha 5: os acessos à Internet devem ser autorizados;

9.3.6. A camada [DAO]

 

9.3.6.1. A interface [IDao] da camada [DAO]

A interface da camada [DAO] será a seguinte:


package android.aleas.dao;

import android.aleas.fragments.Request;
import rx.Observable;

public interface IDao {

  // números aleatórios no intervalo [a,b]
  // são gerados n números, sendo n próprio um número aleatório no intervalo [minCount, maxCount]
  // os números são gerados após um atraso de delay milissegundos,
  // onde [delay] é, por sua vez, um número aleatório no intervalo [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL do serviço web
  public void setUrlServiceWebJson(String url);

  // tempo de espera máximo (ms) da resposta do servidor a um pedido de ligação
  // tempo de espera (ms) máximo da resposta do servidor a uma solicitação
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • linha 12: o método da camada [DAO] que fornece os números aleatórios de forma assíncrona;
  • linha 15: para indicar à implementação [DAO] o URL do serviço de geração de números aleatórios;
  • linha 19: para definir, na implementação [DAO], os tempos máximos de espera, de modo a evitar um tempo de espera excessivamente longo quando o servidor não responde;

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


package android.aleas.fragments;

public class Request {

  // n.º da solicitação
  int id;
  // entradas do utilizador
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // construtores
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // getters e setters
...
}

Reconhecem-se aqui a maioria dos parâmetros do URL do servidor a consultar.

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


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // código de erro
  private int erreur;
  // mensagem de erro
  private String message;
  // tempo de espera do servidor
  private int delay;
  // números aleatórios gerados pelo servidor
  private List<Integer> aleas;
  // estado do cliente
  private ClientState clientState;

  // construtores

  public AleasDaoResponse() {
  }

  public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
    this.erreur = erreur;
    this.message = message;
    this.delay = delay;
    this.aleas = aleas;
    this.clientState = clientState;
  }

  // getters e setters
...
}

O tipo [ClientState] é o seguinte:


package android.aleas.dao;

import org.codehaus.jackson.map.annotate.JsonFilter;

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

public class ClientState {

  // nome do thread de execução
  private String on;
  // hora do pedido
  private String requestAt;
  // hora da resposta
  private String responseAt;
  // ID do cliente
  private int idClient;

  // construtor
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }

  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }

  // métodos privados

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // getters e setters
...
}
  • linha 11: thread de execução da camada [DAO];
  • linha 13: hora do pedido;
  • linha 15: hora da resposta;
  • linha 17: n.º da solicitação;

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

9.3.6.2. Implementação da camada [DAO]

  

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


package android.aleas.dao;

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

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

public class Dao implements IDao {

  // cliente REST
  private RestTemplate restTemplate;
  // URL serviço
  private String urlServiceWebJson;

  // mapeador jSON
  private ObjectMapper mapper;

  // construtores
  public Dao() {
    // mapeador jSON
    mapper = new ObjectMapper();
  }

  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // define-se o URL do serviço REST
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • linha 22: o objeto [RestTemplate], que assegurará a comunicação com o servidor de números aleatórios;
  • linha 24: o URL do serviço de geração — é definido pelo método [setUrlServiceWebJson] da linha 41;
  • linha 27: o mapeador jSON, que servirá para deserializar a cadeia jSON enviada pelo servidor de números aleatórios;
  • linhas 30-33: o construtor da classe;
  • linha 32: é criado o mapeador jSON da linha 27;

O método [setClientTimeouts] é o seguinte:


  // cliente REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // define-se o tempo limite para as solicitações do cliente REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • a comunicação do cliente com o servidor web / JSON é assegurada pelo objeto [RestTemplate] da linha 2. Por enquanto, ainda não o inicializámos. É o método [setClientTimeouts] que o faz;
  • linha 8: a classe [HttpComponentsClientHttpRequestFactory] é fornecida pela dependência [spring-android-rest-template]. Esta vai permitir-nos definir os tempos máximos de espera pela resposta do servidor (linhas 9-10);
  • linha 11: criamos o objeto do tipo [RestTemplate], que servirá de suporte à comunicação com o serviço web. Passamos-lhe como parâmetro o objeto [factory] que acabou de ser criado;
  • linha 12: o diálogo cliente/servidor pode assumir várias formas. As trocas de dados são feitas através de linhas de texto e temos de indicar ao objeto do tipo [RestTemplate] o que deve fazer com essa linha de texto. Para tal, fornecemos-lhe conversores, ou seja, classes capazes de processar as linhas de texto. A escolha do conversor é geralmente feita através dos cabeçalhos HTTP que acompanham a linha de texto. De acordo com esses cabeçalhos, o objeto [RestTemplate] irá escolher, entre os seus conversores, aquele que for mais adequado à situação. Neste caso, teremos apenas um único conversor, um conversor String --> String, o que faz com que o tipo String recebido do servidor não sofra qualquer transformação.

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


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // execução do serviço
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL do serviço: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
          String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
            urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
            request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
          // informações do cliente
          ClientState clientState = new ClientState(request.getId());
          // pedido HTTP síncrono
          String response = executeRestService("get", urlService, null);
          // deserialização da resposta jSON do servidor
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // erro?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // a exceção é encaminhada
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // regista-se a hora de receção
            clientState.setResponseAt();
            // o resultado é encaminhado para o assinante
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // a exceção é encaminhada para o assinante
          subscriber.onError(ex);
        } finally {
          // é sinalizado o fim do observável
          // durante a execução, verifica-se que este método não tem qualquer efeito se o método [onError] tiver sido chamado     anteriormente — conforme a teoria —, pelo que esta instrução poderia ser colocada apenas no bloco «try»
          subscriber.onCompleted();
        }
      }
    });
  }
  • linha 2: é importante lembrar que devemos produzir um tipo [Observable<AleasResponse>];
  • linha 3: uma linha de registo na consola Android;
  • linha 5: o objeto [RestTemplate] garante uma comunicação síncrona com o servidor. Isto significa que o thread de execução que efetua a solicitação fica bloqueado até receber a resposta. No exemplo do Swing, vimos como transformar uma ação síncrona numa ação assíncrona através do método [Observable.create]. É este mesmo caminho que seguimos aqui;
  • linha 7: o método [call] da interface [Observable.OnSubscribe<AleasDaoResponse>] da linha 5. É este método que é chamado quando um observador se subscreve ao observável;
  • linhas 10-12: construção do URL do serviço de números aleatórios;
  • linha 14: inicialização do objeto [ClientState]. Trata-se aqui de registar a hora do pedido;
  • linha 16: pedido síncrono HTTP. Obtém-se uma resposta jSON. O método [executeRestService] espera três parâmetros:
      1. o método HTTP a utilizar para consultar o serviço;
      2. o URL do serviço;
      3. o objeto a enviar do tipo Object, null se o método HTTP não for POST;
  • 18-21: deserialização da cadeia jSON recebida num tipo [AleasServerResponse]. Este tipo é o seguinte:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // código de erro
  private int erreur;
  // mensagem de erro
  private String message;
  // tempo de espera do servidor
  private int delay;
  // números aleatórios
  private List<Integer> aleas;

  // getters e setters
...
}
  • linha 23: recupera-se o código de erro enviado pelo servidor;
  • linhas 24-26: em caso de erro, é lançada uma exceção para o assinante;
  • linha 29: atualiza-se o [clientState], que fará parte da resposta enviada ao assinante;
  • linhas 31-33: envio da resposta ao assinante. A resposta tem o formato [AleasDaoResponse];
  • linhas 35-37: tratam todos os casos de erro de forma indiferenciada. O erro mais provável é um erro de rede;
  • linha 41: notificação do fim da transmissão;

9.3.7. As vistas da aplicação

  

A aplicação apresenta as duas vistas seguintes:

A vista da consulta

Image

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

A classe [Session] contém os dados que os diferentes fragmentos da aplicação partilham. O seu código é o seguinte:

  

package android.aleas.activity;

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

public class Session {

  // atividade da aplicação
  private MainActivity activity;
  // número de pedidos
  private int nbRequests;
  // características das solicitações
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL serviço web / jSON
  private String urlWebJson;
  // operação iniciada
  private boolean onAir;
  // idem, mas um pouco mais tarde
  private boolean operationStarted;
  // o nome do exemplo escolhido pelo utilizador na lista de exemplos
  private String exampleName;
  // o seu n.º na lista de fragmentos
  private int examplePosition;
  // o adaptador do spinner dos exemplos na vista da consulta
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // métodos
  public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
    this.urlWebJson = urlWebJson;
    this.exampleName = exampleName;
    this.examplePosition = examplePosition;
  }

  public Request getRequest() {
    return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
  }

  // getters e setters
...
}

O método da linha 46 permite criar o objeto [Request], que encapsula todas as informações fornecidas pelo utilizador na vista de consulta:

  

package android.aleas.fragments;

public class Request {

  // n.º da consulta
  int id;
  // entradas do utilizador
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // construtores
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // getters e setters
....
}

9.3.7.2. O fragmento [RequestFragment] da consulta

O fragmento da consulta tem os seguintes componentes:

Image

A aplicação tem uma única vista, que é uma vista com duas separadores:

  • [1]: o separador da solicitação;
  • [2]: o separador da resposta;

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

n.º
Tipo
Nome
Função
3
EditText
edtNbRequests
número de pedidos a enviar ao serviço de geração de números aleatórios
4
EditText
edtA, edtB
os limites [a,b] do intervalo de geração de números;
5
EditText
edtMinCount, edtMaxCount
o serviço gera count números, em que count é um número aleatório no intervalo [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
o serviço aguarda delay milissegundos antes de gerar os números, em que delay é um número aleatório no intervalo [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
URL do serviço de geração de números aleatórios;
8
Spinner
spinnerExemples
a lista suspensa de exemplos. Cada exemplo ilustra um método específico da classe [Observable];
8
Botão
btnExecuter
o botão que inicia as chamadas ao serviço de geração de números;

Os erros de introdução de dados são assinalados:

Image

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

9.3.7.3. O fragmento [ResponseFragment] da resposta

O fragmento da resposta tem os seguintes componentes:

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 os pedidos ao servidor

9.3.7.4. A atividade Android [MainActivity]

  

A classe [MainActivity] apresenta a seguinte vista []:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context="android.arduinos.ui.activity.MainActivity">

  <!-- barra de aplicações -->
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <!-- barra de ferramentas -->
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">

      <!-- imagem de espera -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
    </android.support.v7.widget.Toolbar>

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

  <!-- recipiente de vistas -->
  <android.aleas.activity.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:layout_marginBottom="100dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

Os componentes desta vista são os seguintes:

linhas
Tipo
Nome
Função
20-34
Barra de ferramentas
barra de ferramentas
barra de ferramentas da aplicação
29-34
ProgressBar
loadingPanel
imagem de espera exibida enquanto o pedido do utilizador está a ser processado
37-40
TabLayout
separadores
a barra de separadores da aplicação
44-51
MyPager
container
o contentor no qual são apresentados os diferentes fragmentos da aplicação

A classe [MyPager] é a seguinte:


package android.aleas.activity;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // controla o deslize
  private boolean isSwipeEnabled;

  // construtores
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // redefinição de métodos
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // deslizar permitido?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // deslizar autorizado?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • a classe [MyPager] estende a classe padrão do Android [ViewPager]. Utiliza-se a classe [MyPager] em vez da classe [ViewPager] apenas porque se pretende inibir o deslize: por predefinição, com a classe [ViewPager], é possível passar de um separador para outro através de um deslize (deslizando para a esquerda ou para a direita). Neste caso, não queremos esse comportamento;
  • linha 11: a variável booleana que irá controlar o deslize (linhas 26 e 36);
  • linhas 44-46: o método que permite inicializar o campo da linha 11;

A estrutura da atividade Android [MainActivity] é a seguinte:


package android.aleas.activity;

import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;

public class MainActivity extends AppCompatActivity implements IDao {

  // camada [DAO]
  private IDao dao;
  // a sessão
  private Session session;

  // construtor
  public MainActivity() {
    // pai
    super();
    // sessão
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getters

  public Session getSession() {
    return session;
  }

  // implementação IDao ----------------------------------------
  @Override
  public Observable<AleasDaoResponse> getAleas(Request request) {
    return dao.getAleas(request);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    dao.setClientTimeouts(connectTimeout, readTimeOut);
  }

}
  • linha 21: a classe [MainActivity] estende a classe padrão do Android [AppCompatActivity]. Trata-se, portanto, de uma atividade padrão do Android;
  • linha 21: a classe [MainActivity] implementa a interface [IDao];

Voltando à arquitetura da aplicação:

o facto de a atividade implementar a interface da camada [DAO] permite que as vistas não tenham conhecimento da camada [DAO]: os seus gestores de eventos irão recorrer à camada [activité] quando pretenderem comunicar com o servidor.

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

A classe [MainActivity] inicializa os componentes da vista que lhe está associada da seguinte forma:


  // barra de ferramentas
  private Toolbar toolbar;
  // gestor de fragmentos
  private MyPager mViewPager;
  // recipiente de separadores
  private TabLayout tabLayout;
  // imagem de espera
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // clássico
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // sessão
    session.setActivity(this);
    // configuração de tempos limite da camada [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

    // componentes
    mViewPager = (MyPager) findViewById(R.id.container);
    toolbar = (Toolbar) findViewById(R.id.toolbar);
    loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
    tabLayout = (TabLayout) findViewById(R.id.tabs);

    // barra de ferramentas
    setSupportActionBar(toolbar);

    // inicialmente, existe apenas um separador
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // gestor de eventos
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // foi selecionado um separador — altera-se o fragmento apresentado pelo contentor de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // separador de consulta
          showView(0);
        } else {
          // separador de respostas — depende do exemplo escolhido
          showView(session.getExamplePosition());
        }
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
    });

    // criação dos fragmentos das respostas
    createResponseFragments();

    // gestão da imagem de espera
    loadingPanel.setVisibility(View.INVISIBLE);
}

Este código é bastante comum numa atividade. Vamos explicar alguns pontos:

  • a linha 19 faz referência à seguinte classe [Constants]:

package android.aleas.activity;

abstract public class Constants {

  final static public int VUE_REQUEST = 0;
  final static public int VUE_RESPONSE = 1;
  final static public int CONNECT_TIMEOUT = 1000;
  final static public int READ_TIMEOUT = 6000;
  final static public int DELAY_MAX = 5000;
  final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
  • linhas 31-33: cria-se o primeiro separador com o título [Request]. A certa altura, teremos na memória:
    • o fragmento [Request];
    • n fragmentos do tipo [ExampleXXFragment];

A primeira aba exibirá sempre o fragmento [Request]. A segunda aba exibirá o fragmento [ExampleXXFragment] correspondente ao exemplo escolhido pelo utilizador. O fragmento exibido pela segunda aba muda, portanto, ao longo do tempo;

  • linhas 37-48: o código executado quando o utilizador clica numa das separadores;
  • linha 43: é exibido o fragmento n.º 0;
  • linha 46: é exibido o fragmento atualmente em uso (visualizado). O seu n.º é encontrado na sessão;
  • linha 62: criam-se os fragmentos de todos os exemplos presentes no spinner de exemplos na vista [RequestFragment] (1.ª aba);
  • linha 65: a imagem de espera está, por enquanto, oculta;

Para compreender o método [showView] (linhas 43, 46) e o método [createResponseFragments], temos primeiro de apresentar o gestor de fragmentos em memória (classe incluída no ficheiro Java de MainActivity):


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

    // fragmentos geridos
    private MyFragment[] fragments;

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

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

    // indica o número de fragmentos a gerir
    @Override
    public int getCount() {
      // n.º de fragmentos
      return fragments.length;
    }
  }
}
  • A classe [SectionsPagerAdapter] estende a classe Android [FragmentPagerAdapter]. Ela redefine dois métodos da sua classe pai:
    • o método [getItem], linha 15;
    • o método [getCount], linha 22;
  • A classe [SectionsPagerAdapter] contém todos os fragmentos da aplicação. Estes são armazenados na linha 5. Note-se que são do tipo [MyFragment], apresentado no parágrafo 9.3.7.1;
  • linha 8: para ser criada, a classe [SectionsPagerAdapter] recebe os fragmentos que deve gerir;
  • linhas 14-18: o método [getItem] devolve o fragmento na posição [position];
  • linhas 21-25: o método [getCount] devolve o número total de fragmentos;

O método [createResponseFragments] cria todos os fragmentos de que a aplicação necessita:


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

O ficheiro [exemples.xml] contém o seguinte código:


<!-- exemplos -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
  </string-array>
</resources>

Na linha 1, este ficheiro é o segundo parâmetro do método [createFromResource]. Em [R.array.exemples], [exemples] é o nome da tabela, na linha 3 acima, e não o nome do ficheiro.

  • linha 5: associa-se um layout (gestor de visualização) ao adaptador. Agora, o adaptador dispõe tanto dos dados como do respetivo modo de visualização;
  • linha 7: coloca-se o adaptador em sessão. É aí que o fragmento [RequestFragment], que precisa dele, o irá recuperar;

Continuemos com o código do método [createResponseFragments]:


private void createResponseFragments() {
    // exemplos de spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Especificar o layout a utilizar quando a lista de opções for apresentada
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // colocamos o adaptador na sessão para que a vista [Request] o recupere
    session.setSpinnerExemplesAdapter(adapter);
    // criação da tabela de fragmentos (1 consulta, n respostas)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // fragmento da consulta
    tFragments[0] = new RequestFragment();
    // fragmentos das respostas
    for (int i = 1; i < tFragments.length; i++) {
      // constrói-se o nome do fragmento a instanciar, correspondente ao exemplo escolhido pelo utilizador
      // este nome deve ser o nome completo com o respetivo pacote — neste caso, está diretamente associado ao número do exemplo no spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // instancia-se o fragmento associado ao exemplo
      MyFragment fragment;
      try {
        // instanciação da classe
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // o fragmento foi criado — é adicionado à matriz
      tFragments[i] = fragment;
    }
    // instanciação do gestor de fragmentos com estes novos fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Configurar o ViewPager com o adaptador de secções.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navegação entre páginas — esta instrução é importante
    // aqui diz-se que, de ambos os lados da vista exibida, é necessário manter vistas inicializadas [tFragments.length]
    // isto implica que, neste caso, todos os fragmentos utilizados pela aplicação estão na memória e inicializados
    // Se não se fizer isso, por predefinição, o [OffscreenPageLimit] é igual a 1
    // assim, se o fragmento visualizado for o n.º 3, apenas os fragmentos 2 e 4 serão inicializados
    // isto ocorre através da chamada do método [onCreateView] destes dois fragmentos — o que significa que, neste método, é necessário prever
    // regenerar o aspeto visual que o fragmento tinha da última vez que foi utilizado — além disso, não pode haver, neste método
    // haja código que não suporte ser executado duas vezes — isso cria uma confusão enorme e é complexo de gerir
    // aqui optou-se por evitar essas dificuldades — nos registos, verifica-se que, no arranque da aplicação, todos os fragmentos são criados
    // e o respetivo método [onCreateView] é executado — e nunca mais volta a ser executado depois disso —
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // inibimos a transição entre fragmentos
    mViewPager.setSwipeEnabled(false);
  }
  • linha 9: criação da matriz que irá conter todos os fragmentos da aplicação;
  • linha 11: o primeiro fragmento é o da consulta;
  • linhas 13-28: vamos criar tantos fragmentos quantos forem os exemplos. Todos estes fragmentos estendem o fragmento da resposta [ResponseFragment] e implementam apenas o que é específico do exemplo: a criação dos valores observados. Estes, de facto, diferem de um exemplo para outro;
  • linha 16: o fragmento de um exemplo tem um nome padrão: ExampleXXFragment, em que XX é a sua posição no spinner dos exemplos, acrescida de 1. XX é também o n.º do fragmento do exemplo no gestor de fragmentos;
  • linha 21: instanciação do fragmento do exemplo n.º i do spinner:
    • Class.forName(exampleName): carrega o fragmento na memória;
    • Class.forName(exampleName).getConstructors()[0]: obtém a referência ao primeiro construtor da classe. A classe ExampleXXFragment tem apenas um construtor. Por conseguinte, será obtida uma referência a esse construtor;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instancia um objeto do tipo ExampleXXFragment utilizando o construtor da etapa anterior. new Object[]{} representa os parâmetros passados a esse construtor. Como o construtor da classe ExampleXXFragment não espera parâmetros, passa-se um array de objetos vazio;
  • linha 27: este fragmento é adicionado à matriz de fragmentos;
  • linha 30: vimos que o construtor do gestor de fragmentos [SectionsPagerAdapter] esperava, nos seus parâmetros, a matriz de fragmentos que deveria gerir. É agora que lha passamos;
  • linha 22: o contentor de fragmentos [mViewPager] da vista associada à atividade [MainActivity] é aqui associado ao gestor de fragmentos: o contentor de fragmentos [mViewPager] exibe os fragmentos do gestor de fragmentos;
  • linha 43: vamos ler os comentários — a instrução equivale a dizer que todos os fragmentos devem permanecer no estado em que o código os coloca, independentemente do fragmento atualmente exibido. Assim, quando voltamos a ele, encontramo-lo no estado em que o deixámos;
  • linha 45: o contentor de fragmentos [mViewPager] é do tipo [MyPager], o que permite inibir o deslize;

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


  // visualização da vista n.º [position]
  private void showView(int position) {
    // atualiza-se o fragmento antes da sua visualização
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // exibe-se a vista solicitada — passa-se diretamente para a vista (segundo parâmetro definido como «false»)
    // sem este parâmetro, acede-se por predefinição à vista pretendida, exibindo rapidamente as vistas intermédias — comportamento indesejável
    mViewPager.setCurrentItem(position, false);
}
  • linha 3: pretende-se apresentar o fragmento na posição n.º;
  • linha 4: este fragmento é solicitado ao gestor de fragmentos e, em seguida, atualizado. Com efeito, desde a última vez que foi apresentado, a sessão pode ter mudado. O fragmento deve, então, inspecionar a sessão para verificar se deve atualizar-se;
  • linha 7: o fragmento é apresentado pelo [ViewPager]. Como este foi associado ao gestor de fragmentos, é o fragmento n.º [position] que será apresentado, aquele que acabámos de atualizar na linha 4;

Terminemos com os dois métodos de gestão da espera:


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

  public void cancelWaiting() {
    // gestão da imagem de espera
    loadingPanel.setVisibility(View.INVISIBLE);
    // fim da execução
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. O fragmento [RequestFragment]

A classe [RequestFragment] é a seguinte:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.net.URI;
import java.net.URISyntaxException;

public class RequestFragment extends MyFragment {

  // URL do serviço web
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // número de pedidos
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // intervalo de geração
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // atraso
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // número de valores gerados
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // botão
  private Button btnExecuter;
  // lista de respostas
  private ListView listReponses;
  private TextView infoReponses;
  // seletor de exemplos
  private Spinner spinnerExemples;

  // as entradas
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

  // construtor
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // recuperamos a atividade e a sessão
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // cria-se a vista do fragmento a partir da sua definição XML
    View view = inflater.inflate(R.layout.request, container, false);
    // componentes
    edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
    txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
    edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
    txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
    edtA = (EditText) view.findViewById(R.id.edt_a);
    edtB = (EditText) view.findViewById(R.id.edt_b);
    txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
    edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
    edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
    txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
    edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
    edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
    txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
    btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);

    // botão [Exécuter]
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });

    // Inicialmente, não há mensagens de erro
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner dos exemplos
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // resultado
    return view;
  }
...
}
  • linha 16: a classe [RequestFragment] estende a classe [MyFragment] (ver parágrafo 9.3.7.1);
  • linhas 18-42: os componentes visuais do fragmento (ver parágrafo 9.3.7.2);
  • linhas 45-52: os dados introduzidos pelo utilizador no formulário;
  • o construtor (linhas 55-58) e o método [onCreateView] são executados quando a atividade [MainActivity] cria todos os fragmentos da aplicação. Esta é a única vez;
  • linha 61: o código do método [onCreateView] é clássico. Note-se, na linha 102, que o adaptador do spinner dos exemplos é obtido na sessão. Note-se também, na linha 91, que o clique no botão [Exécuter] é gerido pelo método [doExecuter];
  • linhas 64-65: os campos [activity] e [session] pertencem à classe pai [MyFragment];

O método [doExecuter] é o seguinte:


  // as entradas
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

...

  private void doExecuter() {
    // entradas válidas?
    if (isPageValid()) {
      // guardamos as informações na sessão
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // memoriza-se o URL do serviço web
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // ação em curso
      session.setOnAir(true);
      // mas ainda não iniciada
      session.setOperationStarted(false);
      // exibe-se o fragmento da resposta
      activity.selectTab(Constants.VUE_RESPONSE);
      // inicia-se a espera
      beginWaiting();
    }
}
  • linha 15: não iremos comentar o método [ispageValid]. Este verifica a validade das entradas e retorna «true» apenas se todas forem válidas. Nesse caso, são utilizadas para inicializar os campos das linhas 2 a 9;
  • linha 17: as diferentes entradas são colocadas na sessão:
    • [spinnerExemples.getSelectedItem().toString()] é o nome do exemplo selecionado pelo utilizador e é memorizado em [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] é o n.º do fragmento associado ao exemplo e que foi guardado (o fragmento) pelo gestor de fragmentos. Este n.º é guardado em [session.examplePosition];
  • linha 19: o URL do serviço web / jSON é transmitido à atividade, que, por sua vez, o transmite à camada [DAO];
  • linhas 21-24: verifica-se que uma operação está prestes a iniciar;
  • linha 26: o separador da resposta vai ser apresentado. Para compreender o que vai acontecer, é necessário recordar o código [MainActivity.selectTab]:

  // seleção de um separador
  public void selectTab(int position) {
    // existem, no máximo, 2 separadores
    // inicialmente, existe apenas uma, a da consulta
    // se a aba solicitada for a n.º 1 e esta ainda não existir, então é necessário criá-la
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // mais 1 separador
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // seleciona-se a guia por programa, o que irá desencadear o evento [onTabSelected]
    // que irá associar a vista correta a essa aba
    tabLayout.getTabAt(position).select();
}
  • inicialmente, a atividade tinha criado apenas o separador da consulta (separador n.º 0);
  • linhas 6-11: cria-se o separador da resposta (separador n.º 1) caso ainda não tenha sido criado;
  • linha 14: seleciona-se a guia n.º position (0 ou 1). Isto coloca o evento [onTabSelected] na fila do evento loop da aplicação Android;

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


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // foi selecionada uma aba — altera-se o fragmento apresentado pelo contentor de fragmentos
        int position = tab.getPosition();
        if (position == 0) {
          // separador de consulta
          showView(0);
        } else {
          // separador «Resposta» — depende do exemplo escolhido
          showView(session.getExamplePosition());
        }
}

No caso do separador [Response], é a linha 9 que é executada. O fragmento n.º [session.getExamplePosition()] será apresentado. Por exemplo, no caso do exemplo [exemple-03], o n.º que foi registado em [session.examplePosition] é 3. A linha 10 apresenta, então, o fragmento n.º 3. A tabela de fragmentos criada inicialmente pela atividade é [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Por conseguinte, é efetivamente o fragmento [Exemple03Fragment] que será apresentado. Isto é feito através do seguinte código:


  // exibição da vista n.º [position]
  private void showView(int position) {
    // atualiza-se o fragmento antes da sua exibição
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // exibe-se a vista solicitada - vai-se diretamente para a vista (segundo parâmetro definido como «false»)
    // sem este parâmetro, acede-se por predefinição à vista pretendida, exibindo rapidamente as vistas intermédias — comportamento indesejável
    mViewPager.setCurrentItem(position, false);
}

Vê-se que o fragmento vai ser atualizado (linha 4) antes de ser apresentado (linha 7).

9.3.7.6. O fragmento [ResponseFragment]

A classe [ResponseFragment] apresenta as respostas do servidor. O seu código é o seguinte:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public abstract class ResponseFragment extends MyFragment {

  // lista de respostas
  private ListView listReponses;
  private TextView infoReponses;
  // botão
  private Button btnAnnuler;

  // mapeador jSON
  private ObjectMapper mapper;

  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // recupera-se a atividade e a sessão
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // cria-se a vista do fragmento a partir da sua definição XML
    View view = inflater.inflate(R.layout.response, container, false);
    // componentes
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // botão [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // resultado
    return view;
  }
...
  // método a executar (por código explícito) antes de cada visualização do fragmento
  public void onRefresh() {
...
  }
}
  • linha 21: a classe [ResponseFragment] estende a classe [MyFragment];
  • linhas 23-27: os componentes do fragmento;
  • linhas 32-36: o construtor é executado apenas uma vez, durante a criação inicial dos fragmentos dos exemplos pela atividade. Com efeito, todos os fragmentos dos exemplos estendem o fragmento [ResponseFragment]. Durante a sua instanciação, é chamado o construtor da sua classe pai [ResponseFragment];
  • linha 35: inicializa o mapeador jSON da linha 30, utilizado para exibir a cadeia jSON de uma pilha de exceções;
  • linhas 38-59: o método [onCreateView] é executado apenas uma vez, durante a criação inicial dos fragmentos dos exemplos pela atividade. Aqui encontra-se código clássico de uma aplicação Android;
  • linhas 52-56: o método executado ao clicar no botão [Annuler] é o método [doAnnuler];
  • linhas 62-64: o método [onRefresh] é executado sempre que o separador [Response] é apresentado;

Graças aos vários registos inseridos nos métodos importantes, é possível ver o que acontece no arranque da aplicação:

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 da aplicação;
  • linha 10: inicialização do fragmento [RequestFragment];
  • linhas 11-14: inicialização dos fragmentos dos 4 exemplos da aplicação;

Depois disso, nunca mais se voltam a ver chamadas a estes métodos.

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


  // método a executar (por código explícito) antes de cada visualização do fragmento
  public void onRefresh() {
    Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
    // execução em curso?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // execução da solicitação
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • linha 5: verifica-se se o fragmento [RequestFragment] efetuou um pedido (session.isOnAir) e se este já foi iniciado (isOperationStarted). Se o fragmento [RequestFragment] tiver efetuado uma solicitação e esta ainda não estiver em execução, a operação é iniciada (linhas 7-8);
  • uma vez iniciada a operação, como esta é assíncrona, o utilizador pode alternar entre os dois separadores. Se voltar ao separador [Response] e houver uma operação em curso, as linhas 7-8 não são executadas;

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


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // início da espera
    beginWaiting();
    // preparação da execução
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // estão a ser criados e executados os observáveis do exemplo escolhido
    createAndExecuteObservables();
}

// método implementado pelas classes filhas
protected abstract void createAndExecuteObservables();
  • linha 10: cria, executa e observa observáveis. Estes são diferentes para cada exemplo. É por isso que o método [createAndExecuteObservables] é abstrato (linha 14). Será implementado pelos fragmentos [ExampleXXFragment], que estendem a classe [ResponseFragment];
  • linha 6: a lista de subscrições é esvaziada;
  • linha 7: a lista que apresenta as respostas é esvaziada;
  • linha 8: conta o número de respostas recebidas;

As classes filhas [ExampleXXFragment] atribuem ao método [showAlea], a seguir, a tarefa de apresentar os elementos que observam:


  protected void showAlea(String data) {
    // mais uma informação
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // mais 1 resposta
    reponses.add(0, data);
    Log.d("rxjava", data);
    // atualização do UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • linha 1: verifica-se que o elemento observado chega sob a forma de uma cadeia de caracteres. Trata-se, na verdade, da cadeia jSON do elemento observado. Isto permite-nos dispor de um método único para apresentar o elemento observado, independentemente do seu tipo exato em Java;
  • linha 6: o elemento observado [data] é adicionado na primeira posição da lista de respostas. Assim, o utilizador vê, no início da lista, as respostas mais recentes;

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


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

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

Estes recorrem aos métodos com os mesmos nomes da atividade e limitam-se a mostrar/ocultar o botão [Annuler].

O clique no botão [Annuler] é gerido pelo código seguinte:


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

9.3.8. Exemplos de observáveis

9.3.8.1. Exemple-01

As classes [ExampleXXFragment] têm como funcionalidade criar, executar e observar observáveis. A exibição dos valores observados é feita pela classe pai [ResponseFragment].

A classe [Example01Fragment] é a seguinte:

  

package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example01Fragment extends ResponseFragment {

    // mapeadores jSON
    private ObjectMapper mapperAleasUiResponse;

    // construtor
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // filtros jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // solicitação de números aleatórios
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // configuração observável n.º i
            // solicitação a enviar ao servidor
            Request request = session.getRequest();
            request.setId(i);
            // observável executado na thread de cálculo
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observação na thread do evento loop;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // executam-se todos estes observáveis
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
...
        }, new Action0() {
...
    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // extraem-se as informações a apresentar
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • linha 36: o único observável que será gerado;
  • linhas 37-44: geração e configuração dos diferentes observáveis que são fundidos (linha 43) no observável da linha 36;
  • linha 43: o observável é executado num thread do agendador [Schedulers.io()]. A chamada HTTP ao servidor será executada neste thread;
  • linha 46: o observável final é observado na thread do evento loop;
  • linhas 48-57: execução dos observáveis e, consequentemente, das solicitações ao servidor de números aleatórios. O Android ainda não suporta o Java 8 e as suas lambdas. Por isso, utilizam-se aqui classes anónimas para instanciar as interfaces funcionais de RxJava;
  • linhas 49-52: ação executada quando o observador recebe um novo elemento do tipo [AleasDaoResponse] do observável (ver parágrafo 9.3.6.1);
  • linha 51: chamada do método [showAlea] da classe pai. Recorde-se que este método espera uma cadeia de caracteres. Esta é fornecida pelo método [getDataFrom] das linhas 59-68;
  • linha 63: devolve-se a cadeia jSON do tipo [AleasUiResponse] da seguinte forma:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // resposta [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // thread de observação
  private String observedOn;
  // hora de observação
  private String observedAt;

  // construtores
  public AleasUiResponse() {
    observedOn = Thread.currentThread().getName();
    observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
    this.aleasDaoResponse = aleasDaoResponse;
    this.observedOn = on;
    this.observedAt = at;
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// getters e setters
...
}
  • à resposta da camada [DAO] (linha 11), adicionam-se duas informações:
    • linha 13: o fio de observação;
    • linha 15: a hora da observação;

Voltemos ao código de subscrição:


    @Override
    public void createAndExecuteObservables() {
...
        // executam-se todos estes observáveis
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                // exibe-se a exceção
                showAlea(getMessagesFromThrowable(th));
                // após receber uma exceção, o observável não recebe nem onNext, nem onCompleted
                // obrigado a cancelar a subscrição manualmente
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fim da espera
                cancelWaiting();
            }
        }));
}
  • linhas 11-18: caso em que o observador receba uma exceção;
  • linha 14: volta-se a utilizar o método [showAlea] da classe pai para apresentar a exceção. O método [getMessagesFromThrowable] é um método da classe pai [ResponseFragment] que, a partir de uma exceção, gera uma cadeia de caracteres:

  // mensagens de uma exceção
  protected String getMessagesFromThrowable(Throwable ex) {
    // é criada uma lista com as mensagens de erro da pilha de exceções
    List<String> messages = new ArrayList<String>();
    Throwable th = ex;
    while (th != null) {
      messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
      th = th.getCause();
    }
    try {
      return mapper.writeValueAsString(messages);
    } catch (IOException e) {
      return e.getMessage();
    }
}
  • linha 11: devolve-se a cadeia jSON de uma lista de mensagens de erro (linha 4);

Voltemos ao código de subscrição do observável:

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

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

Image

Cada elemento da lista é a cadeia jSON de um valor observado. Os campos da cadeia jSON são os seguintes:

  • aleas: a lista de números aleatórios fornecida pelo servidor;
  • idClient: o número da solicitação (pode-se ver que as respostas chegaram em ordem aleatória);
  • on: o thread de execução do observável que emitiu este valor;
  • requestAt: hora da solicitação do cliente;
  • responseAt: hora da resposta do servidor;
  • delay: tempo de espera observado pelo servidor;
  • erro: código de erro devolvido pelo servidor (0 = sem erro);
  • mensagem: mensagem de erro devolvida pelo servidor (null = sem erro);
  • observedAt: hora em que o valor foi observado;
  • observedOn: thread de observação do valor observado;

9.3.8.2. Exemple-02

A classe [Example02Fragment] é a seguinte:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example02Fragment extends ResponseFragment {

    // mapeadores jSON
    private ObjectMapper mapperAleasUiResponse;

    // construtor
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // filtro jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // solicitação de números aleatórios
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // preparação do pedido
            Request request = session.getRequest();
            request.setId(i);
            // só se mantêm os observáveis com um número de cliente par
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // execução no thread de E/S
                            .subscribeOn(Schedulers.io()));
        }
        // observação no thread do evento loop
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // executam-se estes observáveis
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // fim da espera
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // extraímos a informação a apresentar
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Este exemplo é análogo ao anterior (linha 38). No entanto, das observáveis obtidas no exemplo anterior, apenas se mantêm as observáveis com um número de cliente par (linhas 42-46), graças ao método [filter] (linha 41).

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

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 solicitação
      Request request = session.getRequest();
      request.setId(i);
      // configuração observável
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).map(new Func1<AleasDaoResponse, List<Integer>>() {
        @Override
        public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getAleas();
        }
      })
        // execução no thread de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observação no thread do evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executam-se estes observáveis
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fim da espera
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // extraímos a informação a apresentar
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Este exemplo é semelhante ao Exemplo-02:

  • linha 40: definem-se os mesmos observáveis que no Exemplo-02;
  • linha 45: cada um dos valores emitidos pelos observáveis anteriores é transformado, através do método [map], num tipo List<Integer>, que corresponde à lista de números aleatórios gerados pelo servidor;
  • linha 58: agora, o valor observado é do tipo List<Integer>;

O resultado obtido para 10 pedidos é o seguinte:

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 num thread de E/S
        .subscribeOn(Schedulers.io()));
    }
    // observação num thread do evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executamos estes observáveis
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fim da espera
            cancelWaiting();
          }
        }
      ));

  }
}

Este exemplo é semelhante ao Exemplo-03, com a diferença de que, na linha 42, em vez de utilizar o método [map], utiliza-se o método [flatMap].

  • linha 55: note-se que, a partir de agora, o tipo do valor observado é Integer;

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

Image

Desta vez, temos mais valores observados do que consultas.

9.3.8.5. Exemple-05

Apresentamos agora o procedimento a seguir para adicionar um novo exemplo de observáveis à aplicação.

Suponhamos que se pretenda reproduzir o exemplo [Exemple22h] do parágrafo 7.6.4:


package dvp.rxjava.observables.exemples;

import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;

public class Exemple22h {
    public static void main(String[] args) throws InterruptedException {
        // processo
        Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
        Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
        // subscrições
        ProcessUtils.subscribe(1, process);
    }
}
  • os valores do observável [Observable.range(1, 10)] são primeiro agrupados em valores pares e ímpares pelo método [groupBy] (linha 11) e, em seguida, reunidos num único observável pelo método [concatMap] (linha 12);

passo 1

Cria-se um novo exemplo no ficheiro [exemples.xml]:

  

<!-- exemplos -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
    <item>Exemple-05</item>
  </string-array>
</resources>

Acima, foi adicionada a linha 8. O nome dado ao exemplo pode ser qualquer um.

Passo 2

Duplicamos a classe [Example04Fragment] para [Example05Fragment]. Neste caso, o nome é imposto.

Passo 3

Altera-se o código de [Example05Fragment] da seguinte forma:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;

public class Example05Fragment extends ResponseFragment {

  // mapeadores jSON
  private ObjectMapper mapper;

  // construtor
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // filtro jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instâncias das interfaces funcionais
    // filtro
    Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
      @Override
      public Boolean call(AleasDaoResponse aleasDaoResponse) {
        return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
      }
    };
    // flatMap
    Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
        return Observable.from(aleasDaoResponse.getAleas());
      }
    };
    // groupBy
    Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
      @Override
      public Boolean call(Integer integer) {
        return integer % 2 == 0;
      }
    };
    // concatMap
    Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
        return integerIntegerGroupedObservable.asObservable();
      }
    };
    // solicitam-se os números aleatórios
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // preparação do pedido
      Request request = session.getRequest();
      request.setId(i);
      // configuração observável
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // execução num thread de E/S
        .subscribeOn(Schedulers.io());
    }
    // observação num thread do evento loop
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // executamos estes observáveis
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // fim da espera
            cancelWaiting();
          }
        }
      ));

  }
}
  • linha 67: representa o observável do exemplo 04: um fluxo de inteiros;
  • linha 68: agrupamos este fluxo de inteiros de acordo com um critério booleano que iremos definir. Obteremos assim um observável do tipo Observable<GroupedObservable<Boolean, Integer>> que emite, portanto, elementos do tipo GroupedObservable<Boolean, Integer>;
  • linha 68: o método [concatMap] irá produzir elementos do tipo Integer a partir dos elementos do tipo GroupedObservable<Boolean, Integer>;
  • linhas 32-59: para tornar mais legível a criação do observável nas linhas 67-69, isolámos as instâncias das interfaces funcionais de que os diferentes operadores [filter, flatMap, groupBy, concatMap] necessitam;
  • linhas 47-52: o método [groupBy] espera um parâmetro do tipo Func1<T,K>, em que T é o tipo dos elementos agrupados e K é o tipo do critério de agrupamento. A partir do elemento T, a instância Func1<T,K> é responsável por gerar a chave de agrupamento K do elemento;
  • linhas 48-51: os elementos do tipo Integer serão agrupados por paridade. A instância Func1<Integer,Boolean> produz a chave true ou false, consoante o elemento deva ser colocado num grupo ou noutro. No final, obtêm-se dois grupos: o grupo dos elementos pares com a chave true e o grupo dos elementos ímpares com a chave false;
  • linhas 53-59: o método [concatMap] espera um parâmetro do tipo Func1<T,Observable<R>> e produz um observável de elementos do tipo R. O tipo T será, neste caso, o tipo emitido pelo operador [groupBy], neste caso um tipo GroupedObservable<Boolean, Integer>;
  • linha 57: a partir do elemento do tipo [GroupedObservable<Boolean, Integer>], é produzido um tipo Observable<Integer>. Como o operador [groupBy] produziu dois grupos, o operador [concatMap] irá produzir dois observáveis do tipo [Observable<Integer>]. Tal como o [flatMap], irá fundi-los num único observável. Mas, ao contrário do [flatMap], não mistura os elementos dos observáveis fundidos. Por isso, devemos observar dois grupos isolados: os números aleatórios pares e os restantes ímpares.

Etapa 4

Executamos a aplicação:

Image

e obtêm-se os seguintes resultados:

Image

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

9.3.8.6. Para continuar

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

9.3.9. Conclusão

Criámos no ambiente Android a seguinte arquitetura:

O cliente Android:

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

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

Na realidade, as aplicações Android comunicam com servidores diferentes e, provavelmente, não se irão fundir as respetivas respostas. As solicitações HTTP a esses servidores serão geridas de forma independente umas das outras e os seus resultados observados por métodos separados.