Skip to content

3. Estudo de Caso - Gestão de Agendamentos

3.1. O Projeto

No documento [Tutorial 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 clientes:

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

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

Note uma diferença importante entre as duas soluções:

  • o que vamos criar só funcionará em tablets Android;
  • na versão [rdvmedecins-angular], o cliente web móvel (HTML/CSS/JS) funciona em qualquer plataforma (Android, iOS, Windows);

3.2. As vistas do cliente Android

Existem quatro vistas.

Vista de configuração

Image

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

Image

Visão de seleção do horário da consulta

Image

Visão de seleção do cliente para a consulta

Image

3.3. Arquitetura do projeto

Utilizaremos uma arquitetura cliente/servidor semelhante à do Exemplo [Exemplo-15] (ver Secção 1.16) deste documento:

Image

A comunicação assíncrona entre o cliente e o servidor será gerida utilizando a biblioteca RxAndroid.

3.4. A base de dados

Não desempenha um papel fundamental neste documento. Apresentamo-la apenas para fins informativos. Iremos chamá-la de [ 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: o número de identificação do médico — a chave primária da tabela
  • VERSION: um número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • LAST_NAME: o apelido do médico
  • FIRST_NAME: o nome próprio do médico
  • TITLE: o seu título (Sra., Sra., Sr.)

3.4.2. A tabela [CLIENTS]

Os clientes dos vários médicos estão armazenados na tabela [CLIENTS]:

  • ID: o número de identificação do cliente — a chave primária da tabela
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • LAST NAME: o apelido do cliente
  • NOME: o nome do cliente
  • TÍTULO: o seu título (Sra., Sra., Sr.)

3.4.3. A tabela [SLOTS]

Apresenta os horários disponíveis para marcações:

  • ID: Número de identificação do intervalo de tempo - 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 cada vez que é feita uma alteração na linha.
  • DOCTOR_ID: número de identificação do médico a quem este intervalo de tempo pertence – chave estrangeira na coluna DOCTORS(ID).
  • START_TIME: hora de início do intervalo de tempo
  • MSTART: Minuto de início do intervalo de tempo
  • HFIN: hora de fim do intervalo
  • MFIN: minutos de fim do intervalo

A segunda linha da tabela [SLOTS] (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]

Apresenta a lista de consultas marcadas para cada médico:

  • ID: identificador único da consulta – chave primária
  • DAY: dia da consulta
  • SLOT_ID: intervalo horário da consulta – chave estrangeira no campo [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
  • CUSTOMER_ID: o ID do cliente para quem a reserva é feita – uma chave estrangeira no campo [ID] da tabela [CUSTOMERS]

Esta tabela tem uma restrição de unicidade nos valores das colunas associadas (DAY, SLOT_ID):

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

Se uma linha na tabela [RV] tiver o valor (DAY1, SLOT_ID1) para as colunas (DAY, SLOT_ID), este valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que foram marcadas duas consultas 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 uma SQLException quando isto ocorre.

A linha com ID igual a 3 (ver [1] acima) significa que foi marcada uma consulta para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [SLOTS] 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 Sra. Brigitte BISTROU.

3.4.5. Gerar a base de dados

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

  

Com o [WampServer] (ver secção 6.15), proceda da seguinte forma:

 
  • Em [1], clique no ícone [WampServer] e selecione a opção [PhpMyAdmin] [2],
  • em [3], na janela que se abre, selecione o link [Bases de dados],
 
  • em [4-6], importe um ficheiro SQL,
  • em [7], selecione o script SQL e, em [8], execute-o,
  • em [9], as tabelas da base de dados foram criadas. Siga uma das ligações,
 
  • em [10], o conteúdo da tabela.

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

3.5. O servidor Web / JSON

Image

Aqui, focamo-nos no servidor [1]. Não o iremos desenvolver mais. Foi detalhado no documento [Spring MVC e Thymeleaf por Exemplo]. Os leitores interessados podem consultá-lo. Foi desenvolvido tal como o servidor do Exemplo 15. O seu código-fonte está incluído nos exemplos. Aqui, iremos utilizar o seu binário:

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

3.5.1. Implementação

Numa janela de comando, navegue até à pasta que contém o 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

Em seguida, 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 inúmeros registos. Incluímos apenas aqueles relevantes para a compreensão do processo acima:

  • linhas 14–18: Um servidor Tomcat incorporado é iniciado na porta 8080 da máquina. Este servidor executa a aplicação web de gestão de consultas. Esta aplicação é, na verdade, um serviço web/JSON: é consultada através de URLs e responde enviando uma cadeia JSON;
  • linha 24: o serviço web é protegido utilizando a estrutura [Spring Security]. As URLs do serviço web são acedidas mediante autenticação;
  • Linhas 29–44: as URLs expostas pelo serviço web;

Iremos abordar estes pontos com mais detalhe.

3.5.2. Proteção do serviço web

As URLs expostas pelo serviço web são protegidas. O servidor espera o seguinte cabeçalho na solicitação HTTP do cliente:

Authorization: Basic code

O código esperado é a codificação Base64 [http://fr.wikipedia.org/wiki/Base64] da cadeia de caracteres «username:password». No seu estado inicial, o serviço web apenas aceita um utilizador chamado «admin» com a palavra-passe «admin». Para este utilizador específico, o cabeçalho acima torna-se a seguinte linha:

Authorization: Basic YWRtaW46YWRtaW4=

Para enviar este cabeçalho HTTP, utilizamos o cliente HTTP [Advanced Rest Client], que é um plugin do navegador Chrome (ver secção 6.13). Iremos testar manualmente os vários URLs expostos pelo serviço web para compreender:

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

3.5.3. Lista de médicos

A URL [/getAllMedecins] recupera a lista de médicos:

  • em [1], a URL que está a ser consultada;
  • em [2], o método HTTP utilizado para este pedido;
  • em [3], o cabeçalho de segurança HTTP do utilizador (admin, admin);
  • em [4], a solicitação HTTP é enviada;

A resposta do servidor é a seguinte:

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

O formato em [5] facilita a visualização da estrutura da resposta. Todas as respostas do serviço web são instâncias da seguinte classe [Response]:


package rdvmedecins.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • linha 9: o estado da resposta. Um valor de 0 significa que não houve erro; caso contrário, ocorreu um erro;
  • linha 11: uma lista de mensagens de erro, caso tenha ocorrido um erro;
  • linha 13: a resposta realmente esperada pelo cliente;

A resposta ao URL [/getAllMedecins] é uma 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 {
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }
 
}

Linha 3: A classe [Doctor] estende a seguinte classe [Person]:


package rdvmedecins.android.dao.entities;
 
public class Personne extends AbstractEntity {
    // attributes of a person
    private String titre;
    private String nom;
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    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 and setters
    ...
}

Linha 3: A classe [Person] 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;
    }
 
    // initialization
    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 and setters
    ...
}

Em última análise, a estrutura de um objeto [Doctor] é a seguinte:


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

e a de [Response<List<Doctor>>] é a seguinte:

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

Daqui em diante, utilizaremos estas definições abreviadas para descrever a resposta do servidor. Além disso, por enquanto, deixaremos de incluir capturas de ecrã. Basta rever o que acabámos de abordar. Voltaremos às capturas de ecrã quando chegar a altura de efetuar um pedido POST. Apresentaremos também um exemplo de execução no seguinte formato:

URL

/getAllDoctors
Resposta
{"status":0,"mensagens":null,"médicos":
[{"id":1,"version":1,"title":"Sra.","lastName":"PELISSIER","firstName":"Marie"},
{"id":2,"version":1,"title":"Sr.","lastName":"BROMARD","firstName":"Jacques"},
{"id":3,"version":1,"title":"Sr.","lastName":"JANDOT","firstName":"Philippe"},
{"id":4,"version":1,"title":"Sra.","lastName":"JACQUEMOT","firstName":"Justine"}]}

3.5.4. Lista de clientes

URL

/getAllClients
Resposta

Resposta<Lista<Cliente>> :[int estado; Lista<String> mensagens;
 Lista<Cliente> clientes]
Client: [Long id; Long version; String title;
 String apelido; String nome;]

Exemplo:

URL

/getAllClients
Resposta
{"status":0,"mensagens":null,"clientes":
[{"id":1,"version":1,"title":"Sr.","lastName":"MARTIN","firstName":"Jules"},
{"id":2,"version":1,"title":"Sra.","lastName":"GERMAN","firstName":"Christine"},
{"id":3,"version":1,"title":"Sr.","lastName":"JACQUARD","firstName":"Jules"},
{"id":4,"version":1,"title":"Sra.","lastName":"BISTROU","firstName":"Brigitte"}]}

3.5.5. Lista de horários disponíveis para consultas médicas

URL
/getAllSlots/{doctorId}
Resposta

Resposta<Lista<Compromisso>>:[int estado ; Lista<String> mensagens ;
 Lista<Compromisso> compromissos]
Slot: [int horaInício; int minutoInício; int horaFim; int minutoFim;]
  • [idMedecin]: ID do médico para o qual pretende os horários de consulta;
  • [startTime]: hora de início da consulta;
  • [start_time]: hora de início da consulta;
  • [hfin]: hora de fim da consulta;
  • [endmin] : minutos de término da consulta;

Para um intervalo de tempo entre as 10:20 e as 10:40, temos [starts, starts, ends, ends] = [10, 20, 10, 40].

Exemplo:

URL
/getAllSlots/1
Resposta
{"status":0,"mensagens":null,"horários":
[{"id":1,"version":1,"startTime":8,"startDate":0,"endTime":8,"endDate":20,"doctorId":1},
{"id":2,"version":1,"startTime":8,"startMin":20,"endTime":8,"endMin":40,"doctorId":1},
{"id":3,"version":1,"startHour":8,"startMinute":40,"endHour":9,"endMinute":0,"doctorId":1},
{"id":4,"version":1,"startHour":9,"startMinute":0,"endHour":9,"endMinute":20,"doctorId":1},
{"id":5,"version":1,"hstart":9,"mstart":20,"hend":9,"mend":40,"doctorId":1},
{"id":6,"version":1,"startHour":9,"startMinute":40,"endHour":10,"endMinute":0,"doctorId":1},
{"id":7,"version":1,"startTime":10,"startDate":0,"endTime":10,"endDate":20,"doctorId":1},
{"id":8,"version":1,"startTime":10,"startMin":20,"endTime":10,"endMin":40,"doctorId":1},
{"id":9,"version":1,"startTime":10,"startDate":40,"endTime":11,"endDate":0,"doctorId":1},
{"id":10,"version":1,"startTime":11,"startDate":0,"endTime":11,"endDate":20,"doctorId":1},
{"id":11,"version":1,"startTime":11,"startDate":20,"endTime":11,"endDate":40,"doctorId":1},
{"id":12,"version":1,"startTime":11,"startDate":40,"endTime":12,"endDate":0,"doctorId":1},
{"id":13,"version":1,"startTime":14,"startDate":0,"endTime":14,"endDate":20,"doctorId":1},
{"id":14,"version":1,"startTime":14,"startDate":20,"endTime":14,"endDate":40,"doctorId":1},
{"id":15,"version":1,"startTime":14,"startDate":40,"endTime":15,"endDate":0,"doctorId":1},
{"id":16,"version":1,"startTime":15,"startDate":0,"endTime":15,"endDate":20,"doctorId":1},
{"id":17,"version":1,"startTime":15,"startDate":20,"endTime":15,"endDate":40,"doctorId":1},
{"id":18,"version":1,"startTime":15,"startDate":40,"endTime":16,"endDate":0,"doctorId":1},
{"id":19,"version":1,"startTime":16,"startDate":0,"endTime":16,"endDate":20,"doctorId":1},
{"id":20,"version":1,"startTime":16,"startDate":20,"endTime":16,"endDate":40,"doctorId":1},
{"id":21,"version":1,"startTime":16,"startDate":40,"endTime":17,"endDate":0,"doctorId":1},
{"id":22,"version":1,"startTime":17,"startDate":0,"endTime":17,"endDate":20,"doctorId":1},
{"id":23,"version":1,"startTime":17,"startDate":20,"endTime":17,"endDate":40,"doctorId":1},
{"id":24,"version":1,"startTime":17,"startDate":40,"endTime":18,"endDate":0,"doctorId":1}]}

3.5.6. Lista de consultas de um médico

URL
/getRvMedecinJour/{idMedecin}/{day}
Resposta

Resposta<List<Rv>>: [int status; List<String> mensagens;
 List<Rv> rvs]
Rv: [Date day; Client client; Slot slot;
 long clientId; long slotId]
  • [idMedic] : identificador do médico cujas consultas são solicitadas;
  • URL [day]: dia das consultas no formato «aaaa-mm-dd»;
  • Resposta [dia]: igual ao anterior, mas na forma de uma data Java;
  • [client]: o cliente para a consulta. A sua estrutura foi descrita anteriormente;
  • [idClient]: o identificador do cliente;
  • [slot]: o horário da consulta. A sua estrutura foi descrita anteriormente;
  • [slotId]: o identificador do horário;

Exemplo:

URL
/getRvMedecinJour/1/2014-07-08
Resposta
{"status":0,"mensagens":null,
"rvs":[{"id":45,"version":0,"date":"2014-07-08","client":
{"id":1,"version":1,"title":"Sr.","lastName":"MARTIN","firstName":"Jules"},"slot":
{"id":1,"version":1,"startTime":8,"startMinute":0,"endTime":8,"endMinute":20,"doctorId":1},
"idDoCliente":1,"idDaConsulta":1}]}

3.5.7. Agenda de um médico

URL
/getDoctorScheduleDay/{doctorId}/{day}
Resposta

Resposta<DoctorScheduleDay>:[int status ; List<String> mensagens ;
 AgendaMédicaDiária agenda]
DailyDoctorSchedule: [Doctor médico; Date dia;
DoctorAppointmentSlot[] doctorAppointmentSlots]
DailyDoctorSlot : [Slot slot ; Appointment appointment]
  • [doctorId]: identificador do médico cujas consultas são pretendidas;
  • URL [dia] : dia das consultas no formato «aaaa-mm-dd» ;
  • [calendar]: calendário do médico;
  • [doctor]: o médico em questão. A sua estrutura foi definida anteriormente;
  • Resposta [dia]: o dia do calendário na forma de uma data Java;
  • [doctorDaySlots]: uma matriz de elementos do tipo [DoctorDaySlot];
  • [slot]: um horário. A sua estrutura foi descrita anteriormente;
  • [appointment]: uma consulta. A sua estrutura foi descrita anteriormente;

Exemplo:

URL
/getDoctorScheduleDay/1/2014-07-08
Resposta

{"status":0,"mensagens":null,"agenda":{"médico":
{"id":1,"version":1,"title":"Sra.","lastName":"PELISSIER","firstName":"Marie"},
"dia":1404770400000,"horáriosDoMédico":[{"horário":
{"id":1,"versão":1,"horaInício":8,"minutoInício":0,"horaFim":8,"minutoFim":20,"idMédico":1},
"appointment":{"id":45,"version":0,"date":"2014-07-08","client":
{"id":1,"version":1,"title":"Sr.","lastName":"MARTIN","firstName":"Jules"},
"slot":{"id":1,"version":1,"start_h":8,"start_m":0,"end_h":8,"end_m":20,"doctor_id":1},
"clientId":1,"slotId":1}},{"slot":
{"id":2,"version":1,"startTime":8,"startMin":20,"endTime":8,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":3,"version":1,"startTime":8,"startMin":40,"endTime":9,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":4,"version":1,"startHour":9,"startMinute":0,"endHour":9,"endMinute":20,"doctorId":1},
"rv":null},{"slot":{"id":5,"version":1,"startHour":9,"startMinute":20,"endHour":9,"endMinute":40,"doctorId":1},
"rv":null},{"slot":{"id":6,"version":1,"startHour":9,"startMinute":40,"endHour":10,"endMinute":0,"doctorId":1},
"rv":null},{"slot":{"id":7,"version":1,"startHour":10,"startMinute":0,"endHour":10,"endMinute":20,"doctorId":1},
"rv":null},{"slot":{"id":8,"version":1,"startTime":10,"startMin":20,"endTime":10,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":9,"version":1,"startTime":10,"startMin":40,"endTime":11,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":10,"version":1,"startTime":11,"startMin":0,"endTime":11,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":11,"version":1,"startTime":11,"startMin":20,"endTime":11,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":12,"version":1,"startTime":11,"startMin":40,"endTime":12,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":13,"version":1,"startTime":14,"startMin":0,"endTime":14,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":14,"version":1,"startTime":14,"startMin":20,"endTime":14,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":15,"version":1,"startTime":14,"startMin":40,"endTime":15,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":16,"version":1,"startTime":15,"startMin":0,"endTime":15,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":17,"version":1,"startTime":15,"startMin":20,"endTime":15,"endMin":40,"doctorId":1},
"rv":null},{"slot":
{"id":18,"version":1,"startTime":15,"startMin":40,"endTime":16,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":19,"version":1,"startTime":16,"startMin":0,"endTime":16,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":20,"version":1,"startTime":16,"startMin":20,"endTime":16,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":21,"version":1,"startTime":16,"startMin":40,"endTime":17,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":22,"version":1,"startTime":17,"startMin":0,"endTime":17,"endMin":20,"doctorId":1},
"rv":null},{"slot":
{"id":23,"version":1,"startTime":17,"startMin":20,"endTime":17,"endMin":40,"doctorId":1},
"rv":null},{"slot":
{"id":24,"version":1,"startTime":17,"startMin":40,"endTime":18,"endMin":0,"doctorId":1},
"rv":null}]}}

Destacámos o caso em que existe uma consulta nesse horário e o caso em que não existe nenhuma.

3.5.8. Procurar um médico pelo seu ID

URL
/getMedecinById/{idMedecin}
Resposta

Resposta<Médico> :[int status ; List<String> mensagens ; Médico médico]
  • [doctorId]: o ID do médico;

Exemplo 1:

URL
/getDoctorById/1
Resposta
{"status":0,"mensagens":null,"médico":
{"id":1,"version":1,"title":"Sra.",
"lastName":"PELISSIER","firstName":"Marie"}}

Exemplo 2:

URL
/getMedecinById/100
Resposta
{"status":2,
"mensagens":["O médico [100] não existe"],"médico":null}

3.5.9. Obter um cliente pelo ID

URL
/getClientById/{idClient}
Resposta

Resposta<Cliente> :[int status ; Lista<String> mensagens ;
 Client client]
  • [idClient]: o ID do cliente;

Exemplo 1:

URL
/getClientById/1
Resposta
{"status":0,"messages":null,"client":{"id":1,"version":1,"title":"Sr.","lastName":"MARTIN","firstName":"Jules"}}

Exemplo 2:

URL
/getClientById/100
Resposta
{"status":2,"mensagens":["O cliente [100] não existe"],"cliente":null}

3.5.10. Marque um horário utilizando o seu ID

URL
/getCreneauById/{idCreneau}
Resposta

Resposta<Creneau> :[int status ; List<String> mensagens ; Creneau creneau]
  • [slotId]: o ID do slot;

Exemplo 1:

URL
/getCreneauById/10
Resposta
{"status":0,"mensagens":null,"slot":
{"id":10,"version":1,"startHour":11,"startMinute":0,
"endTime":11,"endTime":20,"doctorId":1}}

Note que a resposta não inclui o médico responsável pelo horário, apenas o seu ID.

Exemplo 2:

URL
/getCreneauById/100
Resposta
{"status":2,"mensagens":["O slot [100] não existe"],
"slot":null}

3.5.11. Obter uma marcação pelo seu ID

URL
/getRvById/{idRv}
Resposta

Resposta<Rv> :[int status ; List<String> mensagens ; Rv rv]
  • [idRv]: o ID da marcação;

Exemplo 1:

URL
/getRvById/45
Resposta
{"status":0,"mensagens":null,"rv":{"id":45,"versão":0,
"date":"2014-07-08","clientId":1,"slotId":1}}

Note que a resposta não inclui o cliente nem o horário da consulta, mas apenas os seus identificadores.

Exemplo 2:

URL
/getCreneauById/455
Resposta
{"status":2,"messages":["A marcação [455] não existe"],"rv":null}

3.5.12. Adicionar um compromisso

A URL [/addAppointment] permite-lhe adicionar um compromisso. As informações necessárias para esta adição (o dia, o intervalo de tempo e o cliente) são enviadas através de um pedido HTTP POST. Mostramos como efetuar este pedido utilizando a ferramenta [Advanced Rest Client].

Image

  • em [1], a URL que está a ser consultada;
  • em [2], é consultada através de uma solicitação POST;
  • em [3-4], especificamos ao servidor que os valores a serem enviados estão no formato JSON;
  • em [4], o cabeçalho de autenticação HTTP;
  • em [5], a informação enviada através da solicitação POST. Trata-se de uma string JSON contendo:
    • [day]: o dia da marcação no formato «aaaa-mm-dd»,
    • [idClient]: o ID do cliente para quem a consulta está a ser marcada,
    • [idCreneau]: o identificador do intervalo de tempo da consulta. Uma vez que um intervalo de tempo pertence a um médico específico, isto também se refere ao médico;
  • em [6], a solicitação é enviada;

A cadeia JSON que é enviada é a do seguinte objeto [PostAjouterRv]:


public class PostAjouterRv {
 
  // pOST DATA
  private String jour;
  private long idClient;
  private long idCreneau;
 
  // manufacturers
  public PostAjouterRv() {
 
  }
 
  public PostAjouterRv(String jour, long idCreneau, long idClient) {
    this.jour = jour;
    this.idClient = idClient;
    this.idCreneau = idCreneau;
  }
 
  // getters and setters
  ...
}

A resposta do servidor é do tipo [Response<Rv>] [int status; List<String> messages; Rv rv], em que [rv] é o compromisso adicionado.

A resposta do servidor ao pedido acima é a seguinte:

 

Note que algumas informações não estão incluídas [idClient, idCreneau], mas podem ser encontradas nos campos [client] e [creneau]. A informação importante é o ID do compromisso adicionado (209). O serviço web poderia ter simplesmente devolvido esta única informação.

3.5.13. Eliminar um compromisso

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

URL
/deleteAppointment
POST
{'appId':appId}
Resposta

Resposta<RV> :[int status; List<String> mensagens; Rv rv]

O valor enviado é a cadeia JSON de um objeto do tipo [PostSupprimerRv], conforme se segue:


public class PostSupprimerRv {
 
  // pOST DATA
  private long idRv;
 
  // manufacturers
  public PostSupprimerRv() {
 
  }
 
  public PostSupprimerRv(long idRv) {
    this.idRv = idRv;
  }
 
  // getters and setters
  ...
}
  • Linha 4: [idRv] é o ID do compromisso a ser eliminado.

Exemplo 1:

URL
/eliminarCompromisso
POST
{"idRv":209}
Resposta
{"status":0,"mensagens":null,"rv":null}

A marcação n.º 209 foi eliminada com sucesso porque [status=0].

Exemplo 2:

URL
/deleteAppointment
POST
{"appointmentId":650}
Resposta
{"status":2,"messages":["A marcação [650] não existe"],"rv":null}

3.6. O cliente Android

Image

Agora que o servidor [1] foi descrito em detalhe e está em funcionamento, vamos examinar o cliente Android [2].

3.6.1. Arquitetura do projeto Android Studio

O projeto utiliza a arquitetura do projeto [client-android-skel] (ver secção 1.17). Na arquitetura do cliente Android apresentada acima, existem três camadas distintas:

  • a camada [DAO], responsável pela comunicação com o serviço web;
  • a camada [views], responsável pela comunicação com o utilizador;
  • a [Activity], que atua como elo de ligação entre os dois blocos anteriores. As views não têm conhecimento da camada [DAO]. Comunicam apenas com a Activity.

Esta arquitetura está refletida no projeto do Android Studio para o cliente Android:

 
  • o pacote [activity] implementa a atividade;
  • o pacote [architecture] inclui os elementos arquitetónicos que desenvolvemos anteriormente;
  • o pacote [dao] implementa a camada [DAO];
  • o pacote [fragments] implementa as [views];

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 {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 4;
 
  // view n°s
  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 faz pedidos autenticados ao servidor;
  • linha 40: é necessária uma imagem de carregamento;
  • 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] para os 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 {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and 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 {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
 
  // list of doctors
  private List<Medecin> médecins;
  // customer list
  private List<Client> clients;
  // a doctor's diary for a given day
  private AgendaMedecinJour agenda;
  // position of clicked item in diary
  private int position;
  // rv day in English notation "yyyy-MM-dd"
  private String dayRv;
  // rv day in French notation "dd-MM-yyyy"
  private String jourRv;
 
  // getters and setters
...
}
  • Linhas 17–28: A sessão armazena seis informações. Explicaremos as suas funções quando necessário.

3.6.3. A camada [DAO]

  • em [1], as entidades encapsuladas nas respostas do servidor. Estas foram apresentadas na Secção 3.5;
  • em [2], os componentes do cliente que tratam da comunicação com o servidor;

Não iremos revisitar os componentes em [1]. Estes já foram apresentados. O leitor é convidado a consultar a Secção 3.5, se necessário. Iremos examinar a implementação do pacote [service]. Isto também nos levará a discutir a implementação da comunicação segura entre o cliente e o servidor.

3.6.3.1. Implementação da comunicação cliente/servidor

  

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

  • as URLs expostas pelo serviço web;
  • os seus parâmetros;
  • as suas 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);
 
  // list of doctors
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();
 
  // customer list
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();
 
  // list of physician slots
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
 
  // list of doctor's appointments
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
 
  // Customer
  @Get("/getClientById/{id}")
  public Response<Client> getClientById(@Path long id);
 
  // Doctor
  @Get("/getMedecinById/{id}")
  public Response<Medecin> getMedecinById(@Path long id);
 
  // Rv
  @Get("/getRvById/{id}")
  public Response<Rv> getRvById(@Path long id);
 
  // Niche
  @Get("/getCreneauById/{id}")
  public Response<Creneau> getCreneauById(@Path long id);
 
  // add a RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);
 
  // delete an appointment
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
 
  // get a doctor's schedule
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
 
}
  • linhas 19–60: todas as URLs discutidas na secção 3.5 estão presentes;
  • linha 16: o componente [RestTemplate] do [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 {
  // Web service url
  public void setUrlServiceWebJson(String url);
 
  // user
  public void setUser(String user, String mdp);
 
  // customer timeout
  public void setTimeout(int timeout);
 
  // customer list
  public Observable<List<Client>> getAllClients();
 
  // list of doctors
  public Observable<List<Medecin>> getAllMedecins();
 
  // list of physician slots
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
 
  // list of doctor's appointments on a given day
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
 
  // find a customer identified by its id
  public Observable<Client> getClientById(long id);
 
  // find a doctor identified by his id
  public Observable<Medecin> getMedecinById(long id);
 
  // find an Rv identified by its id
  public Observable<Rv> getRvById(long id);
 
  // find a time slot identified by its id
  public Observable<Creneau> getCreneauById(long id);
 
  // add a RV to the list
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
 
  // delete a RV
  public Observable<Rv> supprimerRv(long idRv);
 
  // job
  public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • linha 10: para definir a URL do serviço web / JSON;
  • linha 13: para definir o utilizador para a comunicação cliente/servidor. [user] é o ID do utilizador, [password] é a palavra-passe;
  • linha 16: para definir um tempo limite máximo para a resposta do servidor;
  • linhas 18–49: cada URL exposta pelo serviço web corresponde a um método. Utilizam as mesmas assinaturas de método que o componente AA [WebClient];
  • linha 52: para controlar o modo de depuração 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 {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du 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));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
 
  }
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // implementation of the IDao interface --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // log
    log("getAllClients");
    // result
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }
 
  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // log
    log("getAllMedecins");
    // result
    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) {
    // log
    log("getAllCreneaux");
    // result
    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) {
    // log
    log("getRvMedecinJour");
    // result
    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) {
    // log
    log("getClientById");
    // result
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Medecin>> getMedecinById(final long id) {
    // log
    log("getMedecinById");
    // result
    return getResponse(new IRequest<Response<Medecin>>() {
      @Override
      public Response<Medecin> getResponse() {
        return webClient.getMedecinById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Rv>> getRvById(final long id) {
    // log
    log("getRvById");
    // result
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.getRvById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // log
    log("getCreneauById");
    // result
    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) {
    // log
    log("ajouterRv");
    // result
    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) {
    // log
    log("supprimerRv");
    // result
    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) {
    // log
    log("getAgendaMedecinJour");
    // result
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }
 
}
  • linhas 18–72: estas são as linhas predefinidas na classe [Dao] do projeto [client-android-skel];
  • linhas 74–216: implementação da interface [IDao]. Os métodos que consultam os URLs expostos pelo serviço web delegam esta consulta ao componente AA [WebClient] (linhas 22–23);
  • linhas 58–63: se as trocas cliente/servidor forem autenticadas usando autenticação básica, um interceptor é adicionado ao componente [RestTemplate]. Isto fará com que qualquer pedido HTTP enviado 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 {
 
  // user
  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 [ClientHttpRequestInterceptor] do Spring. Esta interface possui um método, o método [intercept] na linha 22. Estendemos esta interface para interceptar qualquer pedido HTTP do cliente. O método [intercept] recebe três parâmetros;
    • [HttpRequest request]: a solicitação HTTP interceptada,
    • [byte[] body]: o seu corpo, se tiver um (valores enviados, por exemplo),
    • [ClientHttpRequestExecution execution]: o componente Spring que executa a solicitação;

Interceptamos todas as solicitações HTTP do cliente Android para adicionar o cabeçalho de autenticação HTTP apresentado na Secção 3.5.

  • linha 23: recuperamos os cabeçalhos HTTP da solicitação interceptada;
  • linha 24: criamos o cabeçalho de autenticação HTTP. O método de autenticação utilizado (codificação Base64 da string «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: continuamos a executar a solicitação interceptada. Resumindo, a solicitação interceptada foi enriquecida com o cabeçalho de autenticação;

As implementações dos métodos na interface [IDao] seguem todas o mesmo padrão. Tomemos o exemplo do método [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // result
    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 ID do médico cuja agenda se pretende consultar;
    • [day]: o dia para o qual queremos a agenda;
  • linha 6: chamamos o método [getResponse] da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], onde T é o tipo devolvido pelo método [getAgendaMedecinJour] na 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]:

  // get a doctor's schedule
  @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 para o método [getAgendaMedecinJour] na linha 2. Por este motivo, estes parâmetros devem ter o atributo final;

3.6.4. O [MainActivity]

Servidor
  

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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // parent class ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // log
    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] na linha 26;
  • linhas 42-46: o método [getFragments] devolve a matriz dos quatro fragmentos da aplicação;
  • linhas 58-61: a vista de configuração é a primeira vista a ser exibida quando a aplicação é iniciada;

3.6.5. A Sessão

  

A classe [Session] é utilizada para armazenar informações que precisam de 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 {
  // list of doctors
  private List<Medecin> médecins;
  // customer list
  private List<Client> clients;
  // agenda
  private AgendaMedecinJour agenda;
  // position of clicked item in diary
  private int position;
  // rv day in English notation "yyyy-MM-dd"
  private String dayRv;
  // rv day in French notation "dd-MM-yyyy"
  private String jourRv;
 
 
  // getters and setters
...
}
  • linha 10: a classe [Session] é um componente AA instanciado como uma única instância;
  • linhas 12–15: Neste estudo de caso, vamos assumir que as listas de médicos e clientes não se alteram. Iremos recuperá-las quando a aplicação for iniciada e armazená-las na sessão para que os fragmentos possam utilizá-las;
  • linhas 20–23: a data pretendida para uma consulta. É tratada em dois formatos: na notação francesa (linha 23) no cliente Android e na notação inglesa (linha 21) para comunicação com o servidor;
  • linha 19: a posição do elemento clicado (ligação para adicionar/eliminar) no calendário;

3.6.6. Gestão da Visualização de Configuração

3.6.6.1. A vista

A vista de configuração é a vista exibida quando a aplicação é iniciada:

Image

Os elementos da interface visual são os seguintes:

N.º
Tipo
Nome
1
EditText
edtUrlServiceRest
3
EditText
edtUser
5
EditText
edtPassword
2
TextView
txtErrorUrlServiceRest
3
TextView
txtErroDoUtilizador

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 {
 
  // visual interface elements
  @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;
 
  // seizures
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // implementation methods parent class -------------------------------------------
 ...
 
}
  • 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 do formulário;

Ao clicar na opção de menu [Validate], o método [doValidate] é executado:


// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // hide any previous error messages
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // enter the URL of the web service
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // user information
    mainActivity.setUser(utilisateur, mdp);
    // start of wait - 2 asynchronous tasks will be launched
    beginWaiting(2);
    // doctors
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // we consume the answer
        consumeMedecins(responseMedecins);
      }
    });
    // customers
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // we consume the answer
        consumeClients(responseClients);
      }
    });
  }
 
 
  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // mistake?
    if (responseMedecins.getStatus() != 0) {
      // message
      showAlert(responseMedecins.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // doctors are saved in the session
    session.setMédecins(responseMedecins.getBody());
  }
 
  private void consumeClients(Response<List<Client>> responseClients) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // mistake?
    if (responseClients.getStatus() != 0) {
      // message
      showAlert(responseClients.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // customers are stored in the session
    session.setClients(responseClients.getBody());
  }
  • linhas 8–10: verifica-se a validade das três entradas do formulário. Se o formulário for inválido, o processo termina aí;
  • linhas 11–14: as entradas exigidas pela camada [DAO] são passadas para a atividade;
  • linha 16: a classe pai é notificada de que duas tarefas assíncronas serão iniciadas e a espera é preparada;
  • linhas 17–24: a lista de médicos é solicitada;
  • linha 18: o método [executeInBackground] espera dois parâmetros:
    • linha 18: o processo a ser executado e observado é fornecido pelo método [mainActivity.getAllMedecins()];
    • linhas 18–24: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<List<Medecin>>]
  • linha 22: quando a resposta é recebida, é passada para o método [consumeMedecins] na linha 36;
  • linhas 25–33: após iniciar uma primeira tarefa assíncrona, iniciamos uma segunda para solicitar a lista de clientes. Teremos, portanto, duas tarefas a decorrer em paralelo;
  • linhas 36–52: recebemos a resposta da tarefa dos médicos. Processamo-la;
  • linhas 42–49: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
  • linha 44: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
  • linha 46: cancelamos todas as tarefas;
  • linha 48: voltamos à interface do utilizador;
  • linha 51: se não houve erro, a lista de médicos é carregada na sessão;

A validade da entrada (linha 8) é verificada utilizando o seguinte método:


  private boolean isPageValid() {
    // check the validity of the data entered
    boolean erreur;
    URI service;
    // validity of the URL of the REST service
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    try {
      service = new URI(urlServiceRest);
      erreur = service.getHost() == null || service.getPort() == -1;
    } catch (Exception ex) {
      // we note the error
      erreur = true;
    }
    if (erreur) {
      // error display
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // user
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // error is displayed
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // we note the error
      erreur = true;
    }
    // password
    mdp = edtMdp.getText().toString().trim();
    // return
    return !erreur;
}

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


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
  • linha 6: todas as opções do menu são ocultadas;
  • linha 7: em seguida, torna a opção [Ações/Cancelar] visível;

O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • linha 8: solicitamos à 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 {
 
  // visibility of two error messages
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;
 
  // getters and 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:


// implementation methods parent class -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // save fragment status
    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) {
      // 1st visit
      // hide error messages
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore error msg visibility
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // not the 1st visit - error messages are returned
    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();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }
 
  // méthodes privées ------------------------------------------------
  private void initMenu(){
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • linhas 2–9: quando solicitado pela sua classe pai, o fragmento guarda o estado das suas duas mensagens de erro;
  • linhas 11-14: o ID do fragmento é [IMainActivity.VUE_CONFIG];
  • linhas 16–19: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (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 em ocasiões subsequentes (previousState != null);
    • linhas 24–29: na primeira visita, as mensagens de erro são ocultadas e o menu é exibido sem a ação [Cancel] (linhas 62–66);
  • linhas 33–35: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Isto nunca acontece aqui;
  • linhas 37–44: executado quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE]. O estado das mensagens de erro é restaurado 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: redefina o menu para o seu estado padrão;
    • linhas 56–58: se as tarefas foram concluídas com sucesso, passa-se para a próxima vista; 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:

N.º
Tipo
Nome
1
Spinner
spinnerDoctors
2
Seletor de data
edtAppointment

3.6.7.2. O fragmento

O ecrã inicial é gerido pelo seguinte fragmento [HomeFragment]:

 

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 {
 
  // visual interface elements
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;
 
  // local data
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...
 
  // implementation methods parent class -------------------------------------
...
}
  • 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 para o spinner dos médicos;

O clique no link [Validate] é tratado pelo seguinte método [doValidate]:


// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // note the id of the selected doctor
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // the day is saved in the session
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // switch to date format yyyy-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // start wait - 1 asynchronous task will be launched
    beginWaiting(1);
    // we ask for the doctor's diary
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
 
      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // we consume the answer
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }
 
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • linha 5: recuperar o ID do médico selecionado;
  • linhas 7-8: guardamos a data selecionada na sessão no formato francês;
  • linhas 10-11: definimos a data selecionada na sessão, no formato inglês;
  • linha 13: notificamos a classe pai de que estamos prestes a iniciar uma tarefa assíncrona e preparamo-nos para a espera;
  • linhas 15–22: a agenda do médico é recuperada;
    • linha 15: o método [executeInBackground] espera dois parâmetros:
      • linha 15: o processo a ser executado e observado é fornecido pelo método [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
      • linhas 15–22: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<AgendaMedecinJour>]
    • linha 20: quando a resposta é recebida, é passada para o método [consumeAgenda] na linha 25;
  • linhas 25–37: recebemos a agenda do médico. Processamo-la;
  • linhas 27–34: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
  • linha 29: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
  • linha 31: cancelamos todas as tarefas;
  • linha 33: voltamos à interface do utilizador;
  • linha 36: se não houver erros, o calendário é colocado em foco;

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


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
  • linha 6: todas as opções do menu são ocultadas;
  • linha 7: em seguida, torna a opção [Ações/Cancelar] visível;

O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:


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

Clicar na opção de menu [Voltar às definições] é tratado da seguinte forma:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Linha 4: Navegamos para a vista de configuração utilizando a ação [NAVIGATION]. Isto significa que queremos restaurar a vista de configuração para o estado em que a deixámos;

3.6.7.3. Gestão do ciclo de vida do fragmento

O fragmento tem o seguinte [HomeFragmentState]:


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 {
 
  // fragment status [Home]
  // selected doctor's position
  private int selectedMedecinPosition;
  // selected date
  private int year;
  private int month;
  private int dayOfMonth;
  // doctors' spinner data source
  private String[] spinnerMedecinsDataSource;
 
  // manufacturers
  public AccueilFragmentState() {
 
  }
 
  // getters and setters
...
}
  • linha 11: devolve o item selecionado da lista de médicos;
  • linhas 13–15: devolve a data selecionada do calendário;
  • linha 17: recupera a fonte de dados para a lista de médicos;

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


// implementation methods parent class -------------------------------------
  @Override
  public CoreState saveFragment() {
    // save the view
    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) {
    // we get the doctors back in session
    medecins = session.getMédecins();
    // 1st visit?
    if (previousState == null) {
      // we build the table displayed by the 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 {
      // no 1st visit
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // the calendar
    calendrier = Calendar.getInstance();
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // we associate the doctors' spinner with its data source
    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);
    // minimum calendar date to today
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // 1st visit?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // menu
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore the state currently in session
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // selection in doctors' spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // calendar
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // called after all tasks have been completed or cancelled
    // menu status
    initMenu();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }
 
  // méthodes privées ------------------------------------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • linhas 2–9: quando solicitado pela sua classe pai, 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 para o spinner dos médicos;
  • linhas 14-17: o ID do fragmento é [IMainActivity.VUE_ACCUEIL];
  • linhas 19–39: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (previousState != null);
    • linhas 25–31: para uma primeira visita, a fonte de dados para o spinner de médicos é construída;
    • linhas 33–35: para visitas subsequentes, 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 em visitas subsequentes (previousState !=null);
    • linhas 50–53: para a primeira visita, o menu é exibido sem a ação [Cancelar] (linhas 88–92);
    • linhas 43–48: para todas as visitas, sejam elas a primeira ou não, o spinner dos médicos é associado à sua fonte (linhas 44–46) e a data mínima no calendário é definida para a data de hoje (linha 48);
  • linhas 56–60: executadas quando o fragmento é alcançado através de uma operação [SUBMIT]. O utilizador vem da vista [CONFIG]. O menu é reposto no seu estado inicial;
  • linhas 62–70: executadas quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE];
    • linha 67: o spinner de médicos é reiniciado para o último médico selecionado;
    • linha 69: o calendário é definido para a última data selecionada;
  • linhas 72–74: executadas assim que todas as atualizações anteriores tiverem sido concluídas. Não há mais nada a fazer;
  • linhas 76–85: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 80: redefina o menu para o seu estado padrão;
    • linhas 82–84: se as tarefas foram concluídas normalmente, passa-se para a próxima vista; caso contrário, permanece-se na mesma vista;

3.6.8. Gestão da Vista do Calendário

3.6.8.1. A vista

O ecrã inicial tem o seguinte aspeto:

Image

Os elementos da interface visual são os seguintes:

N.º
Tipo
Nome
1
TextView
txtTitle2
2
ListView
slotList

3.6.8.2. O fragmento

A vista Calendário é 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 {
 
  // visual interface elements
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;
 
  // agenda displayed by the fragment
  private AgendaMedecinJour agenda;
  // info ListView slots
  private int firstPosition;
  private int top;
  // appointment deleted or not
  private boolean rdvSupprimé;
  // slot number added or deleted
  private int numCréneau;
 
  // update schedule after adding/deleting
  private void updateAgenda() {
  ...
  }
 
...
 
  // implementation methods parent class ------------------------------------------------------
  ...
}
  • 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: elementos da interface visual;
  • linhas 37-45: dados globais para os métodos;

3.6.8.2.1. Método [updateAgenda]

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


  // update schedule after adding/deleting
  private void updateAgenda() {
    // (re)generation of calendar slots
    // the agenda is taken from the session and stored in a fragment field
    agenda = session.getAgenda();
    // regeneration of ListView slots
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // we reposition ourselves at the right spot on the ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • linha 5: o calendário é recuperado da sessão e armazenado no campo [calendar] do fragmento;
  • linhas 7–9: definimos o adaptador para o componente [ListView]. Este adaptador define tanto a fonte de dados para o [ListView] como o modelo de exibição para cada um dos seus itens. Apresentaremos este adaptador em breve;
  • linha 11: voltamos à posição anterior no calendário. Isto porque apenas vemos uma parte dos intervalos de tempo do dia. Se adicionarmos ou removemos um compromisso no último intervalo, o código acima atualizará a página para exibir o novo calendário. Esta atualização faz com que a vista volte ao primeiro intervalo, o que não é desejável. A linha 5 resolve este problema. Pode encontrar uma descrição desta solução no URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

A classe [ListCreneauxAdapter] é utilizada para definir uma linha no [ListView]:

Image

Conforme mostrado acima, a exibição difere dependendo de o intervalo de tempo ter ou não um compromisso. O código para a classe [ListCreneauxAdapter] é o seguinte:


...
 
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
 
    // time slot table
    private CreneauMedecinJour[] creneauxMedecinJour;
    // execution context
    private Context context;
    // the layout id for displaying a line in the slot list
    private int layoutResourceId;
    // click listener
    private AgendaFragment vue;
 
    // manufacturer
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // memorize information
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // sort the table of slots in schedule order
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ...
}
 
// sorting the slot table
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • Linha 3: A classe [ListCreneauxAdapter] deve estender um adaptador predefinido para [ListView]s, neste caso a classe [ArrayAdapter], que, como o próprio nome sugere, preenche a [ListView] com uma matriz de objetos, neste caso do tipo [CreneauMedecinJour]. Vamos rever o código desta entidade:

public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
...  
}
  • A classe [CreneauMedecinJour] contém um intervalo de tempo (linha 5) e uma consulta potencial (linha 6) ou nulo, caso não haja consulta;

De volta ao código da classe [ListCreneauxAdapter]:

  • linha 15: o construtor recebe quatro parâmetros:
    1. a atividade Android atual,
    2. o ficheiro XML que define o conteúdo de cada elemento [ListView],
    3. a matriz dos horários disponíveis do médico,
    4. a própria vista;
  • Linha 24: A matriz de horários é ordenada por ordem crescente de hora;

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

 
Não.
ID
Tipo
Função
1
txtCreneau
TextView
intervalo de tempo
2
txtClient
TextView
o cliente
3
btnValidate
TextView
link para adicionar/eliminar um compromisso

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


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // we position ourselves in the right niche
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // create the line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the time slot
        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()));
        // the customer
        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);
        // the link
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // add
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // delete
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // link listener
        btnValider.setOnClickListener(new OnClickListener() {
 
            @Override
            public void onClick(View v) {
                // we skip the news on the calendar view
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // we return the line
        return row;
    }
  • linha 2: position é o número da linha a ser gerada no [ListView]. É também o número do intervalo na matriz [creneauxMedecinJour]. Ignoramos os outros dois parâmetros;
  • linha 4: recuperamos o intervalo de tempo a ser exibido na linha do [ListView];
  • linha 6: a linha é construída com base na sua definição XML
 

O código do ficheiro [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 de tempo [1] é construído;
  • linhas 12–20: o ID do cliente [2] é criado;
  • linha 23: se o intervalo de tempo não tiver nenhuma marcação;
  • linhas 25-26: o link azul [Adicionar] é criado;
  • linhas 29-30: caso contrário, o link vermelho [Apagar] é criado;
  • linhas 33-40: independentemente do tipo de link [Adicionar / Eliminar], o método [doValider] da vista irá tratar o clique no link. O método receberá dois argumentos:
    1. o número do intervalo em que se clicou,
    2. o rótulo do link que foi clicado;
  • linha 42: devolvemos a linha que acabámos de criar.

Note que é o método [doValider] do fragmento [AgendaFragment] que trata dos links. É o seguinte:


  // click on a link [Add / Remove]
  public void doValider(int numCréneau, String texte) {
    // operation in progress?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // note the scroll position to return to it
    // read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // we also note the number of the clicked slot
    this.numCréneau = numCréneau;
    // depending on the text of the link, we do not do the same thing
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • O método [doValider] recebe duas informações:
    • o número do slot em que se clicou;
    • o texto (Adicionar / Eliminar) do link em que se clicou;
  • linhas 4–7: clicar nos links [Eliminar / Adicionar] é desativado se houver tarefas assíncronas em curso. Esta é uma escolha de design que simplifica a escrita do código. Está aberta a discussão;
  • linhas 11–15: armazenamos as informações (firstPosition, top) do slot ListView em campos dentro do fragmento para que o método privado [updateAgenda] possa regenerá-lo com a mesma posição de rolagem;
  • linha 17: armazenamos o número do slot clicado;
  • linhas 19–23: dependendo do texto do link clicado, adicionamos ou removemos um item;

3.6.8.2.2. Método [doDelete]

O método [doSupprimer] garante a remoção do compromisso do slot clicado:


// deleting an appointment
  private void doSupprimer() {
    // waiting for two tasks to be completed
    beginWaiting(2);
    // delete the Rdv in the background
    rdvSupprimé = false;
    // rv identifier to be deleted
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // deletion by an asynchronous task
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
 
      @Override
      public void call(Response<Rv> responseRv) {
        // income consumption
        consumeRv(responseRv);
      }
    });
  }
 
  // consumption of an answer
  private void consumeRv(Response<Rv> responseRv) {
    // mistake?
    if (responseRv.getStatus() != 0) {
      // message
      showAlert(responseRv.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // we note that the appointment has been cancelled
    rdvSupprimé = true;
    // the most recent agenda is requested
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {
 
        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // we consume the answer
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }
 
  // diary consumption
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // update the view's agenda
    updateAgenda();
  }
  • linha 4: notificamos a classe pai de que vamos iniciar duas tarefas assíncronas e começamos a aguardar a conclusão dessas duas tarefas;
  • linha 8: recuperamos o ID do compromisso a ser eliminado. O servidor necessita desta informação;
  • linhas 9–18: solicitamos 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 ser executado e observado é fornecido pelo método [mainActivity.deleteRv(idRv)];
      • linhas 10–17: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
    • linha 15: quando a resposta é recebida, é passada para o método [consumeRv] na linha 21;
  • linhas 21–44: recebemos a resposta da tarefa assíncrona. Processamo-la;
  • linhas 23–30: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
    • linha 25: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
    • linha 27: cancelamos todas as tarefas;
    • linha 29: regressamos à interface do utilizador;
  • linha 32: se não houve erro, indicamos que a consulta foi eliminada;
  • linhas 34–43: em vez de simplesmente eliminar o compromisso do calendário atualmente exibido pelo fragmento, solicitamos o novo calendário do médico. Uma vez que a aplicação é multiutilizador, outros utilizadores também podem ter alterado o calendário do médico. Por isso, é melhor utilizar a versão mais recente;
  • linhas 34–43, 47–61: repetimos o que foi feito no fragmento [AccueilFragment], desta vez utilizando informações recuperadas da sessão;

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


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
  • linha 6: todas as opções do menu estão ocultas;
  • linha 7: em seguida, tornamos a opção [Ações/Cancelar] visível;

3.6.8.2.3. Método [doCancel]

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


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

3.6.8.2.4. Opção de menu [Voltar à configuração]

Ao clicar na opção de menu [Voltar à configuração], o seguinte ocorre:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Linha 4: Navegamos para a vista de configuração utilizando a ação [NAVIGATION]. Isto significa que queremos restaurar a vista de configuração para o estado em que a deixámos;

3.6.8.2.5. Opção de menu [Voltar à página inicial]

Clicar na opção de menu [Voltar à página inicial] é tratado de forma semelhante:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
     // navigate to home view
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. Gestão do ciclo de vida do fragmento

O fragmento tem 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 {
 
  // title view
  private String titre;
  // ListView
  private int firstPosition;
  private int top;
 
  // manufacturers
  public AgendaFragmentState() {
 
  }
 
  public AgendaFragmentState(String titre) {
    this.titre = titre;
  }
 
  // getters and setters
...
}
  • linha 10: o título exibido na parte superior da vista;
  • linhas 12-13: permite a rolagem da ListView que exibe os horários disponíveis do médico;

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


// implementation methods parent class ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // save status
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // note the scroll position to return to it
    // read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // we memorize it all
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AGENDA;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // 1st visit?
    if (previousState != null) {
      // not the 1st visit
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // and information from ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }
 
  @Override
  protected void initView(CoreState previousState) {
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // get the agenda
    agenda = session.getAgenda();
    // generate the page title
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // menu status
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // regenerate the page title
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // regenerate the slot list
    updateAgenda();
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu status
    initMenu();
    // if cancelled but appointment deleted, update local calendar
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // we delete the appointment from the local calendar (we were unable to access the global calendar)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // update the visual interface
      updateAgenda();
    }
  }
 
 
  // méthodes privées ------------------------------------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • linhas 2–19: quando solicitado pela sua classe pai, 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 restaurar a rolagem da ListView;
  • linhas 21–24: o ID do fragmento é [IMainActivity.VUE_AGENDA];
  • linhas 26–35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em visitas subsequentes (previousState != null);
    • linhas 30–34: se esta não for a primeira visita ao fragmento, recuperamos as informações (top, firstPosition) necessárias para restaurar o estado de rolagem do ListView;
  • linhas 38–40: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída em visitas subsequentes (previousState != null). Não há nada a fazer aqui, porque a ListView dos slots será gerada pelo método privado [updateAgenda] (linhas 61–65);
  • linhas 42–52: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Estamos a vir da vista [HOME];
    • linha 45: recuperamos a agenda definida por [AccueilFragment];
    • linhas 47–49: o título da vista é gerado;
    • a ListView dos intervalos de tempo será gerada pelo método privado [updateAgenda] (linhas 61-65);
  • linhas 54–59: executadas quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE];
    • linhas 57-58: o título da vista é regenerado;
    • a ListView dos intervalos de tempo será gerada pelo método privado [updateAgenda] (linhas 61–65);
  • linhas 72–74: executadas quando todas as atualizações anteriores tiverem sido concluídas. A ListView dos intervalos de tempo é atualizada porque esta atualização é necessária independentemente da forma como o fragmento é acedido;
  • linhas 67–77: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 70: o menu é reposto no seu estado padrão (linhas 82–86);
    • linha 72: havia duas tarefas assíncronas. Verificamos se a primeira (eliminar o compromisso) foi bem-sucedida, apesar de um cancelamento;
    • linha 74: se sim, o compromisso é eliminado do calendário local
    • linha 75: e atualizamos a exibição do calendário;

3.6.9. Tratamento da vista de adição de compromisso

3.6.9.1. A vista

A tela para adicionar um compromisso é a seguinte:

Image

Os elementos da interface visual são os seguintes:

N.º
Tipo
Nome
1
TextView
txtTitle2
2
Spinner
spinnerClientes

3.6.9.2. O fragmento

A visualização para adicionar 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 {
 
  // visual interface elements
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;
 
  // our customers
  private List<Client> clients;
 
  // local data
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...
 
  // implementation methods parent class ----------------------------------
...
}
  • 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 para o spinner do cliente;

O clique no link [Validate] é tratado pelo seguinte método [doValidate]:


  // our customers
  private List<Client> clients;
 
  // local data
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // the selected customer is retrieved
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // start waiting for 2 asynchronous tasks
    beginWaiting(2);
    // we add the RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {
 
        @Override
        public void call(Response<Rv> responseRv) {
          // we consume the answer
          consumeRv(responseRv);
        }
      });
  }
 
  // consumption of a Response<Rv> object
  void consumeRv(Response<Rv> responseRv) {
    // mistake?
    if (responseRv.getStatus() != 0) {
      // message
      showAlert(responseRv.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // note that the rdv has been added
    rdvAjouté = true;
    // memorize the appointment
    this.rv = responseRv.getBody();
    // we ask for the new agenda
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
 
      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // we consume the answer
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }
 
  // consumption of a Response<AgendaMedecinJour> object
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • linha 13: quando o método [doValider] começa, os campos 2, 5, 6 e 9 já foram inicializados durante o ciclo de vida do fragmento. Vamos ver como;
  • linha 15: recuperamos a entidade [Client] correspondente ao elemento selecionado no spinner do cliente;
  • linha 17: notificamos a classe pai de que vamos lançar duas tarefas assíncronas e nos preparamos para a espera;
  • linha 19: inicialmente, a consulta ainda não foi adicionada ao calendário do médico;
  • linhas 20–30: solicitamos que o servidor adicione uma consulta;
    • linha 20: o método [executeInBackground] espera dois parâmetros:
      • linha 20: o processo a ser executado e observado é fornecido pelo método [mainActivity.addRv(session.getDayRv(), slot.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 a resposta é recebida, é passada para o método [consumeRV] na linha 33;
  • linhas 33–56: recebemos a resposta do servidor. Processamo-la;
    • linhas 35–42: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
    • linha 37: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
    • linha 39: cancelamos todas as tarefas;
    • linha 41 : voltamos à interface do utilizador;
    • linha 44: se não houve erro, indicamos que o compromisso foi adicionado;
    • linha 46: o compromisso adicionado é armazenado num campo do fragmento;
    • linhas 47–55: tal como foi feito ao eliminar um compromisso, após adicionar o compromisso, solicitamos a agenda mais recente do médico ao servidor;
  • linhas 47–56, 59–71: este código já foi encontrado várias vezes anteriormente;

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


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
  • linha 6: todas as opções do menu são ocultadas;
  • linha 7: em seguida, torna a opção [Ações/Cancelar] visível;

O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:


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

A navegação para trás é tratada pelos três métodos seguintes:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }
 
  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }
 
  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
     // navigate to the calendar view
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. Gestão do ciclo de vida do fragmento

O fragmento tem o seguinte estado [AjoutRvFragmentState]:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
// fragment status AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {
 
  // selected customer position
  private int selectedClientPosition;
  // title view
  private String titre;
  // customer spinner data source
  private String[] spinnerClientsDataSource;
 
  // getters and setters
...
}

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


// implementation methods parent class ----------------------------------
  @Override
  public CoreState saveFragment() {
    // save view
    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) {
    // retrieve clients in session
    clients = session.getClients();
    // 1st visit?
    if (previousState == null) {
      // we build the table displayed by the 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 {
      // no 1st visit
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // association spinner to its data source
    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);
    // 1st visit?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // retrieve the number of the slot to be reserved in the session
    int position = session.getPosition();
    // the doctor's agenda is retrieved from the session
    AgendaMedecinJour agenda = session.getAgenda();
    // we get the doctor and the time slot we're going to schedule an appointment for
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // build page title 2
    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()));
    // customer selection
    spinnerClients.setSelection(0);
    // menu
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore previous state
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // title
    txtTitre2.setText(state.getTitre());
    // spinner
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu status
    initMenu();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // there has been a cancellation - appointment already added?
    if (rdvAjouté) {
      // we modify the local agenda (we didn't get the global agenda)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // the agenda is displayed
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }
 
  // private methods -------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
 
  • linhas 2–10: quando solicitado pela sua classe pai, o fragmento guarda o estado dos seguintes elementos:
    • linha 6: o título na parte superior da vista;
    • linha 7: a posição do item selecionado no spinner do cliente;
    • linha 8: a fonte de dados do spinner do cliente;
  • linhas 12–15: o ID do fragmento é [IMainActivity.VUE_AJOUT_RV];
  • linhas 17–35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (previousState != null);
    • linha 20: a lista de clientes é recuperada da sessão e colocada num campo do fragmento;
    • linhas 22–30: para uma primeira visita, a fonte de dados para o spinner do cliente é construída;
    • linhas 32–33: para visitas subsequentes, a fonte de dados para o 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 em ocasiões subsequentes (previousState != null);
    • linhas 40–43: em todos os casos, o spinner do cliente é associado à sua fonte de dados;
    • linhas 45–48: na primeira visita, o menu é apresentado sem a ação [Cancel] (linhas 107–111);
  • linhas 51-70: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Estamos a vir da vista [CALENDAR];
    • linha 54: recuperamos o número do horário em que agendaremos uma consulta;
    • linhas 56–59: recuperamos as entidades [Médico] e [Intervalo de tempo] necessárias para adicionar esta consulta e colocamo-las nos campos dentro do fragmento;
    • linhas 61–65: utilizando esta informação, podemos construir o título da vista;
    • linha 67: o spinner do cliente é definido para o seu primeiro item;
    • linha 69: o menu é definido para o seu estado inicial (sem a opção [Cancelar]);
  • linhas 72-80: executadas quando se chega ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
    • linha 77: o título da vista é regenerado;
    • linha 79: o indicador de carregamento do cliente é reiniciado para o último cliente selecionado;
  • linhas 82–84: executadas quando todas as atualizações anteriores tiverem sido concluídas. Não há mais nada a fazer aqui;
  • linhas 86–104: executadas quando todas as tarefas assíncronas estiverem concluídas;
    • linha 89: o menu é reposto no seu estado padrão;
    • linhas 91–94: se as tarefas foram concluídas normalmente, regressa à vista [CALENDÁRIO] através de um [ENVIAR] (aqui, isto também poderia ter sido uma ação de NAVEGAÇÃO);
    • linhas 96–103: se as tarefas terminaram com um cancelamento, verificamos ainda se o compromisso foi adicionado (isto significaria que a recuperação do novo calendário falhou);
    • linhas 98-99: se o compromisso tiver sido adicionado;
      • linhas 98-99: o compromisso devolvido pelo servidor é adicionado ao calendário atual, aquele que está ativo;
      • linha 101: regressamos à vista [AGENDA] através de um [SUBMIT] (aqui, isto também poderia ter sido uma ação do tipo NAVEGAÇÃO);

3.7. Execução

Realize os seguintes testes:

  • utilize a aplicação em condições normais e verifique se funciona;
  • Gire o dispositivo para cada vista e verifique se cada uma é restaurada corretamente;
  • Adicione uma espera de alguns segundos em [IMainActivity];
  • Em seguida, cancele as tarefas e verifique se o resultado corresponde ao esperado;
  • Gire o dispositivo durante os períodos de espera e verifique se as tarefas são canceladas corretamente e se não ocorrem falhas;
  • Altere a ordem dos fragmentos em [IMainActivity] e verifique se a aplicação continua a funcionar;