Skip to content

3. Caso de estudo - Gestão de marcações

3.1. O projeto

No documento [Tutoriel AngularJS / Spring 4], foi desenvolvida uma aplicação cliente/servidor para gerir consultas médicas. Doravante, referir-nos-emos a este documento como [rdvmedecins-angular]. A aplicação tinha dois tipos de cliente:

  • um cliente HTML / CSS / JS;
  • um cliente Android;

O cliente Android era obtido automaticamente a partir da versão HTML do cliente, utilizando a ferramenta [Cordova]. O objetivo deste projeto será recriar manualmente este cliente Android, utilizando os conhecimentos adquiridos nos capítulos anteriores.

É de salientar uma diferença importante entre as duas soluções:

  • a que vamos criar só poderá ser utilizada em tablets Android;
  • na versão [rdvmedecins-angular], o cliente web móvel (HTML / CSS / JS) pode ser utilizado em qualquer plataforma (Android, IoS, Windows);

3.2. As vistas do cliente Android

Existem quatro vistas.

Vista de configuração

Image

Tela de seleção do médico e da data da consulta

Image

Página de seleção do horário da consulta

Image

Visão da escolha do cliente para a consulta

Image

3.3. A arquitetura do projeto

Teremos uma arquitetura cliente/servidor semelhante à do exemplo [Exemple-15] (ver parágrafo 1.16) deste documento:

Image

As trocas assíncronas entre o cliente e o servidor serão geridas com a biblioteca RxAndroid.

3.4. A base de dados

Não desempenha um papel fundamental neste documento. Apresentamo-la a título informativo. Chamar-lhe-emos [dbrdvmedecins] . Trata-se de uma base de dados MySQL5 com quatro tabelas:

  

3.4.1. A tabela [MEDECINS]

Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].

  • ID: número que identifica o médico — chave primária da tabela
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
  • NOM: o nome do médico
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

3.4.2. A tabela [CLIENTS]

Os clientes dos diferentes médicos estão registados na tabela [CLIENTS]:

  • ID: número de identificação do cliente — chave primária da tabela
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
  • NOM: o nome do cliente
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

3.4.3. A tabela [CRENEAUX]

Esta tabela lista os intervalos horários em que os RV são possíveis:

  • ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
  • ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
  • HDEBUT: hora de início do intervalo
  • MDEBUT: minutos de início do intervalo
  • HFIN: hora de fim do intervalo
  • MFIN: minutos de fim do intervalo

A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).

3.4.4. A tabela [RV]

Esta tabela lista os RV atribuídos a cada médico:

  • ID: número que identifica o RV de forma única – chave primária
  • JOUR: dia do RV
  • ID_CRENEAU: intervalo horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
  • ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]

Esta tabela possui uma restrição de unicidade na sobre os valores das colunas associadas (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) para as colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança um SQLException quando esta situação ocorre.

A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Srta. Brigitte BISTROU.

3.4.5. Criação da base de dados

Para criar as tabelas e preenchê-las, pode utilizar-se o script [dbrdvmedecins.sql], que se encontra no arquivo de exemplos |ICI|.

  

Com o [WampServer] (ver parágrafo 6.15), pode-se proceder da seguinte forma:

 
  • em [1], clica-se no ícone de [WampServer] e seleciona-se a opção [PhpMyAdmin] [2],
  • em [3], na janela que se abriu, selecione o link [Bases de données],
 
  • em [4-6], importa-se um ficheiro SQL,
  • em [7], seleciona-se o script SQL e, em [8], executa-se o mesmo,
  • em [9], as tabelas da base de dados foram criadas. Segue-se uma das ligações,
 
  • em [10], o conteúdo da tabela.

Posteriormente, não voltaremos a abordar esta base de dados, mas convidamos o leitor a acompanhar a sua evolução ao longo dos testes, sobretudo quando a aplicação não funcionar.

3.5. O servidor web / jSON

Image

Estamos aqui a centrar-nos no servidor [1]. Não vamos desenvolvê-lo. Este foi detalhado no documento [Spring MVC et Thymeleaf par l'exemple]. O leitor interessado poderá consultar esse documento. Foi desenvolvido à semelhança do servidor do exemplo 15. O seu código-fonte é fornecido nos exemplos. Vamos utilizar aqui o seu ficheiro binário:

  
  • [rdvmedecins-server-all-1.0.jar] é o ficheiro binário do servidor;

3.5.1. Implementação

Numa janela de comandos, acedemos à pasta que contém o ficheiro binário do servidor:


...\rdvmedecins>dir
 Le volume dans le lecteur D s’appelle Données
 Le numéro de série du volume est 7A34-AE5F

 Répertoire de D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins

09/06/2016  10:50    <DIR>          .
09/06/2016  10:50    <DIR>          ..
06/07/2014  16:36             7 631 dbrdvmedecins.sql
08/06/2016  16:31    <DIR>          rdvmedecins-client
08/06/2016  16:22    <DIR>          rdvmedecins-server
08/06/2016  16:23        29 618 709 rdvmedecins-server-all-1.0.jar

e, para iniciar o servidor, introduza o seguinte comando (o SGBD MySQL já deve estar em execução):


...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                  (v1.0)

10:55:48.617 [main] INFO  rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO  rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting service Tomcat
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFOS: Starting Servlet Engine: Apache Tomcat/8.0.33
juin 09, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFOS: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO  rdvmedecins.boot.Boot - Started Boot in 8.231 seconds

O servidor apresenta vários registos. Acima, selecionámos apenas aqueles que são úteis para a compreensão:

  • linhas 14-18: é iniciado um servidor Tomcat incorporado na porta 8080 da máquina. É este servidor que executa a aplicação web de gestão de marcações. Esta aplicação é, na verdade, um serviço web / jSON: é consultada através de URL e responde enviando uma cadeia jSON;
  • linha 24: o serviço web é protegido pelo framework [Spring Security]. O acesso às URL do serviço web é feito mediante autenticação;
  • linhas 29-44: os URL expostos pelo serviço web;

Vamos detalhar estas últimas.

3.5.2. Segurança do serviço web

Os URL expostos pelo serviço web estão protegidos. O servidor espera que a solicitação HTTP do cliente inclua o seguinte cabeçalho:

Authorization: Basic code

O código esperado é a codificação em base64 [http://fr.wikipedia.org/wiki/Base64] da cadeia «utilizador:palavra-passe». O serviço web, no seu estado inicial, só aceita um utilizador «admin» com a palavra-passe «admin». O cabeçalho acima torna-se, para este utilizador específico, a seguinte linha:

Authorization: Basic YWRtaW46YWRtaW4=

Para podermos enviar este cabeçalho HTTP, utilizamos o cliente HTTP [Advanced Rest Client], que é um plugin do navegador Chrome (ver parágrafo 6.13). Vamos testar manualmente os diferentes URL expostos pelo serviço web para compreender:

  • os parâmetros esperados pelo URL;
  • a natureza exata da sua resposta;

3.5.3. Lista de médicos

O URL [/getAllMedecins] permite obter a lista de médicos:

  • em [1], o URL consultado;
  • em [2], o método HTTP utilizado para esta consulta;
  • em [3], o cabeçalho de segurança do utilizador (admin, admin) HTTP;
  • em [4], envia-se o pedido HTTP;

A resposta do servidor é a seguinte:

  • em [5], a resposta jSON do servidor, formatada;
  • em [6], a mesma resposta no formato bruto;

O formato [5] permite ver melhor a estrutura da resposta. Todas as respostas do serviço web são uma instância da seguinte classe [Response]:


package rdvmedecins.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // estado da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
...
}
  • linha 9: o estado da resposta. O valor 0 significa que não houve erro; caso contrário, houve um erro;
  • linha 11: uma lista de mensagens de erro, caso tenha ocorrido algum erro;
  • linha 13: a resposta efetivamente esperada pelo cliente;

A resposta ao URL [/getAllMedecins] é a cadeia jSON de um objeto do tipo [Response<List<Medecin>>]. A classe [Medecin] é a seguinte:


package rdvmedecins.android.dao.entities;

public class Medecin extends Personne {

    // construtor por predefinição
    public Medecin() {
    }

    // construtor com parâmetros
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}

Na linha 3, a classe [Medecin] estende a seguinte classe [Personne]:


package rdvmedecins.android.dao.entities;

public class Personne extends AbstractEntity {
    // atributos de uma pessoa
    private String titre;
    private String nom;
    private String prenom;

    // construtor por predefinição
    public Personne() {
    }

    // construtor com parâmetros
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // getters e setters
    ...
}

Na linha 3, a classe [Personne] estende a seguinte classe [AbstractEntity]:


package rdvmedecins.android.dao.entities;

import java.io.Serializable;

public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    protected Long id;
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // inicialização
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // getters e setters
    ...
}

Por fim, a estrutura de um objeto [Medecin] é a seguinte:


[Long id; Long version; String titre; String nom; String prenom;]

e a do [Response<List<Medecin>>] é a seguinte:

[int status; List<String> messages; List<Medecin> medecins]

Posteriormente, utilizaremos estas definições abreviadas para caracterizar a resposta do servidor. Além disso, durante algum tempo, não iremos apresentar mais capturas de ecrã. Basta repetir o que acabámos de ver. Voltaremos às capturas de ecrã quando for necessário efetuar um pedido POST. Apresentaremos também um exemplo de execução com o seguinte formato:

URL

/getAllMedecins
Réponse
{"status":0,"messages":null,"medecins":
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},
{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},
{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]}

3.5.4. Lista de clientes

URL

/getAllClients
Réponse

Response<List<Client>> :[int status; List<String> messages;
 List<Client> clients]
Client : [Long id;  Long version; String titre;
 String nom; String prenom;]

Exemplo:

URL

/getAllClients
Réponse
{"status":0,"messages":null,"clients":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]}

3.5.5. Lista de horários de um médico

URL
/getAllCreneaux/{idMedecin}
Réponse

Response<List<Creneau>>:[int status ; List<String> messages ;
 List<Creneau> creneaux]
Creneau : [int hdebut ; int mdebut ; int hfin ; int mfin ;]
  • [idMedecin]: identificador do médico cujos horários de consulta se pretendem consultar;
  • [hdebut]: hora de início da consulta;
  • [mdebut]: minutos de início da consulta;
  • [hfin]: hora de fim da consulta;
  • [mfin]: minutos de fim da consulta;

Para um intervalo entre as 10h20 e as 10h40, teremos [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].

Exemplo:

URL
/getAllCreneaux/1
Réponse
{"status":0,"messages":null,"creneaux":
[{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1}]}

3.5.6. Lista de consultas de um médico

URL
/getRvMedecinJour/{idMedecin}/{jour}
Réponse

Response<List<Rv>>:[int status ; List<String> messages ;
 List<Rv> rvs]
Rv : [Date jour ; Client client ; Creneau creneau ;
 long idClient ; long idCreneau]
  • [idMedecin]: identificador do médico cujas consultas se pretendem consultar;
  • URL [jour]: dia das consultas no formato «aaaa-mm-dd»;
  • Resposta [jour]: o mesmo, mas no formato de uma data Java;
  • [client]: o cliente da consulta. A sua estrutura foi descrita anteriormente;
  • [idClient]: o identificador do cliente;
  • [creneau]: o intervalo de tempo do compromisso. A sua estrutura foi descrita anteriormente;
  • [idCreneau]: o identificador do intervalo de tempo;

Exemplo:

URL
/getRvMedecinJour/1/2014-07-08
Réponse
{"status":0,"messages":null,
"rvs":[{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}]}

3.5.7. A agenda de um médico

URL
/getAgendaMedecinJour/{idMedecin}/{jour}
Réponse

Response<AgendaMedecinJour>:[int status ; List<String> messages ;
 AgendaMedecinJour agenda]
AgendaMedecinJour : [Medecin medecin ;Date jour ; 
CreneauMedecinJour[] creneauxMedecinJour]
CreneauMedecinJour : [Creneau creneau ; Rv rv]
  • [idMedecin]: identificador do médico cujas consultas se pretendem consultar;
  • URL [jour]: dia das consultas no formato «aaaa-mm-dd»;
  • [agenda]: agenda do médico;
  • [medecin]: o médico em questão. A sua estrutura foi definida anteriormente;
  • Resposta [jour]: o dia da agenda no formato de uma data Java;
  • [creneauxMedecinJour]: um tabuleiro de elementos do tipo [CreneauMedecinJour];
  • [creneau]: um intervalo de tempo. A sua estrutura foi descrita anteriormente;
  • [rv]: um compromisso. A sua estrutura foi descrita anteriormente;

Exemplo:

URL
/getAgendaMedecinJour/1/2014-07-08
Réponse

{"status":0,"messages":null,"agenda":{"medecin":
{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
"jour":1404770400000,"creneauxMedecinJour":[{"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"rv":{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}},{"creneau":
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1},
"rv":null}]}}

Foram destacados os casos em que existe uma consulta nesse intervalo horário e os casos em que não existe.

3.5.8. Obter um médico através do seu identificador

URL
/getMedecinById/{idMedecin}
Réponse

Response<Medecin> :[int status ; List<String> messages ; Medecin medecin]
  • [idMedecin]: o identificador do médico;

Exemplo 1:

URL
/getMedecinById/1
Réponse
{"status":0,"messages":null,"medecin":
{"id":1,"version":1,"titre":"Mme",
"nom":"PELISSIER","prenom":"Marie"}}

Exemplo 2:

URL
/getMedecinById/100
Réponse
{"status":2,
"messages":["Médecin [100] inexistant"],"medecin":null}

3.5.9. Obter um cliente através do seu identificador

URL
/getClientById/{idClient}
Réponse

Response<Client> :[int status ; List<String> messages ;
 Client client]
  • [idClient]: o identificador do cliente;

Exemplo 1:

URL
/getClientById/1
Réponse
{"status":0,"messages":null,"client":{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}}

Exemplo 2:

URL
/getClientById/100
Réponse
{"status":2,"messages":["Client [100] inexistant"],"client":null}

3.5.10. Obter um horário através do seu identificador

URL
/getCreneauById/{idCreneau}
Réponse

Response<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [idCreneau]: o identificador do horário;

Exemplo 1:

URL
/getCreneauById/10
Réponse
{"status":0,"messages":null,"creneau":
{"id":10,"version":1,"hdebut":11,"mdebut":0,
"hfin":11,"mfin":20,"idMedecin":1}}

Note-se que, na resposta, não consta o nome do médico titular do horário, mas apenas o seu identificador.

Exemplo 2:

URL
/getCreneauById/100
Réponse
{"status":2,"messages":["Créneau [100] inexistant"],
"creneau":null}

3.5.11. Marcar uma consulta com o seu identificador

URL
/getRvById/{idRv}
Réponse

Response<Rv> :[int status ; List<String> messages ; Rv rv]
  • [idRv]: o identificador da consulta;

Exemplo 1:

URL
/getRvById/45
Réponse
{"status":0,"messages":null,"rv":{"id":45,"version":0,
"jour":"2014-07-08","idClient":1,"idCreneau":1}}

Note-se que, na resposta, não constam nem o cliente nem o horário da marcação, mas apenas os respetivos identificadores.

Exemplo 2:

URL
/getCreneauById/455
Réponse
{"status":2,"messages":["Rv [455] inexistant"],"rv":null}

3.5.12. Adicionar um compromisso

O URL [/ajouterRv] permite adicionar um compromisso. As informações necessárias para esta adição (o dia, o intervalo horário e o cliente) são transmitidas através de uma solicitação HTTP POST. Mostramos como efetuar esta solicitação com a ferramenta [Advanced Rest Client].

Image

  • em [1], a URL é consultada;
  • em [2], esta é consultada por um POST;
  • em [3-4], especifica-se ao servidor que os valores que lhe são enviados são apresentados sob a forma de uma cadeia jSON;
  • em [4], o cabeçalho HTTP da autenticação;
  • em [5], as informações transmitidas pelo POST. Trata-se de uma cadeia jSON que contém:
    • [jour]: o dia da consulta no formato «aaaa-mm-dd»,
    • [idClient]: o identificador do cliente para quem a consulta foi marcada,
    • [idCreneau]: o identificador do intervalo horário da consulta. Como um intervalo horário pertence a um médico específico, este identificador designa também o médico;
  • em [6], envia-se o pedido;

A cadeia jSON que é enviada corresponde ao objeto do tipo [PostAjouterRv] seguinte:


public class PostAjouterRv {

  // dados da publicação
  private String jour;
  private long idClient;
  private long idCreneau;

  // construtores
  public PostAjouterRv() {

  }

  public PostAjouterRv(String jour, long idCreneau, long idClient) {
    this.jour = jour;
    this.idClient = idClient;
    this.idCreneau = idCreneau;
  }

  // getters e setters
  ...
}

A resposta do servidor é do tipo [Response<Rv>] [int status; List<String> messages; Rv rv], em que [rv] é a consulta adicionada.

A resposta do servidor à solicitação acima é a seguinte:

 

Note-se que, no exemplo acima, algumas informações não estão preenchidas em [idClient, idCreneau], mas encontram-se nos campos [client] e [creneau]. A informação importante é o identificador do compromisso adicionado (209). O serviço web poderia ter-se limitado a devolver apenas esta informação.

3.5.13. Eliminar um compromisso

Esta operação também é realizada através de um POST:

URL
/supprimerRv
POST
{'idRv':idRv}
Réponse

Response<RV> :[int status ; List<String> messages ; Rv rv]

O valor lançado é a cadeia jSON de um objeto do tipo [PostSupprimerRv], como se segue:


public class PostSupprimerRv {

  // dados do post
  private long idRv;

  // construtores
  public PostSupprimerRv() {

  }

  public PostSupprimerRv(long idRv) {
    this.idRv = idRv;
  }

  // getters e setters
  ...
}
  • na linha 4, [idRv] é o identificador do compromisso a eliminar.

Exemplo 1:

URL
/supprimerRv
POST
{"idRv":209}
Réponse
{"status":0,"messages":null,"rv":null}

O compromisso n.º 209 foi efetivamente cancelado devido a [status=0].

Exemplo 2:

URL
/supprimerRv
POST
{"idRv":650}
Réponse
{"status":2,"messages":["Rv [650] inexistant"],"rv":null}

3.6. O cliente Android

Image

Agora que o servidor [1] foi detalhado e está operacional, vamos analisar o cliente Android [2].

3.6.1. Arquitetura do projeto no Android Studio

O projeto segue a arquitetura do projeto [client-android-skel] (ver parágrafo 1.17). Na arquitetura acima do cliente Android, distinguem-se três blocos:

  • a camada [DAO], responsável pela comunicação com o serviço web;
  • as camadas [vues], responsáveis pela comunicação com o utilizador;
  • a [activité], que faz a ligação entre os dois blocos anteriores. As vistas não têm conhecimento da camada [DAO]. Comunicam apenas com a atividade.

Esta arquitetura reflete-se na arquitetura do projeto Android Studio do cliente Android:

 
  • o pacote [activity] implementa a atividade;
  • o pacote [architecture] incorpora os elementos de arquitetura que desenvolvemos anteriormente;
  • o pacote [dao] implementa a camada [DAO];
  • o pacote [fragments] implementa o [vues];

3.6.2. Personalização do projeto

  

A pasta [architecture / custom] contém os elementos personalizáveis da arquitetura.

A interface [IMainActivity] é a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // mudança de vista
  void navigateToView(int position, ISession.Action action);

  // gestão da espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução do pedido do cliente
  int DELAY = 000;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de separadores
  boolean ARE_TABS_NEEDED = false;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos da aplicação
  int FRAGMENTS_COUNT = 4;

  // número de visualizações
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • linhas 25, 28: personalização da camada [DAO];
  • linha 31: esta aplicação efetua acessos autenticados ao servidor;
  • linha 40: é necessária uma imagem de espera;
  • linha 43: a aplicação tem quatro fragmentos;
  • linhas 46-49: os números dos quatro fragmentos;
  • linha 37: não há separadores;

A classe base [CoreState] dos estados dos fragmentos será a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = AccueilFragmentState.class),
  @JsonSubTypes.Type(value = AgendaFragmentState.class),
  @JsonSubTypes.Type(value = AjoutRvFragmentState.class),
  @JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linhas 15-18: os quatro fragmentos têm um estado:
  

Por fim, a sessão contém os dados partilhados entre fragmentos:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;

import java.util.List;

public class Session extends AbstractSession {
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore

  // lista de médicos
  private List<Medecin> médecins;
  // lista de clientes
  private List<Client> clients;
  // agenda de um médico para um determinado dia
  private AgendaMedecinJour agenda;
  // posição do elemento clicado na agenda
  private int position;
  // dia da consulta no formato inglês «yyyy-MM-dd»
  private String dayRv;
  // dia da consulta no formato francês «dd-MM-yyyy»
  private String jourRv;

  // getters e setters
...
}
  • linhas 17-28: a sessão armazena seis informações. Explicaremos o papel destas quando for necessário.

3.6.3. A camada [DAO]

  • em [1], as entidades encapsuladas nas respostas do servidor. Estas foram apresentadas no parágrafo 3.5;
  • em [2], os elementos do cliente que gerem as trocas de dados com o servidor;

Não vamos voltar a abordar os elementos [1]. Estes já foram apresentados. O leitor é convidado a consultar o parágrafo 3.5, se necessário. Vamos estudar a implementação do pacote [service]. Isto levar-nos-á a abordar também a implementação das comunicações seguras entre o cliente e o servidor.

3.6.3.1. Implementação das comunicações cliente/servidor

  

A classe [WebClient] é um componente AA que descreve:

  • as URL expostas pelo serviço web;
  • os seus parâmetros;
  • as respetivas respostas;

package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  public void setRestTemplate(RestTemplate restTemplate);

  // lista de médicos
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();

  // lista de clientes
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();

  // lista de horários de um médico
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);

  // lista de consultas de um médico
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);

  // Cliente
  @Get("/getClientById/{id}")
  public Response<Client> getClientById(@Path long id);

  // Médico
  @Get("/getMedecinById/{id}")
  public Response<Medecin> getMedecinById(@Path long id);

  // Consulta
  @Get("/getRvById/{id}")
  public Response<Rv> getRvById(@Path long id);

  // Intervalo
  @Get("/getCreneauById/{id}")
  public Response<Creneau> getCreneauById(@Path long id);

  // adicionar um RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);

  // eliminar uma consulta
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);

  // obter a agenda de um médico
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

}
  • linhas 19-60: encontram-se todas as URL analisadas no parágrafo 3.5;
  • linha 16: o componente [RestTemplate] de [Spring Android], no qual se baseia a comunicação cliente/servidor;

3.6.3.2. A interface [IDao]

  

A interface [IDao] da camada [DAO] é a seguinte:


package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import rx.Observable;

import java.util.List;

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

  // utilizador
  public void setUser(String user, String mdp);

  // tempo limite do cliente
  public void setTimeout(int timeout);

  // lista de clientes
  public Observable<List<Client>> getAllClients();

  // lista de médicos
  public Observable<List<Medecin>> getAllMedecins();

  // lista de horários disponíveis de um médico
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);

  // lista de consultas de um médico, num determinado dia
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);

  // encontrar um cliente identificado pelo seu ID
  public Observable<Client> getClientById(long id);

  // encontrar um médico identificado pelo seu ID
  public Observable<Medecin> getMedecinById(long id);

  // encontrar uma consulta identificada pelo seu ID
  public Observable<Rv> getRvById(long id);

  // encontrar um intervalo horário identificado pelo seu ID
  public Observable<Creneau> getCreneauById(long id);

  // adicionar um RV
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);

  // eliminar um RV
  public Observable<Rv> supprimerRv(long idRv);

  // função
  public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);
}
  • linha 10: para definir o URL do serviço web / jSON;
  • linha 13: para definir o utilizador da comunicação cliente/servidor. [user] é o identificador do utilizador, [mdp] a sua palavra-passe;
  • linha 16: para definir um tempo máximo de espera pela resposta do servidor;
  • linhas 18-49: a cada URL exposta pelo serviço web corresponde um método. Estas reproduzem a assinatura dos métodos com os mesmos nomes do componente AA [WebClient];
  • linha 52: para controlar o modo debug da camada [DAO];

3.6.3.3. A classe [Dao]

  

A implementação [DAO] da interface [IDao] anterior é a seguinte:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente do serviço web
  @RestService
  protected WebClient webClient;
  // segurança
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // o RestTemplate
  private RestTemplate restTemplate;
  // fábrica do RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    ...
  }

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

  @Override
  public void setUser(String user, String mdp) {
    ...
  }

  @Override
  public void setTimeout(int timeout) {
    ...
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // interceptor de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptor de autenticação
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }

  }

  // métodos privados -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // implementação da interface IDao --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // registo
    log("getAllClients");
    // resultado
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // registo
    log("getAllMedecins");
    // resultado
    return getResponse(new IRequest<Response<List<Medecin>>>() {
      @Override
      public Response<List<Medecin>> getResponse() {
        return webClient.getAllMedecins();
      }
    });
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
    // registo
    log("getAllCreneaux");
    // resultado
    return getResponse(new IRequest<Response<List<Creneau>>>() {
      @Override
      public Response<List<Creneau>> getResponse() {
        return webClient.getAllCreneaux(idMedecin);
      }
    });
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
    // registo
    log("getRvMedecinJour");
    // resultado
    return getResponse(new IRequest<Response<List<Rv>>>() {
      @Override
      public Response<List<Rv>> getResponse() {
        return webClient.getRvMedecinJour(idMedecin, jour);
      }
    });
  }

  @Override
  public Observable<Response<Client>> getClientById(final long id) {
    // registo
    log("getClientById");
    // resultado
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(final long id) {
    // registo
    log("getMedecinById");
    // resultado
    return getResponse(new IRequest<Response<Medecin>>() {
      @Override
      public Response<Medecin> getResponse() {
        return webClient.getMedecinById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> getRvById(final long id) {
    // registo
    log("getRvById");
    // resultado
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.getRvById(id);
      }
    });
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // registo
    log("getCreneauById");
    // resultado
    return getResponse(new IRequest<Response<Creneau>>() {
      @Override
      public Response<Creneau> getResponse() {
        return webClient.getCreneauById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
    // registo
    log("ajouterRv");
    // resultado
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
      }
    });
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(final long idRv) {
    // registo
    log("supprimerRv");
    // resultado
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.supprimerRv(new PostSupprimerRv(idRv));
      }
    });
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // registo
    log("getAgendaMedecinJour");
    // resultado
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }

}
  • linhas 18-72: são as que se encontram por defeito na classe [Dao] do projeto [client-android-skel];
  • linhas 74-216: implementação da interface [IDao]. Os métodos que consultam o URL expostos pelo serviço web delegam essa consulta ao componente AA [WebClient] (linhas 22-23);
  • linhas 58-63: se as comunicações cliente/servidor forem autenticadas por uma autorização do tipo básica, é adicionado um interceptor ao componente [RestTemplate]. Isto fará com que qualquer pedido HTTP emitido pelo componente [RestTemplate] seja interceptado pela classe [MyAuthInterceptor] (linhas 25-26);

A classe [MyAuthInterceptor] é a seguinte:


package rdvmedecins.android.dao.security;

import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // utilizador
  private String user;
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    HttpHeaders headers = request.getHeaders();
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    headers.setAuthorization(auth);
    return execution.execute(request, body);
  }

  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}
  • linha 15: a classe [MyAuthInterceptor] é um componente AA do tipo [singleton];
  • linha 16: a classe [MyAuthInterceptor] estende a interface Spring [ClientHttpRequestInterceptor]. Esta interface possui um método, o método [intercept] da linha 22. Esta interface é estendida para interceptar qualquer pedido HTTP do cliente. O método [intercept] recebe três parâmetros;
    • [HtpRequest request]: a solicitação HTTP interceptada,
    • [byte[] body]: o seu corpo, caso exista (por exemplo, valores enviados via POST),
    • [ClientHttpRequestExecution execution]: o componente Spring que executa a solicitação;

Interceptamos todas as solicitações HTTP do cliente Android para lhes adicionar o cabeçalho de autenticação HTTP apresentado no parágrafo 3.5.

  • linha 23: recuperamos os cabeçalhos HTTP da solicitação interceptada;
  • linha 24: criamos o cabeçalho de autenticação HTTP. O modo de autenticação utilizado (codificação base64 da cadeia «user:mdp») é fornecido pela classe Spring [HttpBasicAuthentication];
  • linha 25: o cabeçalho de autenticação que acabámos de criar é adicionado aos cabeçalhos atuais da solicitação interceptada;
  • linha 26: prossegue-se com a execução da solicitação interceptada. Resumindo, a solicitação interceptada foi enriquecida com o cabeçalho de autenticação;

As implementações dos métodos da interface [IDao] seguem todas o mesmo modelo. Tomemos como exemplo o método [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // registo
    log("getAgendaMedecinJour");
    // resultado
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • linha 2: o método espera dois parâmetros:
    • [idMedecin]: o identificador do médico cuja agenda se pretende consultar;
    • [jour]: o dia para o qual se pretende a agenda;
  • linha 6: chama-se o método [getResponse] da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que T é o tipo devolvido pelo método [getAgendaMedecinJour] da linha 2, neste caso [Response<AgendaMedecinJour>]. A interface [IRequest] tem apenas um método: [getResponse] (linha 8);
  • linhas 8-10: implementação do método [IRequest.getResponse]. Este método deve devolver o resultado esperado pelo método [getAgendaMedecinJour], na linha 2, do tipo [Response<AgendaMedecinJour>];
  • linha 9: a resposta é devolvida pelo método [webClient.getAgendaMedecinJour]:

  // consultar a agenda de um médico
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

Os parâmetros utilizados na linha 9 são os que foram passados ao método [getAgendaMedecinJour] na linha 2. Por este motivo, estes parâmetros devem ter o atributo final;

3.6.4. A atividade [MainActivity]

Serveur
  

A classe [MainActivity] é a seguinte:


package client.android.activity;

import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.AccueilFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AjoutRvFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;

import java.util.List;

@EActivity
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // classe dos pais ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
    return fragments;
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // interface IDao -----------------------------------------------------
...

  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
    return dao.getAllCreneaux(idMedecin);
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
    return dao.getRvMedecinJour(idMedecin, jour);
  }

  @Override
  public Observable<Response<Client>> getClientById(long id) {
    return dao.getClientById(id);
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(long id) {
    return dao.getMedecinById(id);
  }

  @Override
  public Observable<Response<Rv>> getRvById(long id) {
    return dao.getRvById(id);
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(long id) {
    return dao.getCreneauById(id);
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
    return dao.ajouterRv(jour, idCreneau, idClient);
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(long idRv) {
    return dao.supprimerRv(idRv);
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
    return dao.getAgendaMedecinJour(idMedecin, jour);
  }
}
  • linhas 21-66: estas linhas são fornecidas por predefinição no modelo [client-android-skel];
  • linhas 66-119: implementação da interface [IDao]. Todos os métodos delegam o trabalho à camada [DAO] da linha 26;
  • linhas 42-46: o método [getFragments] devolve o array com os quatro fragmentos da aplicação;
  • linhas 58-61: a vista de configuração é a primeira vista a ser apresentada quando a aplicação é iniciada;

3.6.5. A sessão

  

A classe [Session] serve para armazenar as informações que devem ser transmitidas entre fragmentos. É a seguinte:


package rdvmedecins.android.architecture;

import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Medecin;
import org.androidannotations.annotations.EBean;

import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // lista de médicos
  private List<Medecin> médecins;
  // lista de clientes
  private List<Client> clients;
  // agenda
  private AgendaMedecinJour agenda;
  // posição do elemento clicado na agenda
  private int position;
  // dia da consulta no formato inglês «yyyy-MM-dd»
  private String dayRv;
  // dia do compromisso no formato francês «dd-MM-yyyy»
  private String jourRv;


  // getters e setters
...
}
  • linha 10: a classe [Session] é um componente AA instanciado numa única instância;
  • linhas 12-15: neste estudo de caso, assumiremos que as listas de médicos e de clientes não se alteram. Estas serão solicitadas no arranque da aplicação e armazenadas na sessão para que os fragmentos as possam utilizar;
  • linhas 20-23: o dia pretendido para uma consulta. É tratado de duas formas: na notação francesa (linha 23) na aplicação Android, e na notação inglesa (linha 21) para as comunicações com o servidor;
  • linha 19: a posição do elemento clicado (ligação «adicionar»/«eliminar») na agenda;

3.6.6. Gestão da vista de configuração

3.6.6.1. A vista

A vista de configuração é a vista apresentada ao iniciar a aplicação:

Image

Os elementos da interface visual são os seguintes:

Type
Nom
1
EditText
edtUrlServiceRest
3
EditText
edtUtilisateur
5
EditText
edtMdp
2
TextView
txtErrorUrlServiceRest
3
TextView
txtErrorUtilisateur

3.6.6.2. O fragmento

A vista de configuração é gerida pelo seguinte fragmento [ConfigFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.net.URI;
import java.util.List;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {

  // os elementos da interface visual
  @ViewById(R.id.edt_urlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_errorUrlServiceRest)
  protected TextView txtErrorUrlServiceRest;
  @ViewById(R.id.txt_errorUtilisateur)
  protected TextView txtErrorUtilisateur;
  @ViewById(R.id.edt_utilisateur)
  protected EditText edtUtilisateur;
  @ViewById(R.id.edt_mdp)
  protected EditText edtMdp;

  // os campos de introdução de dados
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;

  // validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // implementação dos métodos da classe pai -------------------------------------------
 ...

}
  • linha 25: o fragmento está associado ao seguinte menu [menu_config]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>

</menu>
  • linhas 28-38: os elementos da interface visual;
  • linhas 41-43: os três campos de preenchimento do formulário;

O clique na opção de menu [Valider] é gerido pelo método [doValider]:


// validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // ocultamos eventuais mensagens de erro anteriores
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // verifica-se a validade dos dados introduzidos
    if (!isPageValid()) {
      return;
    }
    // preenche-se o URL do serviço web
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // preenche-se o utilizador
    mainActivity.setUser(utilisateur, mdp);
    // início da espera — vamos iniciar duas tarefas assíncronas
    beginWaiting(2);
    // médicos
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // processa-se a resposta
        consumeMedecins(responseMedecins);
      }
    });
    // clientes
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // a resposta é processada
        consumeClients(responseClients);
      }
    });
  }


  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // erro?
    if (responseMedecins.getStatus() != 0) {
      // mensagem
      showAlert(responseMedecins.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // os médicos são guardados na sessão
    session.setMédecins(responseMedecins.getBody());
  }

  private void consumeClients(Response<List<Client>> responseClients) {
    // registo
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // erro?
    if (responseClients.getStatus() != 0) {
      // mensagem
      showAlert(responseClients.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // os clientes são memorizados na sessão
    session.setClients(responseClients.getBody());
  }
  • linhas 8-10: verifica-se a validade dos três campos de preenchimento do formulário. Se o formulário for inválido, o processo não prossegue;
  • linhas 11-14: os dados necessários para a camada [DAO] são passados para a atividade;
  • linha 16: indica-se à classe pai que se vão iniciar duas tarefas assíncronas e prepara-se a espera;
  • linhas 17-24: é solicitada a lista de médicos;
  • linha 18: o método [executeInBackground] espera dois parâmetros:
    • linha 18: o processo a executar e a observar é fornecido pelo método [mainActivity.getAllMedecins()];
    • linhas 18-24: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<List<Medecin>>]
  • linha 22: quando se recebe a resposta, esta é passada para o método [consumeMedecins] da linha 36;
  • linhas 25-33: após ter lançado uma primeira tarefa assíncrona, lança-se uma segunda para solicitar a lista de clientes. Teremos, portanto, duas tarefas a serem executadas em paralelo;
  • linhas 36-52: recebemos a resposta da tarefa relativa aos médicos. Analisamos essa resposta;
  • linhas 42-49: verificamos primeiro se o servidor sinalizou um erro no campo [status] da resposta;
  • linha 44: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
  • linha 46: cancelam-se todas as tarefas;
  • linha 48: regressa-se à interface do utilizador;
  • linha 51: se não tiver havido erros, a lista de médicos é carregada na sessão;

A validade dos dados introduzidos (linha 8) é verificada através do seguinte método:


  private boolean isPageValid() {
    // verifica-se a validade dos dados introduzidos
    boolean erreur;
    URI service;
    // validade do URL do serviço REST
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    try {
      service = new URI(urlServiceRest);
      erreur = service.getHost() == null || service.getPort() == -1;
    } catch (Exception ex) {
      // regista-se o erro
      erreur = true;
    }
    if (erreur) {
      // exibição do erro
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // utilizador
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // é apresentado o erro
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // regista-se o erro
      erreur = true;
    }
    // palavra-passe
    mdp = edtMdp.getText().toString().trim();
    // voltar
    return !erreur;
}

O método [beginWaiting] (linha 16) é o seguinte:


  // início da espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepara-se o lançamento das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // estado dos botões e menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • linha 4: indica-se à tarefa principal que se vai iniciar as tarefas [numberOfRunningTasks];
  • linha 6: ocultam-se todas as opções do menu;
  • linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];

O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelamento das tarefas assíncronas
    cancelRunningTasks();
}
  • linha 8: solicita-se à classe pai que cancele as tarefas assíncronas;

3.6.6.3. Gestão do ciclo de vida do fragmento

O fragmento apresenta o seguinte estado [ConfigFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // visibilidade das duas mensagens de erro
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;

  // getters e setters
...
}
  • quando a classe pai o solicitar, o fragmento guardará a visibilidade das suas duas mensagens de erro;

O ciclo de vida do fragmento é implementado da seguinte forma:


// Implementação dos métodos da classe pai -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // guardar o estado do fragmento
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
    state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected int getNumView() {
    return     IMainActivity.VUE_CONFIG;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    if (previousState == null) {
      // 1.ª visita
      // ocultamos as mensagens de erro
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restabelecimento da visibilidade das mensagens de erro
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // não é a primeira visita - restabelecer as mensagens de erro
    txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
  }


  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // próxima vista?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }

  // métodos privados ------------------------------------------------
  private void initMenu(){
    // estado do menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • linhas 2-9: quando a sua classe pai o solicitar, o fragmento guarda o estado das suas duas mensagens de erro;
  • linhas 11-14: o número do fragmento é [IMainActivity.VUE_CONFIG];
  • linhas 16-19: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado nas vezes seguintes (previousState != null). Aqui, não há nada a fazer;
  • linhas 21-31: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
    • linhas 24-29: na primeira visita, ocultam-se as mensagens de erro e apresenta-se o menu sem a ação [Annuler] (linhas 62-66);
  • linhas 33-35: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Isso nunca acontece aqui;
  • linhas 37-44: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE]. Recria-se o estado das mensagens de erro a partir do estado anterior;
  • linhas 47-49: executadas quando todas as atualizações anteriores tiverem sido feitas. Não há mais nada a fazer;
  • linhas 51-59: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linhas 53-54: o menu é reposto no seu estado predefinido;
    • linhas 56-58: se as tarefas tiverem sido concluídas normalmente, passa-se para a vista seguinte; caso contrário, permanece-se na mesma vista;

3.6.7. Gestão da vista inicial

3.6.7.1. A vista

A vista inicial é a seguinte:

Image

Os elementos da interface visual são os seguintes:

Type
Nom
1
Spinner
spinnerMedecins
2
DatePicker
edtJourRv

3.6.7.2. O fragmento

A página inicial é gerida pelo seguinte fragmento [AccueilFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.util.Calendar;
import java.util.List;
import java.util.Locale;

@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {

  // elementos da interface visual
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;

  // dados locais
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;

  // validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...

  // implementação de métodos da classe pai -------------------------------------
...
}
  • linha 26: o fragmento está associado ao seguinte menu [menu_accueil]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
    </menu>
  </item>
</menu>
  • linhas 31-34: os elementos da interface visual;
  • linha 37: a lista de médicos;
  • linha 38: um calendário;
  • linha 39: a fonte de dados do spinner dos médicos;

O clique no link [Valider] é gerido pelo seguinte método [doValider]:


// validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // regista-se o ID do médico selecionado
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // o dia é guardado na sessão
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // converte-se para o formato de data aaaa-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // início da espera — vai ser iniciada uma tarefa assíncrona
    beginWaiting(1);
    // solicita-se a agenda do médico
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // processa-se a resposta
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // erro?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // mensagem
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // coloca-se a agenda na sessão
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • linha 5: recupera-se o identificador do médico selecionado;
  • linhas 7-8: insere-se, no formato francês, a data escolhida;
  • linhas 10-11: insere-se, no formato inglês, a data escolhida;
  • linha 13: indica-se à classe pai que se vai iniciar uma tarefa assíncrona e prepara-se a espera;
  • linhas 15-22: solicita-se a agenda do médico;
    • linha 15: o método [executeInBackground] espera dois parâmetros:
      • linha 15: o processo a executar e a observar é fornecido pelo método [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
      • linhas 15-22: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<AgendaMedecinJour>]
    • linha 20: quando se recebe a resposta, esta é passada para o método [consumeAgenda] da linha 25;
  • linhas 25-37: recebemos a agenda do médico. Analisamo-la;
  • linhas 27-34: verifica-se, em primeiro lugar, se o servidor sinalizou um erro no campo [status] da resposta;
  • linha 29: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
  • linha 31: cancelam-se todas as tarefas;
  • linha 33: regressa-se à interface do utilizador;
  • linha 36: se não tiver havido erros, a agenda é ativada;

O método [beginWaiting] (linha 13) é o seguinte:


  // início da espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepara-se o lançamento das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // estado dos botões e menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • linha 4: indica-se à tarefa pai que se vai iniciar as tarefas [numberOfRunningTasks];
  • linha 6: ocultam-se todas as opções do menu;
  • linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];

O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelam-se as tarefas assíncronas
    cancelRunningTasks();
}
  • linha 8: solicita-se à classe pai que cancele as tarefas assíncronas;

O clique na opção de menu [Retour à la configuration] é tratado da seguinte forma:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navegação para a vista de configuração
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • linha 4: navega-se para a vista de configuração com a ação [NAVIGATION]. Isto significa que se pretende recuperar a vista de configuração no estado em que foi deixada;

3.6.7.3. Gestão do ciclo de vida do fragmento

O fragmento apresenta o seguinte estado [AccueilFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AccueilFragmentState extends CoreState {

  // estado do fragmento [Accueil]
  // posição do médico selecionado
  private int selectedMedecinPosition;
  // data selecionada
  private int year;
  private int month;
  private int dayOfMonth;
  // fonte de dados do seletor de médicos
  private String[] spinnerMedecinsDataSource;

  // construtores
  public AccueilFragmentState() {

  }

  // getters e setters
...
}
  • linha 11: permite recuperar o elemento selecionado na lista de médicos;
  • linhas 13-15: permite recuperar a data selecionada no calendário;
  • linha 17: permite recuperar a fonte de dados da lista de médicos;

O ciclo de vida do fragmento é implementado da seguinte forma:


// implementação dos métodos da classe pai -------------------------------------
  @Override
  public CoreState saveFragment() {
    // guardamos a vista
    AccueilFragmentState state = new AccueilFragmentState();
    state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
    state.setDayOfMonth(edtJourRv.getDayOfMonth());
    state.setMonth(edtJourRv.getMonth());
    state.setYear(edtJourRv.getYear());
    state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_ACCUEIL;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // recuperar os médicos da sessão
    medecins = session.getMédecins();
    // 1.ª consulta?
    if (previousState == null) {
      // construção da tabela apresentada pelo spinner
      spinnerMedecinsDataSource = new String[medecins.size()];
      int i = 0;
      for (Medecin medecin : medecins) {
        spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
        i++;
      }
    } else {
      // não é a primeira consulta
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // o calendário
    calendrier = Calendar.getInstance();
  }

  @Override
  protected void initView(CoreState previousState) {
    // associamos o spinner dos médicos à sua fonte de dados
    ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
    dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerMedecins.setAdapter(dataAdapterMedecins);
    // data mínima do calendário até hoje
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // 1.ª consulta?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restaura-se o estado da sessão atual
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // seleção de médicos no spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // calendário
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // chamada depois de todas as tarefas estarem concluídas ou canceladas
    // estado do menu
    initMenu();
    // próxima vista?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }

  // métodos privados ------------------------------------------------
  private void initMenu() {
    // estado do menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • linhas 2-9: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
    • linha 6: a posição selecionada na lista de médicos;
    • linhas 7-9: o dia do mês, o mês e o ano da data selecionada no calendário;
    • linha 10: a fonte de dados do spinner dos médicos;
  • linhas 14-17: o n.º do fragmento é [IMainActivity.VUE_ACCUEIL];
  • linhas 19-39: executadas quando o fragmento é gerado pela primeira vez (previousState==null) ou regenerado nas vezes seguintes (previousState !=null);
    • linhas 25-31: no caso de uma primeira visita, a fonte de dados do spinner dos médicos é criada;
    • linhas 33-35: para as restantes visitas, a fonte de dados do spinner é recuperada do estado anterior do fragmento;
  • linhas 41-54: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
    • linhas 50-53: na primeira visita, é apresentado o menu sem a ação [Annuler] (linhas 88-92);
    • linhas 43-48: para todas as visitas, seja a primeira ou não, associa-se o spinner dos médicos à sua fonte (linhas 44-46) e define-se a data mínima do calendário como a data de hoje (linha 48);
  • linhas 56-60: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Nesse caso, provém-se da vista [CONFIG]. Coloca-se o menu no seu estado inicial;
  • linhas 62-70: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
    • linha 67: reposiciona o spinner dos médicos no último médico selecionado;
    • linha 69: posiciona-se o calendário na última data escolhida;
  • linhas 72-74: executadas quando todas as atualizações anteriores tiverem sido feitas. Não há mais nada a fazer;
  • linhas 76-85: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 80: o menu é reposto no seu estado predefinido;
    • linhas 82-84: se as tarefas tiverem sido concluídas normalmente, passa-se para a vista seguinte; caso contrário, permanece-se na mesma vista;

3.6.8. Gestão da vista Agenda

3.6.8.1. A vista

A vista inicial é a seguinte:

Image

Os elementos da interface visual são os seguintes:

Type
Nom
1
TextView
txtTitre2
2
ListView
lstCreneaux

3.6.8.2. O fragmento

A vista Agenda é gerida pelo seguinte fragmento [AgendaFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {

  // elementos da interface visual
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;

  // agenda apresentada pelo fragmento
  private AgendaMedecinJour agenda;
  // informações ListView sobre os horários
  private int firstPosition;
  private int top;
  // compromisso eliminado ou não
  private boolean rdvSupprimé;
  // n.º do intervalo de tempo adicionado ou eliminado
  private int numCréneau;

  // atualização da agenda após uma adição/eliminação
  private void updateAgenda() {
  ...
  }

...

  // implementação de métodos da classe pai ------------------------------------------------------
  ...
}
  • linha 27: o fragmento está associado ao seguinte menu [menu_agenda]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionAgenda"
        android:title="@string/actionAgenda"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
    </menu>
  </item>
</menu>
  • linhas 32-35: os elementos da interface visual;
  • linhas 37-45: dados globais dos métodos;

3.6.8.2.1. Método [updateAgenda]

A (re)geração da lista de intervalos da agenda é necessária em vários pontos do código. Foi factorizada no seguinte método privado [updateAgenda]:


  // atualização da agenda após uma adição/eliminação
  private void updateAgenda() {
    // (re)geração dos intervalos da agenda
    // a agenda é capturada na sessão e armazenada num campo do fragmento
    agenda = session.getAgenda();
    // regeneração do ListView dos intervalos
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // reposiciona-se no local correto do ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • linha 5: a agenda é obtida da sessão e armazenada no campo [agenda] do fragmento;
  • linhas 7-9: define-se o adaptador do componente [ListView]. Este adaptador define tanto a fonte de dados do [ListView] como o modelo de visualização de cada elemento da mesma. Apresentaremos este adaptador em breve;
  • linha 11: regressamos à posição anterior da agenda. Com efeito, apenas se vê uma parte dos intervalos do dia. Se adicionarmos ou eliminarmos um compromisso no último intervalo, o código acima irá atualizar a página para apresentar a nova agenda. Esta atualização faz com que fiquemos novamente posicionados no primeiro intervalo, o que não é desejável. A linha 5 resolve este problema. A descrição desta solução pode ser encontrada no URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

A classe [ListCreneauxAdapter] serve para definir uma linha do [ListView]:

Image

Como se pode ver acima, dependendo de o intervalo de tempo ter ou não um compromisso, a apresentação não é a mesma. O código da classe [ListCreneauxAdapter] é o seguinte:


...

public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {

    // a tabela de intervalos horários
    private CreneauMedecinJour[] creneauxMedecinJour;
    // o contexto de execução
    private Context context;
    // o ID do layout de visualização de uma linha da lista de intervalos
    private int layoutResourceId;
    // ouvinte de cliques
    private AgendaFragment vue;

    // construtor
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // guardamos as informações
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // ordena-se a tabela de intervalos por ordem horária
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ...
}

// ordenação da tabela de intervalos
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • linha 3: a classe [ListCreneauxAdapter] deve estender um adaptador predefinido para as classes [ListView], neste caso, a classe [ArrayAdapter] que, tal como o próprio nome indica, alimenta a [ListView] com um tabuleiro de objetos, neste caso do tipo [CreneauMedecinJour]. Recorde-se o código desta entidade:

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campos
    private Creneau creneau;
    private Rv rv;
...  
}
  • a classe [CreneauMedecinJour] contém um intervalo horário (linha 5) e um eventual compromisso (linha 6) ou null, caso não haja compromisso;

Voltando ao código da classe [ListCreneauxAdapter]:

  • linha 15: o construtor recebe quatro parâmetros:
    1. a atividade Android em execução,
    2. o ficheiro XML que define o conteúdo de cada elemento do [ListView],
    3. a tabela de horários do médico,
    4. a própria vista;
  • linha 24: a tabela dos horários está ordenada por ordem crescente dos horários;

O método [getView] é responsável por gerar a vista correspondente a uma linha do [ListView]. Esta inclui três elementos:

 
Id
Type
Rôle
1
txtCreneau
TextView
créneau horaire
2
txtClient
TextView
le client
3
btnValider
TextView
lien pour ajouter / supprimer un rendez-vous

O código do método [getView] é o seguinte:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // seleciona-se o intervalo correto
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // cria-se a linha
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // o intervalo horário
        TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
        txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
                .getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
        // o cliente
        TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
        String text;
        if (creneauMedecin.getRv() != null) {
            Client client = creneauMedecin.getRv().getClient();
            text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        } else {
            text = "";
        }
        txtClient.setText(text);
        // o link
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // adicionar
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // eliminar
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // ouvinte do link
        btnValider.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // passamos as informações para a vista da agenda
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // formata a linha
        return row;
    }
  • linha 2: «position» é o número da linha que vai ser gerada no [ListView]. É também o número do intervalo horário na tabela [creneauxMedecinJour]. Ignora-se os outros dois parâmetros;
  • linha 4: recupera-se o intervalo horário a apresentar na linha do [ListView];
  • linha 6: a linha é construída a partir da sua definição XML
 

O código de [creneau_medecin.xml] é o seguinte:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat" >

    <TextView
        android:id="@+id/txt_Creneau"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/txt_Client"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Creneau"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Creneau"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/btn_Valider"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Client"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Client"
        android:text="@string/btn_valider"
        android:textColor="@color/blue" />

</RelativeLayout>
 
  • linhas 8-10: o intervalo horário [1] é construído;
  • linhas 12-20: a identidade do cliente [2] é criada;
  • linha 23: se o intervalo de tempo não tiver qualquer compromisso;
  • linhas 25-26: cria-se o link [Ajouter] na cor azul;
  • linhas 29-30: caso contrário, cria-se o link [Supprimer] na cor vermelha;
  • linhas 33-40: independentemente da natureza do link [Ajouter / Supprimer], é o método [doValider] da vista que irá gerir o clique no link. O método receberá dois argumentos:
    1. o número do intervalo em que se clicou,
    2. o texto do link em que se clicou;
  • linha 42: devolve-se a linha que acabou de ser construída.

Note-se que é o método [doValider] do fragmento [AgendaFragment] que gere os links. Este é o seguinte:


  // clique num link [Ajouter / Supprimer]
  public void doValider(int numCréneau, String texte) {
    // operação em curso?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // anotamos a posição da barra de deslocamento para voltar a ela
    // ler [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posição do primeiro elemento, visível na totalidade ou não
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // desvio no eixo Y deste elemento em relação à parte superior do ListView
    // mede a altura da parte eventualmente oculta
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // regista-se também o número do intervalo em que se clicou
    this.numCréneau = numCréneau;
    // dependendo do texto do link, não se faz a mesma coisa
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • o método [doValider] recebe duas informações:
    • o número do intervalo em que se clicou;
    • o texto («Adicionar» / «Eliminar») do link em que se clicou;
  • linhas 4-7: o clique nos links [Supprimer / Ajouter] é inibido se houver tarefas assíncronas em curso. Trata-se de uma escolha que facilita a escrita do código. Pode ser discutida;
  • linhas 11-15: registam-se as informações (firstPosition, top) do ListView dos intervalos nos campos do fragmento, para que o método privado [updateAgenda] possa regenerá-lo com a mesma posição de deslocamento;
  • linha 17: regista-se o n.º do intervalo em que se clicou;
  • linhas 19-23: consoante o texto do link clicado, efetua-se uma adição ou uma eliminação;

3.6.8.2.2. Método [doSupprimer]

O método [doSupprimer] assegura a eliminação do compromisso do intervalo em que se clicou:


// eliminação de um compromisso
  private void doSupprimer() {
    // aguarda a conclusão de duas tarefas
    beginWaiting(2);
    // o compromisso é eliminado em segundo plano
    rdvSupprimé = false;
    // identificador da consulta a eliminar
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // eliminação por uma tarefa assíncrona
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {

      @Override
      public void call(Response<Rv> responseRv) {
        // utilização do resultado
        consumeRv(responseRv);
      }
    });
  }

  // utilização de uma resposta
  private void consumeRv(Response<Rv> responseRv) {
    // erro?
    if (responseRv.getStatus() != 0) {
      // mensagem
      showAlert(responseRv.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // observa-se que a marcação foi eliminada
    rdvSupprimé = true;
    // solicita-se a agenda mais recente
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {

        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // processa-se a resposta
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }

  // consumo de uma agenda
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // erro?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // mensagem
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // coloca-se a agenda na sessão
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // atualiza-se a agenda da vista
    updateAgenda();
  }
  • linha 4: notifica-se a classe pai de que se vão iniciar duas tarefas assíncronas e inicia-se a espera pelo término dessas duas tarefas;
  • linha 8: recupera-se o identificador do compromisso a eliminar. Com efeito, o servidor necessita desta informação;
  • linhas 9-18: solicita-se a eliminação do compromisso através de uma tarefa assíncrona;
    • linha 10: o método [executeInBackground] espera dois parâmetros:
      • linha 10: o processo a executar e a observar é fornecido pelo método [mainActivity.supprimerRv(idRv)];
      • linhas 10-17: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
    • linha 15: quando se recebe a resposta, esta é passada para o método [consumeRv] da linha 21;
  • linhas 21-44: recebemos a resposta da tarefa assíncrona. Processamos essa resposta;
  • linhas 23-30: verifica-se primeiro se o servidor sinalizou um erro no campo [status] da resposta;
    • linha 25: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
    • linha 27: cancelam-se todas as tarefas;
    • linha 29: regressa-se à interface do utilizador;
  • linha 32: se não tiver ocorrido nenhum erro, regista-se que a consulta foi eliminada;
  • linhas 34-43: em vez de simplesmente eliminar a consulta da agenda atualmente apresentada pelo fragmento, solicita-se a nova agenda do médico. Com efeito, a aplicação é multiutilizador e outros utilizadores também podem ter alterado a agenda do médico. Por isso, é melhor ter a versão mais recente;
  • linhas 34-43, 47-61: repete-se o que tinha sido feito no fragmento [AccueilFragment], desta vez com informações obtidas da sessão;

O método [beginWaiting] (linha 4) é o seguinte:


  // início da espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepara-se o lançamento das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // estado dos botões e menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • linha 4: indica-se à tarefa pai que se vai iniciar as tarefas [numberOfRunningTasks];
  • linha 6: ocultam-se todas as opções do menu;
  • linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];

3.6.8.2.3. Método [doAnnuler]

O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelam-se as tarefas assíncronas
    cancelRunningTasks();
}
  • linha 7: solicita-se à classe pai que cancele as tarefas assíncronas;

3.6.8.2.4. Opção de menu [Retour à la configuration]

Ao clicar na opção de menu [Retour à la configuration], o processo é gerido da seguinte forma:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navegação para a vista de configuração
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • linha 4: acede-se à vista de configuração com a ação [NAVIGATION]. Isto significa que se pretende recuperar a vista de configuração no estado em que foi deixada;

3.6.8.2.5. Opção de menu [Retour à l'accueil]

O clique na opção de menu [Retour à l'accueil] é tratado de forma semelhante:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // navega-se para a vista inicial
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. Gestão do ciclo de vida do fragmento

O fragmento apresenta o seguinte estado: [AgendaFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AgendaFragmentState extends CoreState {

  // título da vista
  private String titre;
  // ListView
  private int firstPosition;
  private int top;

  // construtores
  public AgendaFragmentState() {

  }

  public AgendaFragmentState(String titre) {
    this.titre = titre;
  }

  // getters e setters
...
}
  • linha 10: o título exibido na parte superior da vista;
  • linhas 12-13: permitem apresentar o scrolling do ListView dos horários do médico;

O ciclo de vida do fragmento é implementado da seguinte forma:


// implementação de métodos da classe pai ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // guardar estado
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // registar a posição do scroll para voltar a ela
    // ler [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posição do primeiro elemento visível na totalidade ou parcialmente
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // desvio no eixo Y deste elemento em relação à parte superior do ListView
    // mede a altura da parte eventualmente oculta
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // guardamos tudo isto
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AGENDA;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 1.ª visita?
    if (previousState != null) {
      // não é a primeira visita
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // e as informações do ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // recuperamos a agenda
    agenda = session.getAgenda();
    // geramos o título da página
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // estado do menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // regenera-se o título da página
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }

  @Override
  protected void notifyEndOfUpdates() {
    // regenera a lista de horários
    updateAgenda();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // estado do menu
    initMenu();
    // Se houver um cancelamento, mas a reunião tiver sido eliminada, é necessário atualizar a agenda local
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // elimina-se o compromisso da agenda local (não foi possível aceder à agenda global)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // atualiza-se a interface visual
      updateAgenda();
    }
  }


  // métodos privados ------------------------------------------------
  private void initMenu() {
    // estado do menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • linhas 2-19: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
    • linha 6: o título exibido na parte superior da vista;
    • linhas 7-17: as informações (top, firstPosition) que permitirão restabelecer o scrolling do ListView;
  • linhas 21-24: o número do fragmento é [IMainActivity.VUE_AGENDA];
  • linhas 26-35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado nas vezes seguintes (previousState != null);
    • linhas 30-34: se não for a primeira visita ao fragmento, recuperam-se as informações (top, firstPosition) que permitirão restabelecer o scrolling a partir do ListView;
  • linhas 38-40: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída nas vezes seguintes (previousState != null). Não há nada a fazer aqui, porque o ListView dos intervalos será gerado pelo método privado [updateAgenda] (linhas 61-65);
  • linhas 42-52: executadas quando se chega ao fragmento através de uma operação [SUBMIT]. Vem-se, então, da vista [ACCUEIL];
    • linha 45: recupera-se a agenda ativada pela sessão por [AccueilFragment];
    • linhas 47-49: gera-se o título da vista;
    • o ListView dos intervalos de tempo será gerado pelo método privado [updateAgenda] (linhas 61-65);
  • linhas 54-59: executadas quando se chega ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
    • linhas 57-58: regenera-se o título da vista;
    • o ListView dos intervalos será gerado pelo método privado [updateAgenda] (linhas 61-65);
  • linhas 72-74: executadas quando todas as atualizações anteriores tiverem sido efetuadas. Atualiza-se o ListView dos intervalos, uma vez que esta atualização é necessária independentemente da forma como se acede ao fragmento;
  • linhas 67-77: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 70: o menu é reposto no seu estado predefinido (linhas 82-86);
    • linha 72: havia duas tarefas assíncronas. Verifica-se se a primeira (a eliminação do compromisso) foi bem-sucedida, apesar de ter sido cancelada;
    • linha 74: se sim, elimina-se o compromisso da agenda local
    • linha 75: e atualiza-se a visualização do mesmo;

3.6.9. Gestão da vista de adição de um compromisso

3.6.9.1. A vista

A vista de adição de um compromisso é a seguinte:

Image

Os elementos da interface visual são os seguintes:

Type
Nom
1
TextView
txtTitre2
2
Spinner
spinnerClients

3.6.9.2. O fragmento

A vista de adição de um compromisso é gerida pelo seguinte fragmento [AjoutRvFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

import java.util.List;
import java.util.Locale;

@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {

  // elementos da interface visual
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;

  // os clientes
  private List<Client> clients;

  // dados locais
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;

  // validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...

  // implementação dos métodos da classe pai ----------------------------------
...
}
  • linha 26: o fragmento está associado ao seguinte menu [menu_ajout_rv]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
      <item
        android:id="@+id/navigationToAgenda"
        android:title="@string/navigationToAgenda"/>
    </menu>
  </item>
</menu>
  • linhas 30-33: os elementos da interface visual;
  • linha 36: a lista de clientes;
  • linha 43: a fonte de dados do spinner dos clientes;

O clique no link [Valider] é gerido pelo seguinte método [doValider]:


  // os clientes
  private List<Client> clients;

  // dados locais
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// validação da página
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // recuperar o cliente selecionado
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // início da espera por 2 tarefas assíncronas
    beginWaiting(2);
    // adiciona-se o RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {

        @Override
        public void call(Response<Rv> responseRv) {
          // processa-se a resposta
          consumeRv(responseRv);
        }
      });
  }

  // consumo de um objeto Response<Rv>
  void consumeRv(Response<Rv> responseRv) {
    // erro?
    if (responseRv.getStatus() != 0) {
      // mensagem
      showAlert(responseRv.getMessages());
      // anulação
      doAnnuler();
      // regresso ao UI
      return;
    }
    // observa-se que a marcação foi adicionada
    rdvAjouté = true;
    // o compromisso é guardado
    this.rv = responseRv.getBody();
    // solicita-se a nova agenda
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // processa-se a resposta
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  // consumo de um objeto Response<AgendaMedecinJour>
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // erro?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // mensagem
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancelamento
      doAnnuler();
      // regresso ao UI
      return;
    }
    // coloca-se a agenda na sessão
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • linha 13: quando o método [doValider] é iniciado, os campos 2, 5, 6 e 9 foram inicializados durante o ciclo de vida do fragmento. Veremos como;
  • linha 15: recupera-se a entidade [Client] correspondente ao elemento selecionado no spinner dos clientes;
  • linha 17: indica-se à classe pai que se vão iniciar duas tarefas assíncronas e prepara-se a espera;
  • linha 19: inicialmente, a consulta ainda não foi adicionada à agenda do médico;
  • linhas 20-30: solicita-se ao servidor a adição de um compromisso;
    • linha 20: o método [executeInBackground] espera dois parâmetros:
      • linha 20: o processo a executar e a observar é fornecido pelo método [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
      • linhas 22-29: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
    • linha 27: quando se recebe a resposta, esta é passada para o método [consumeRV] da linha 33;
  • linhas 33-56: recebemos a resposta do servidor. Analisamo-la;
    • linhas 35-42: verifica-se primeiro se o servidor sinalizou um erro no campo [status] da resposta;
    • linha 37: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
    • linha 39: cancelam-se todas as tarefas;
    • linha 41 : regressa-se à interface do utilizador;
    • linha 44: se não tiver ocorrido nenhum erro, regista-se que o compromisso foi adicionado;
    • linha 46: guarda-se a consulta adicionada num campo do fragmento;
    • linhas 47-55: tal como foi feito aquando da eliminação de um compromisso, após a adição do compromisso solicita-se ao servidor a agenda mais recente do médico;
  • linhas 47-56, 59-71: trata-se de um código que já apareceu várias vezes;

O método [beginWaiting] (linha 17) é o seguinte:


  // início da espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepara-se o lançamento das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // estado dos botões e menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • linha 4: indica-se à tarefa principal que se vai iniciar as tarefas [numberOfRunningTasks];
  • linha 6: ocultam-se todas as opções do menu;
  • linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];

O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelam-se as tarefas assíncronas
    cancelRunningTasks();
}
  • linha 7: solicita-se à classe pai que cancele as tarefas assíncronas;

As navegações para trás são asseguradas pelos três métodos seguintes:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navegação para a vista de configuração
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // navega-se para a vista de configuração
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
    // navega-se para a vista da agenda
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. Gestão do ciclo de vida do fragmento

O fragmento apresenta o seguinte estado: [AjoutRvFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

// estado do fragmento AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {

  // posição do cliente selecionado
  private int selectedClientPosition;
  // título da vista
  private String titre;
  // fonte de dados do spinner de clientes
  private String[] spinnerClientsDataSource;

  // getters e setters
...
}

O ciclo de vida do fragmento é implementado da seguinte forma:


// implementação dos métodos da classe pai ----------------------------------
  @Override
  public CoreState saveFragment() {
    // guardar vista
    AjoutRvFragmentState state = new AjoutRvFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
    state.setSpinnerClientsDataSource(spinnerClientsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AJOUT_RV;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // recuperação dos clientes na sessão
    clients = session.getClients();
    // Primeira visita?
    if (previousState == null) {
      // construção da tabela apresentada pelo spinner
      spinnerClientsDataSource = new String[clients.size()];
      int i = 0;
      for (Client client : clients) {
        spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        i++;
      }
    } else {
      // não é a primeira visita
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
    // associação do spinner à sua fonte de dados
    ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
      spinnerClientsDataSource);
    dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerClients.setAdapter(dataAdapterClients);
    // 1.ª visita?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // recupera-se o número do horário a reservar na sessão
    int position = session.getPosition();
    // recupera-se a agenda do médico na sessão
    AgendaMedecinJour agenda = session.getAgenda();
    // recupera-se o médico e o horário em que se vai marcar uma consulta
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // constrói-se o título 2 da página
    String jour = session.getJourRv();
    txtTitre2.setText(String.format(Locale.FRANCE,
      "Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
      medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
      creneau.getMfin()));
    // seleção do cliente
    spinnerClients.setSelection(0);
    // menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restabelecimento do estado anterior
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // título
    txtTitre2.setText(state.getTitre());
    // indicador de carregamento
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // estado do menu
    initMenu();
    // próxima vista?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // houve um cancelamento - compromisso já adicionado?
    if (rdvAjouté) {
      // estamos a alterar a agenda local (não recebemos a agenda global)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // exibir a agenda
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }

  // métodos privados -------------------
  private void initMenu() {
    // estado do menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  • linhas 2-10: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
    • linha 6: o título na parte superior da vista;
    • linha 7: a posição do elemento selecionado no spinner de clientes;
    • linha 8: a fonte de dados do seletor de clientes;
  • linhas 12-15: o número do fragmento é [IMainActivity.VUE_AJOUT_RV];
  • linhas 17-35: executadas quando o fragmento é gerado pela primeira vez (previousState==null) ou regenerado nas vezes seguintes (previousState !=null);
    • linha 20: recupera-se a lista de clientes da sessão para a inserir num campo do fragmento;
    • linhas 22-30: no caso de uma primeira visita, a fonte de dados do spinner dos clientes é criada;
    • linhas 32-33: para as restantes visitas, a fonte de dados do spinner de clientes é recuperada do estado anterior do fragmento;
  • linhas 37-49: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
    • linhas 40-43: em todos os casos, o spinner dos clientes é associado à sua fonte de dados;
    • linhas 45-48: na primeira visita, o menu é apresentado sem a ação [Annuler] (linhas 107-111);
  • linhas 51-70: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Nesse caso, provém-se da vista [AGENDA];
    • linha 54: recupera-se o número do intervalo de tempo no qual se vai inserir um compromisso;
    • linhas 56-59: recuperam-se as entidades [Medecin] e [Creneau] necessárias para a adição deste compromisso e colocam-se nos campos do fragmento;
    • linhas 61-65: com estas informações, é possível construir o título da vista;
    • linha 67: o spinner dos clientes é posicionado no seu primeiro elemento;
    • linha 69: o menu é colocado no seu estado inicial (sem a opção [Annuler]);
  • linhas 72-80: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
    • linha 77: o título da vista é regenerado;
    • linha 79: reposiciona o seletor de clientes no último cliente selecionado;
  • linhas 82-84: executadas quando todas as atualizações anteriores tiverem sido feitas. Aqui não há mais nada a fazer;
  • linhas 86-104: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 89: o menu é reposto no seu estado predefinido;
    • linhas 91-94: se as tarefas tiverem sido concluídas normalmente, regressa-se à vista [AGENDA] através de um [SUBMIT] (aqui, poderia também ter sido uma ação do tipo NAVIGATION);
    • linhas 96-103: se as tarefas tiverem terminado com uma anulação, verifica-se na mesma se o compromisso foi adicionado (o que significaria que foi a obtenção da nova agenda que falhou);
    • linhas 98-99: se o compromisso tiver sido adicionado;
      • linhas 98-99: o compromisso devolvido pelo servidor é adicionado à agenda atual, a que está ativa na sessão;
      • linha 101: regressa-se à vista [AGENDA] através de um [SUBMIT] (neste caso, poderia também ter sido uma ação do tipo NAVIGATION);

3.7. Exécution

Faça os seguintes testes:

  • utilize a aplicação em condições normais e verifique se funciona;
  • gire o dispositivo para cada uma das vistas e verifique se todas são restauradas corretamente;
  • definir um tempo de espera de alguns segundos em [IMainActivity];
  • em seguida, cancele as tarefas e verifique se o resultado obtido é o esperado;
  • gire o dispositivo durante os períodos de espera e verifique se as tarefas são efetivamente canceladas e se não ocorre nenhuma falha;
  • alterar a adjacência dos fragmentos em [IMainActivity] e verificar se a aplicação continua a funcionar;