3. Caso de estudo - Gestão de marcações
3.1. O projeto
No documento [Tutoriel AngularJS / Spring 4], foi desenvolvida uma aplicação cliente/servidor para gerir consultas médicas. Doravante, referir-nos-emos a este documento como [rdvmedecins-angular]. A aplicação tinha dois tipos de cliente:
- um cliente HTML / CSS / JS;
- um cliente Android;
O cliente Android era obtido automaticamente a partir da versão HTML do cliente, utilizando a ferramenta [Cordova]. O objetivo deste projeto será recriar manualmente este cliente Android, utilizando os conhecimentos adquiridos nos capítulos anteriores.
É de salientar uma diferença importante entre as duas soluções:
- a que vamos criar só poderá ser utilizada em tablets Android;
- na versão [rdvmedecins-angular], o cliente web móvel (HTML / CSS / JS) pode ser utilizado em qualquer plataforma (Android, IoS, Windows);
3.2. As vistas do cliente Android
Existem quatro vistas.
Vista de configuração

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

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

Visão da escolha do cliente para a consulta

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

As trocas assíncronas entre o cliente e o servidor serão geridas com a biblioteca RxAndroid.
3.4. A base de dados
Não desempenha um papel fundamental neste documento. Apresentamo-la a título informativo. Chamar-lhe-emos [dbrdvmedecins] . Trata-se de uma base de dados MySQL5 com quatro tabelas:
![]() |
3.4.1. A tabela [MEDECINS]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: número que identifica o médico — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do médico
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
3.4.2. A tabela [CLIENTS]
Os clientes dos diferentes médicos estão registados na tabela [CLIENTS]:
![]() | ![]() |
- ID: número de identificação do cliente — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do cliente
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
3.4.3. A tabela [CRENEAUX]
Esta tabela lista os intervalos horários em que os RV são possíveis:
![]() |
![]() | ![]() | ![]() |
- ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
- HDEBUT: hora de início do intervalo
- MDEBUT: minutos de início do intervalo
- HFIN: hora de fim do intervalo
- MFIN: minutos de fim do intervalo
A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
3.4.4. A tabela [RV]
Esta tabela lista os RV atribuídos a cada médico:
![]() | ![]() |
- ID: número que identifica o RV de forma única – chave primária
- JOUR: dia do RV
- ID_CRENEAU: intervalo horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
- ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]
Esta tabela possui uma restrição de unicidade na sobre os valores das colunas associadas (JOUR, ID_CRENEAU):
Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) para as colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança um SQLException quando esta situação ocorre.
A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Srta. Brigitte BISTROU.
3.4.5. Criação da base de dados
Para criar as tabelas e preenchê-las, pode utilizar-se o script [dbrdvmedecins.sql], que se encontra no arquivo de exemplos |ICI|.
![]() |
Com o [WampServer] (ver parágrafo 6.15), pode-se proceder da seguinte forma:
![]() | ![]() |
- em [1], clica-se no ícone de [WampServer] e seleciona-se a opção [PhpMyAdmin] [2],
- em [3], na janela que se abriu, selecione o link [Bases de données],
![]() |
- em [4-6], importa-se um ficheiro SQL,
![]() | ![]() | ![]() |
- em [7], seleciona-se o script SQL e, em [8], executa-se o mesmo,
- em [9], as tabelas da base de dados foram criadas. Segue-se uma das ligações,
![]() |
- em [10], o conteúdo da tabela.
Posteriormente, não voltaremos a abordar esta base de dados, mas convidamos o leitor a acompanhar a sua evolução ao longo dos testes, sobretudo quando a aplicação não funcionar.
3.5. O servidor web / jSON

Estamos aqui a centrar-nos no servidor [1]. Não vamos desenvolvê-lo. Este foi detalhado no documento [Spring MVC et Thymeleaf par l'exemple]. O leitor interessado poderá consultar esse documento. Foi desenvolvido à semelhança do servidor do exemplo 15. O seu código-fonte é fornecido nos exemplos. Vamos utilizar aqui o seu ficheiro binário:
![]() |
- [rdvmedecins-server-all-1.0.jar] é o ficheiro binário do servidor;
3.5.1. Implementação
Numa janela de comandos, acedemos à pasta que contém o ficheiro binário do servidor:
...\rdvmedecins>dir
Le volume dans le lecteur D s’appelle Données
Le numéro de série du volume est 7A34-AE5F
Répertoire de D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins
09/06/2016 10:50 <DIR> .
09/06/2016 10:50 <DIR> ..
06/07/2014 16:36 7 631 dbrdvmedecins.sql
08/06/2016 16:31 <DIR> rdvmedecins-client
08/06/2016 16:22 <DIR> rdvmedecins-server
08/06/2016 16:23 29 618 709 rdvmedecins-server-all-1.0.jar
e, para iniciar o servidor, introduza o seguinte comando (o SGBD MySQL já deve estar em execução):
...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.0)
10:55:48.617 [main] INFO rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting service Tomcat
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFOS: Starting Servlet Engine: Apache Tomcat/8.0.33
juin 09, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFOS: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO rdvmedecins.boot.Boot - Started Boot in 8.231 seconds
O servidor apresenta vários registos. Acima, selecionámos apenas aqueles que são úteis para a compreensão:
- linhas 14-18: é iniciado um servidor Tomcat incorporado na porta 8080 da máquina. É este servidor que executa a aplicação web de gestão de marcações. Esta aplicação é, na verdade, um serviço web / jSON: é consultada através de URL e responde enviando uma cadeia jSON;
- linha 24: o serviço web é protegido pelo framework [Spring Security]. O acesso às URL do serviço web é feito mediante autenticação;
- linhas 29-44: os URL expostos pelo serviço web;
Vamos detalhar estas últimas.
3.5.2. Segurança do serviço web
Os URL expostos pelo serviço web estão protegidos. O servidor espera que a solicitação HTTP do cliente inclua o seguinte cabeçalho:
O código esperado é a codificação em base64 [http://fr.wikipedia.org/wiki/Base64] da cadeia «utilizador:palavra-passe». O serviço web, no seu estado inicial, só aceita um utilizador «admin» com a palavra-passe «admin». O cabeçalho acima torna-se, para este utilizador específico, a seguinte linha:
Para podermos enviar este cabeçalho HTTP, utilizamos o cliente HTTP [Advanced Rest Client], que é um plugin do navegador Chrome (ver parágrafo 6.13). Vamos testar manualmente os diferentes URL expostos pelo serviço web para compreender:
- os parâmetros esperados pelo URL;
- a natureza exata da sua resposta;
3.5.3. Lista de médicos
O URL [/getAllMedecins] permite obter a lista de médicos:
![]() |
- em [1], o URL consultado;
- em [2], o método HTTP utilizado para esta consulta;
- em [3], o cabeçalho de segurança do utilizador (admin, admin) HTTP;
- em [4], envia-se o pedido HTTP;
A resposta do servidor é a seguinte:
![]() |
- em [5], a resposta jSON do servidor, formatada;
![]() |
- em [6], a mesma resposta no formato bruto;
O formato [5] permite ver melhor a estrutura da resposta. Todas as respostas do serviço web são uma instância da seguinte classe [Response]:
package rdvmedecins.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de erro
private List<String> messages;
// o corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e setters
...
}
- linha 9: o estado da resposta. O valor 0 significa que não houve erro; caso contrário, houve um erro;
- linha 11: uma lista de mensagens de erro, caso tenha ocorrido algum erro;
- linha 13: a resposta efetivamente esperada pelo cliente;
A resposta ao URL [/getAllMedecins] é a cadeia jSON de um objeto do tipo [Response<List<Medecin>>]. A classe [Medecin] é a seguinte:
package rdvmedecins.android.dao.entities;
public class Medecin extends Personne {
// construtor por predefinição
public Medecin() {
}
// construtor com parâmetros
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
Na linha 3, a classe [Medecin] estende a seguinte classe [Personne]:
package rdvmedecins.android.dao.entities;
public class Personne extends AbstractEntity {
// atributos de uma pessoa
private String titre;
private String nom;
private String prenom;
// construtor por predefinição
public Personne() {
}
// construtor com parâmetros
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters e setters
...
}
Na linha 3, a classe [Personne] estende a seguinte classe [AbstractEntity]:
package rdvmedecins.android.dao.entities;
import java.io.Serializable;
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
protected Long id;
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// inicialização
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters e setters
...
}
Por fim, a estrutura de um objeto [Medecin] é a seguinte:
[Long id; Long version; String titre; String nom; String prenom;]
e a do [Response<List<Medecin>>] é a seguinte:
Posteriormente, utilizaremos estas definições abreviadas para caracterizar a resposta do servidor. Além disso, durante algum tempo, não iremos apresentar mais capturas de ecrã. Basta repetir o que acabámos de ver. Voltaremos às capturas de ecrã quando for necessário efetuar um pedido POST. Apresentaremos também um exemplo de execução com o seguinte formato:
3.5.4. Lista de clientes
| |
|
Exemplo:
3.5.5. Lista de horários de um médico
|
- [idMedecin]: identificador do médico cujos horários de consulta se pretendem consultar;
- [hdebut]: hora de início da consulta;
- [mdebut]: minutos de início da consulta;
- [hfin]: hora de fim da consulta;
- [mfin]: minutos de fim da consulta;
Para um intervalo entre as 10h20 e as 10h40, teremos [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].
Exemplo:
3.5.6. Lista de consultas de um médico
|
- [idMedecin]: identificador do médico cujas consultas se pretendem consultar;
- URL [jour]: dia das consultas no formato «aaaa-mm-dd»;
- Resposta [jour]: o mesmo, mas no formato de uma data Java;
- [client]: o cliente da consulta. A sua estrutura foi descrita anteriormente;
- [idClient]: o identificador do cliente;
- [creneau]: o intervalo de tempo do compromisso. A sua estrutura foi descrita anteriormente;
- [idCreneau]: o identificador do intervalo de tempo;
Exemplo:
3.5.7. A agenda de um médico
|
- [idMedecin]: identificador do médico cujas consultas se pretendem consultar;
- URL [jour]: dia das consultas no formato «aaaa-mm-dd»;
- [agenda]: agenda do médico;
- [medecin]: o médico em questão. A sua estrutura foi definida anteriormente;
- Resposta [jour]: o dia da agenda no formato de uma data Java;
- [creneauxMedecinJour]: um tabuleiro de elementos do tipo [CreneauMedecinJour];
- [creneau]: um intervalo de tempo. A sua estrutura foi descrita anteriormente;
- [rv]: um compromisso. A sua estrutura foi descrita anteriormente;
Exemplo:
|
Foram destacados os casos em que existe uma consulta nesse intervalo horário e os casos em que não existe.
3.5.8. Obter um médico através do seu identificador
|
- [idMedecin]: o identificador do médico;
Exemplo 1:
Exemplo 2:
3.5.9. Obter um cliente através do seu identificador
|
- [idClient]: o identificador do cliente;
Exemplo 1:
Exemplo 2:
3.5.10. Obter um horário através do seu identificador
|
- [idCreneau]: o identificador do horário;
Exemplo 1:
Note-se que, na resposta, não consta o nome do médico titular do horário, mas apenas o seu identificador.
Exemplo 2:
3.5.11. Marcar uma consulta com o seu identificador
|
- [idRv]: o identificador da consulta;
Exemplo 1:
Note-se que, na resposta, não constam nem o cliente nem o horário da marcação, mas apenas os respetivos identificadores.
Exemplo 2:
3.5.12. Adicionar um compromisso
O URL [/ajouterRv] permite adicionar um compromisso. As informações necessárias para esta adição (o dia, o intervalo horário e o cliente) são transmitidas através de uma solicitação HTTP POST. Mostramos como efetuar esta solicitação com a ferramenta [Advanced Rest Client].

- em [1], a URL é consultada;
- em [2], esta é consultada por um POST;
- em [3-4], especifica-se ao servidor que os valores que lhe são enviados são apresentados sob a forma de uma cadeia jSON;
- em [4], o cabeçalho HTTP da autenticação;
- em [5], as informações transmitidas pelo POST. Trata-se de uma cadeia jSON que contém:
- [jour]: o dia da consulta no formato «aaaa-mm-dd»,
- [idClient]: o identificador do cliente para quem a consulta foi marcada,
- [idCreneau]: o identificador do intervalo horário da consulta. Como um intervalo horário pertence a um médico específico, este identificador designa também o médico;
- em [6], envia-se o pedido;
A cadeia jSON que é enviada corresponde ao objeto do tipo [PostAjouterRv] seguinte:
public class PostAjouterRv {
// dados da publicação
private String jour;
private long idClient;
private long idCreneau;
// construtores
public PostAjouterRv() {
}
public PostAjouterRv(String jour, long idCreneau, long idClient) {
this.jour = jour;
this.idClient = idClient;
this.idCreneau = idCreneau;
}
// getters e setters
...
}
A resposta do servidor é do tipo [Response<Rv>] [int status; List<String> messages; Rv rv], em que [rv] é a consulta adicionada.
A resposta do servidor à solicitação acima é a seguinte:
![]() |
Note-se que, no exemplo acima, algumas informações não estão preenchidas em [idClient, idCreneau], mas encontram-se nos campos [client] e [creneau]. A informação importante é o identificador do compromisso adicionado (209). O serviço web poderia ter-se limitado a devolver apenas esta informação.
3.5.13. Eliminar um compromisso
Esta operação também é realizada através de um POST:
|
O valor lançado é a cadeia jSON de um objeto do tipo [PostSupprimerRv], como se segue:
public class PostSupprimerRv {
// dados do post
private long idRv;
// construtores
public PostSupprimerRv() {
}
public PostSupprimerRv(long idRv) {
this.idRv = idRv;
}
// getters e setters
...
}
- na linha 4, [idRv] é o identificador do compromisso a eliminar.
Exemplo 1:
O compromisso n.º 209 foi efetivamente cancelado devido a [status=0].
Exemplo 2:
3.6. O cliente Android

Agora que o servidor [1] foi detalhado e está operacional, vamos analisar o cliente Android [2].
3.6.1. Arquitetura do projeto no Android Studio
O projeto segue a arquitetura do projeto [client-android-skel] (ver parágrafo 1.17). Na arquitetura acima do cliente Android, distinguem-se três blocos:
- a camada [DAO], responsável pela comunicação com o serviço web;
- as camadas [vues], responsáveis pela comunicação com o utilizador;
- a [activité], que faz a ligação entre os dois blocos anteriores. As vistas não têm conhecimento da camada [DAO]. Comunicam apenas com a atividade.
Esta arquitetura reflete-se na arquitetura do projeto Android Studio do cliente Android:
![]() |
- o pacote [activity] implementa a atividade;
- o pacote [architecture] incorpora os elementos de arquitetura que desenvolvemos anteriormente;
- o pacote [dao] implementa a camada [DAO];
- o pacote [fragments] implementa o [vues];
3.6.2. Personalização do projeto
![]() |
A pasta [architecture / custom] contém os elementos personalizáveis da arquitetura.
A interface [IMainActivity] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// acesso à sessão
ISession getSession();
// mudança de vista
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 000;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos da aplicação
int FRAGMENTS_COUNT = 4;
// número de visualizações
int VUE_CONFIG = 0;
int VUE_ACCUEIL = 1;
int VUE_AGENDA = 2;
int VUE_AJOUT_RV = 3;
}
- linhas 25, 28: personalização da camada [DAO];
- linha 31: esta aplicação efetua acessos autenticados ao servidor;
- linha 40: é necessária uma imagem de espera;
- linha 43: a aplicação tem quatro fragmentos;
- linhas 46-49: os números dos quatro fragmentos;
- linha 37: não há separadores;
A classe base [CoreState] dos estados dos fragmentos será a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = AccueilFragmentState.class),
@JsonSubTypes.Type(value = AgendaFragmentState.class),
@JsonSubTypes.Type(value = AjoutRvFragmentState.class),
@JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linhas 15-18: os quatro fragmentos têm um estado:
![]() |
Por fim, a sessão contém os dados partilhados entre fragmentos:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import java.util.List;
public class Session extends AbstractSession {
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
// lista de médicos
private List<Medecin> médecins;
// lista de clientes
private List<Client> clients;
// agenda de um médico para um determinado dia
private AgendaMedecinJour agenda;
// posição do elemento clicado na agenda
private int position;
// dia da consulta no formato inglês «yyyy-MM-dd»
private String dayRv;
// dia da consulta no formato francês «dd-MM-yyyy»
private String jourRv;
// getters e setters
...
}
- linhas 17-28: a sessão armazena seis informações. Explicaremos o papel destas quando for necessário.
3.6.3. A camada [DAO]
![]() |
![]() | ![]() |
- em [1], as entidades encapsuladas nas respostas do servidor. Estas foram apresentadas no parágrafo 3.5;
- em [2], os elementos do cliente que gerem as trocas de dados com o servidor;
Não vamos voltar a abordar os elementos [1]. Estes já foram apresentados. O leitor é convidado a consultar o parágrafo 3.5, se necessário. Vamos estudar a implementação do pacote [service]. Isto levar-nos-á a abordar também a implementação das comunicações seguras entre o cliente e o servidor.
3.6.3.1. Implementação das comunicações cliente/servidor
![]() |
A classe [WebClient] é um componente AA que descreve:
- as URL expostas pelo serviço web;
- os seus parâmetros;
- as respetivas respostas;
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
public void setRestTemplate(RestTemplate restTemplate);
// lista de médicos
@Get("/getAllMedecins")
public Response<List<Medecin>> getAllMedecins();
// lista de clientes
@Get("/getAllClients")
public Response<List<Client>> getAllClients();
// lista de horários de um médico
@Get("/getAllCreneaux/{idMedecin}")
public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
// lista de consultas de um médico
@Get("/getRvMedecinJour/{idMedecin}/{jour}")
public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
// Cliente
@Get("/getClientById/{id}")
public Response<Client> getClientById(@Path long id);
// Médico
@Get("/getMedecinById/{id}")
public Response<Medecin> getMedecinById(@Path long id);
// Consulta
@Get("/getRvById/{id}")
public Response<Rv> getRvById(@Path long id);
// Intervalo
@Get("/getCreneauById/{id}")
public Response<Creneau> getCreneauById(@Path long id);
// adicionar um RV
@Post("/ajouterRv")
public Response<Rv> ajouterRv(@Body PostAjouterRv post);
// eliminar uma consulta
@Post("/supprimerRv")
public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
// obter a agenda de um médico
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
}
- linhas 19-60: encontram-se todas as URL analisadas no parágrafo 3.5;
- linha 16: o componente [RestTemplate] de [Spring Android], no qual se baseia a comunicação cliente/servidor;
3.6.3.2. A interface [IDao]
![]() |
A interface [IDao] da camada [DAO] é a seguinte:
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import rx.Observable;
import java.util.List;
public interface IDao {
// URL do serviço web
public void setUrlServiceWebJson(String url);
// utilizador
public void setUser(String user, String mdp);
// tempo limite do cliente
public void setTimeout(int timeout);
// lista de clientes
public Observable<List<Client>> getAllClients();
// lista de médicos
public Observable<List<Medecin>> getAllMedecins();
// lista de horários disponíveis de um médico
public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
// lista de consultas de um médico, num determinado dia
public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
// encontrar um cliente identificado pelo seu ID
public Observable<Client> getClientById(long id);
// encontrar um médico identificado pelo seu ID
public Observable<Medecin> getMedecinById(long id);
// encontrar uma consulta identificada pelo seu ID
public Observable<Rv> getRvById(long id);
// encontrar um intervalo horário identificado pelo seu ID
public Observable<Creneau> getCreneauById(long id);
// adicionar um RV
public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
// eliminar um RV
public Observable<Rv> supprimerRv(long idRv);
// função
public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
}
- linha 10: para definir o URL do serviço web / jSON;
- linha 13: para definir o utilizador da comunicação cliente/servidor. [user] é o identificador do utilizador, [mdp] a sua palavra-passe;
- linha 16: para definir um tempo máximo de espera pela resposta do servidor;
- linhas 18-49: a cada URL exposta pelo serviço web corresponde um método. Estas reproduzem a assinatura dos métodos com os mesmos nomes do componente AA [WebClient];
- linha 52: para controlar o modo debug da camada [DAO];
3.6.3.3. A classe [Dao]
![]() |
A implementação [DAO] da interface [IDao] anterior é a seguinte:
package client.android.dao.service;
import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// cliente do serviço web
@RestService
protected WebClient webClient;
// segurança
@Bean
protected MyAuthInterceptor authInterceptor;
// o RestTemplate
private RestTemplate restTemplate;
// fábrica do RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
...
}
@Override
public void setUrlServiceWebJson(String url) {
...
}
@Override
public void setUser(String user, String mdp) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// interceptor de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptor de autenticação
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// implementação da interface IDao --------------------------------------------------------------------
@Override
public Observable<Response<List<Client>>> getAllClients() {
// registo
log("getAllClients");
// resultado
return getResponse(new IRequest<Response<List<Client>>>() {
@Override
public Response<List<Client>> getResponse() {
return webClient.getAllClients();
}
});
}
@Override
public Observable<Response<List<Medecin>>> getAllMedecins() {
// registo
log("getAllMedecins");
// resultado
return getResponse(new IRequest<Response<List<Medecin>>>() {
@Override
public Response<List<Medecin>> getResponse() {
return webClient.getAllMedecins();
}
});
}
@Override
public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
// registo
log("getAllCreneaux");
// resultado
return getResponse(new IRequest<Response<List<Creneau>>>() {
@Override
public Response<List<Creneau>> getResponse() {
return webClient.getAllCreneaux(idMedecin);
}
});
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
// registo
log("getRvMedecinJour");
// resultado
return getResponse(new IRequest<Response<List<Rv>>>() {
@Override
public Response<List<Rv>> getResponse() {
return webClient.getRvMedecinJour(idMedecin, jour);
}
});
}
@Override
public Observable<Response<Client>> getClientById(final long id) {
// registo
log("getClientById");
// resultado
return getResponse(new IRequest<Response<Client>>() {
@Override
public Response<Client> getResponse() {
return webClient.getClientById(id);
}
});
}
@Override
public Observable<Response<Medecin>> getMedecinById(final long id) {
// registo
log("getMedecinById");
// resultado
return getResponse(new IRequest<Response<Medecin>>() {
@Override
public Response<Medecin> getResponse() {
return webClient.getMedecinById(id);
}
});
}
@Override
public Observable<Response<Rv>> getRvById(final long id) {
// registo
log("getRvById");
// resultado
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.getRvById(id);
}
});
}
@Override
public Observable<Response<Creneau>> getCreneauById(final long id) {
// registo
log("getCreneauById");
// resultado
return getResponse(new IRequest<Response<Creneau>>() {
@Override
public Response<Creneau> getResponse() {
return webClient.getCreneauById(id);
}
});
}
@Override
public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
// registo
log("ajouterRv");
// resultado
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
}
});
}
@Override
public Observable<Response<Rv>> supprimerRv(final long idRv) {
// registo
log("supprimerRv");
// resultado
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.supprimerRv(new PostSupprimerRv(idRv));
}
});
}
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// registo
log("getAgendaMedecinJour");
// resultado
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
}
- linhas 18-72: são as que se encontram por defeito na classe [Dao] do projeto [client-android-skel];
- linhas 74-216: implementação da interface [IDao]. Os métodos que consultam o URL expostos pelo serviço web delegam essa consulta ao componente AA [WebClient] (linhas 22-23);
- linhas 58-63: se as comunicações cliente/servidor forem autenticadas por uma autorização do tipo básica, é adicionado um interceptor ao componente [RestTemplate]. Isto fará com que qualquer pedido HTTP emitido pelo componente [RestTemplate] seja interceptado pela classe [MyAuthInterceptor] (linhas 25-26);
A classe [MyAuthInterceptor] é a seguinte:
package rdvmedecins.android.dao.security;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// utilizador
private String user;
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
headers.setAuthorization(auth);
return execution.execute(request, body);
}
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
- linha 15: a classe [MyAuthInterceptor] é um componente AA do tipo [singleton];
- linha 16: a classe [MyAuthInterceptor] estende a interface Spring [ClientHttpRequestInterceptor]. Esta interface possui um método, o método [intercept] da linha 22. Esta interface é estendida para interceptar qualquer pedido HTTP do cliente. O método [intercept] recebe três parâmetros;
- [HtpRequest request]: a solicitação HTTP interceptada,
- [byte[] body]: o seu corpo, caso exista (por exemplo, valores enviados via POST),
- [ClientHttpRequestExecution execution]: o componente Spring que executa a solicitação;
Interceptamos todas as solicitações HTTP do cliente Android para lhes adicionar o cabeçalho de autenticação HTTP apresentado no parágrafo 3.5.
- linha 23: recuperamos os cabeçalhos HTTP da solicitação interceptada;
- linha 24: criamos o cabeçalho de autenticação HTTP. O modo de autenticação utilizado (codificação base64 da cadeia «user:mdp») é fornecido pela classe Spring [HttpBasicAuthentication];
- linha 25: o cabeçalho de autenticação que acabámos de criar é adicionado aos cabeçalhos atuais da solicitação interceptada;
- linha 26: prossegue-se com a execução da solicitação interceptada. Resumindo, a solicitação interceptada foi enriquecida com o cabeçalho de autenticação;
As implementações dos métodos da interface [IDao] seguem todas o mesmo modelo. Tomemos como exemplo o método [getAgendaMedecinJour]:
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// registo
log("getAgendaMedecinJour");
// resultado
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
- linha 2: o método espera dois parâmetros:
- [idMedecin]: o identificador do médico cuja agenda se pretende consultar;
- [jour]: o dia para o qual se pretende a agenda;
- linha 6: chama-se o método [getResponse] da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que T é o tipo devolvido pelo método [getAgendaMedecinJour] da linha 2, neste caso [Response<AgendaMedecinJour>]. A interface [IRequest] tem apenas um método: [getResponse] (linha 8);
- linhas 8-10: implementação do método [IRequest.getResponse]. Este método deve devolver o resultado esperado pelo método [getAgendaMedecinJour], na linha 2, do tipo [Response<AgendaMedecinJour>];
- linha 9: a resposta é devolvida pelo método [webClient.getAgendaMedecinJour]:
// consultar a agenda de um médico
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
Os parâmetros utilizados na linha 9 são os que foram passados ao método [getAgendaMedecinJour] na linha 2. Por este motivo, estes parâmetros devem ter o atributo final;
3.6.4. A atividade [MainActivity]
Serveur ![]() |
![]() |
A classe [MainActivity] é a seguinte:
package client.android.activity;
import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.AccueilFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AjoutRvFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;
import java.util.List;
@EActivity
public class MainActivity extends AbstractActivity {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// classe dos pais ---------------------------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return IMainActivity.VUE_CONFIG;
}
// interface IDao -----------------------------------------------------
...
@Override
public Observable<Response<List<Client>>> getAllClients() {
return dao.getAllClients();
}
@Override
public Observable<Response<List<Medecin>>> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
return dao.getAllCreneaux(idMedecin);
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
return dao.getRvMedecinJour(idMedecin, jour);
}
@Override
public Observable<Response<Client>> getClientById(long id) {
return dao.getClientById(id);
}
@Override
public Observable<Response<Medecin>> getMedecinById(long id) {
return dao.getMedecinById(id);
}
@Override
public Observable<Response<Rv>> getRvById(long id) {
return dao.getRvById(id);
}
@Override
public Observable<Response<Creneau>> getCreneauById(long id) {
return dao.getCreneauById(id);
}
@Override
public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
return dao.ajouterRv(jour, idCreneau, idClient);
}
@Override
public Observable<Response<Rv>> supprimerRv(long idRv) {
return dao.supprimerRv(idRv);
}
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
return dao.getAgendaMedecinJour(idMedecin, jour);
}
}
- linhas 21-66: estas linhas são fornecidas por predefinição no modelo [client-android-skel];
- linhas 66-119: implementação da interface [IDao]. Todos os métodos delegam o trabalho à camada [DAO] da linha 26;
- linhas 42-46: o método [getFragments] devolve o array com os quatro fragmentos da aplicação;
- linhas 58-61: a vista de configuração é a primeira vista a ser apresentada quando a aplicação é iniciada;
3.6.5. A sessão
![]() |
A classe [Session] serve para armazenar as informações que devem ser transmitidas entre fragmentos. É a seguinte:
package rdvmedecins.android.architecture;
import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Medecin;
import org.androidannotations.annotations.EBean;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// lista de médicos
private List<Medecin> médecins;
// lista de clientes
private List<Client> clients;
// agenda
private AgendaMedecinJour agenda;
// posição do elemento clicado na agenda
private int position;
// dia da consulta no formato inglês «yyyy-MM-dd»
private String dayRv;
// dia do compromisso no formato francês «dd-MM-yyyy»
private String jourRv;
// getters e setters
...
}
- linha 10: a classe [Session] é um componente AA instanciado numa única instância;
- linhas 12-15: neste estudo de caso, assumiremos que as listas de médicos e de clientes não se alteram. Estas serão solicitadas no arranque da aplicação e armazenadas na sessão para que os fragmentos as possam utilizar;
- linhas 20-23: o dia pretendido para uma consulta. É tratado de duas formas: na notação francesa (linha 23) na aplicação Android, e na notação inglesa (linha 21) para as comunicações com o servidor;
- linha 19: a posição do elemento clicado (ligação «adicionar»/«eliminar») na agenda;
3.6.6. Gestão da vista de configuração
3.6.6.1. A vista
A vista de configuração é a vista apresentada ao iniciar a aplicação:

Os elementos da interface visual são os seguintes:
3.6.6.2. O fragmento
A vista de configuração é gerida pelo seguinte fragmento [ConfigFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.net.URI;
import java.util.List;
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {
// os elementos da interface visual
@ViewById(R.id.edt_urlServiceRest)
protected EditText edtUrlServiceRest;
@ViewById(R.id.txt_errorUrlServiceRest)
protected TextView txtErrorUrlServiceRest;
@ViewById(R.id.txt_errorUtilisateur)
protected TextView txtErrorUtilisateur;
@ViewById(R.id.edt_utilisateur)
protected EditText edtUtilisateur;
@ViewById(R.id.edt_mdp)
protected EditText edtMdp;
// os campos de introdução de dados
private String urlServiceRest;
private String utilisateur;
private String mdp;
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
..
// implementação dos métodos da classe pai -------------------------------------------
...
}
- linha 25: o fragmento está associado ao seguinte menu [menu_config]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
</menu>
- linhas 28-38: os elementos da interface visual;
- linhas 41-43: os três campos de preenchimento do formulário;
O clique na opção de menu [Valider] é gerido pelo método [doValider]:
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
// ocultamos eventuais mensagens de erro anteriores
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
// verifica-se a validade dos dados introduzidos
if (!isPageValid()) {
return;
}
// preenche-se o URL do serviço web
mainActivity.setUrlServiceWebJson(urlServiceRest);
// preenche-se o utilizador
mainActivity.setUser(utilisateur, mdp);
// início da espera — vamos iniciar duas tarefas assíncronas
beginWaiting(2);
// médicos
executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
@Override
public void call(Response<List<Medecin>> responseMedecins) {
// processa-se a resposta
consumeMedecins(responseMedecins);
}
});
// clientes
executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
@Override
public void call(Response<List<Client>> responseClients) {
// a resposta é processada
consumeClients(responseClients);
}
});
}
private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
// registo
if (isDebugEnabled) {
Log.d(className, "consume médecins");
}
// erro?
if (responseMedecins.getStatus() != 0) {
// mensagem
showAlert(responseMedecins.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// os médicos são guardados na sessão
session.setMédecins(responseMedecins.getBody());
}
private void consumeClients(Response<List<Client>> responseClients) {
// registo
if (isDebugEnabled) {
Log.d(className, "consume clients");
}
// erro?
if (responseClients.getStatus() != 0) {
// mensagem
showAlert(responseClients.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// os clientes são memorizados na sessão
session.setClients(responseClients.getBody());
}
- linhas 8-10: verifica-se a validade dos três campos de preenchimento do formulário. Se o formulário for inválido, o processo não prossegue;
- linhas 11-14: os dados necessários para a camada [DAO] são passados para a atividade;
- linha 16: indica-se à classe pai que se vão iniciar duas tarefas assíncronas e prepara-se a espera;
- linhas 17-24: é solicitada a lista de médicos;
- linha 18: o método [executeInBackground] espera dois parâmetros:
- linha 18: o processo a executar e a observar é fornecido pelo método [mainActivity.getAllMedecins()];
- linhas 18-24: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<List<Medecin>>]
- linha 22: quando se recebe a resposta, esta é passada para o método [consumeMedecins] da linha 36;
- linhas 25-33: após ter lançado uma primeira tarefa assíncrona, lança-se uma segunda para solicitar a lista de clientes. Teremos, portanto, duas tarefas a serem executadas em paralelo;
- linhas 36-52: recebemos a resposta da tarefa relativa aos médicos. Analisamos essa resposta;
- linhas 42-49: verificamos primeiro se o servidor sinalizou um erro no campo [status] da resposta;
- linha 44: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 46: cancelam-se todas as tarefas;
- linha 48: regressa-se à interface do utilizador;
- linha 51: se não tiver havido erros, a lista de médicos é carregada na sessão;
A validade dos dados introduzidos (linha 8) é verificada através do seguinte método:
private boolean isPageValid() {
// verifica-se a validade dos dados introduzidos
boolean erreur;
URI service;
// validade do URL do serviço REST
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
try {
service = new URI(urlServiceRest);
erreur = service.getHost() == null || service.getPort() == -1;
} catch (Exception ex) {
// regista-se o erro
erreur = true;
}
if (erreur) {
// exibição do erro
txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
}
// utilizador
utilisateur = edtUtilisateur.getText().toString().trim();
if (utilisateur.length() == 0) {
// é apresentado o erro
txtErrorUtilisateur.setVisibility(View.VISIBLE);
// regista-se o erro
erreur = true;
}
// palavra-passe
mdp = edtMdp.getText().toString().trim();
// voltar
return !erreur;
}
O método [beginWaiting] (linha 16) é o seguinte:
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// prepara-se o lançamento das tarefas
beginRunningTasks(numberOfRunningTasks);
// estado dos botões e menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: indica-se à tarefa principal que se vai iniciar as tarefas [numberOfRunningTasks];
- linha 6: ocultam-se todas as opções do menu;
- linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];
O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelamento das tarefas assíncronas
cancelRunningTasks();
}
- linha 8: solicita-se à classe pai que cancele as tarefas assíncronas;
3.6.6.3. Gestão do ciclo de vida do fragmento
O fragmento apresenta o seguinte estado [ConfigFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// visibilidade das duas mensagens de erro
private boolean txtErrorUrlServiceRestVisible;
private boolean txtErrorUtilisateurVisible;
// getters e setters
...
}
- quando a classe pai o solicitar, o fragmento guardará a visibilidade das suas duas mensagens de erro;
O ciclo de vida do fragmento é implementado da seguinte forma:
// Implementação dos métodos da classe pai -------------------------------------------
@Override
public CoreState saveFragment() {
// guardar o estado do fragmento
ConfigFragmentState state = new ConfigFragmentState();
state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_CONFIG;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
if (previousState == null) {
// 1.ª visita
// ocultamos as mensagens de erro
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restabelecimento da visibilidade das mensagens de erro
ConfigFragmentState state = (ConfigFragmentState) previousState;
// não é a primeira visita - restabelecer as mensagens de erro
txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// próxima vista?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
}
}
// métodos privados ------------------------------------------------
private void initMenu(){
// estado do menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2-9: quando a sua classe pai o solicitar, o fragmento guarda o estado das suas duas mensagens de erro;
- linhas 11-14: o número do fragmento é [IMainActivity.VUE_CONFIG];
- linhas 16-19: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado nas vezes seguintes (previousState != null). Aqui, não há nada a fazer;
- linhas 21-31: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
- linhas 24-29: na primeira visita, ocultam-se as mensagens de erro e apresenta-se o menu sem a ação [Annuler] (linhas 62-66);
- linhas 33-35: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Isso nunca acontece aqui;
- linhas 37-44: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE]. Recria-se o estado das mensagens de erro a partir do estado anterior;
- linhas 47-49: executadas quando todas as atualizações anteriores tiverem sido feitas. Não há mais nada a fazer;
- linhas 51-59: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linhas 53-54: o menu é reposto no seu estado predefinido;
- linhas 56-58: se as tarefas tiverem sido concluídas normalmente, passa-se para a vista seguinte; caso contrário, permanece-se na mesma vista;
3.6.7. Gestão da vista inicial
3.6.7.1. A vista
A vista inicial é a seguinte:

Os elementos da interface visual são os seguintes:
3.6.7.2. O fragmento
A página inicial é gerida pelo seguinte fragmento [AccueilFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.spinnerMedecins)
protected Spinner spinnerMedecins;
@ViewById(R.id.edt_JourRv)
protected DatePicker edtJourRv;
// dados locais
private List<Medecin> medecins;
private Calendar calendrier;
private String[] spinnerMedecinsDataSource;
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementação de métodos da classe pai -------------------------------------
...
}
- linha 26: o fragmento está associado ao seguinte menu [menu_accueil]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
</menu>
</item>
</menu>
- linhas 31-34: os elementos da interface visual;
- linha 37: a lista de médicos;
- linha 38: um calendário;
- linha 39: a fonte de dados do spinner dos médicos;
O clique no link [Valider] é gerido pelo seguinte método [doValider]:
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
// regista-se o ID do médico selecionado
Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
// o dia é guardado na sessão
String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
session.setJourRv(jourRv);
// converte-se para o formato de data aaaa-MM-dd
String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
session.setDayRv(dayRv);
// início da espera — vai ser iniciada uma tarefa assíncrona
beginWaiting(1);
// solicita-se a agenda do médico
executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// processa-se a resposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// erro?
if (responseAgendaMedecinJour.getStatus() != 0) {
// mensagem
showAlert(responseAgendaMedecinJour.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// coloca-se a agenda na sessão
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- linha 5: recupera-se o identificador do médico selecionado;
- linhas 7-8: insere-se, no formato francês, a data escolhida;
- linhas 10-11: insere-se, no formato inglês, a data escolhida;
- linha 13: indica-se à classe pai que se vai iniciar uma tarefa assíncrona e prepara-se a espera;
- linhas 15-22: solicita-se a agenda do médico;
- linha 15: o método [executeInBackground] espera dois parâmetros:
- linha 15: o processo a executar e a observar é fornecido pelo método [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
- linhas 15-22: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<AgendaMedecinJour>]
- linha 20: quando se recebe a resposta, esta é passada para o método [consumeAgenda] da linha 25;
- linha 15: o método [executeInBackground] espera dois parâmetros:
- linhas 25-37: recebemos a agenda do médico. Analisamo-la;
- linhas 27-34: verifica-se, em primeiro lugar, se o servidor sinalizou um erro no campo [status] da resposta;
- linha 29: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 31: cancelam-se todas as tarefas;
- linha 33: regressa-se à interface do utilizador;
- linha 36: se não tiver havido erros, a agenda é ativada;
O método [beginWaiting] (linha 13) é o seguinte:
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// prepara-se o lançamento das tarefas
beginRunningTasks(numberOfRunningTasks);
// estado dos botões e menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: indica-se à tarefa pai que se vai iniciar as tarefas [numberOfRunningTasks];
- linha 6: ocultam-se todas as opções do menu;
- linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];
O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelam-se as tarefas assíncronas
cancelRunningTasks();
}
- linha 8: solicita-se à classe pai que cancele as tarefas assíncronas;
O clique na opção de menu [Retour à la configuration] é tratado da seguinte forma:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navegação para a vista de configuração
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- linha 4: navega-se para a vista de configuração com a ação [NAVIGATION]. Isto significa que se pretende recuperar a vista de configuração no estado em que foi deixada;
3.6.7.3. Gestão do ciclo de vida do fragmento
O fragmento apresenta o seguinte estado [AccueilFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
public class AccueilFragmentState extends CoreState {
// estado do fragmento [Accueil]
// posição do médico selecionado
private int selectedMedecinPosition;
// data selecionada
private int year;
private int month;
private int dayOfMonth;
// fonte de dados do seletor de médicos
private String[] spinnerMedecinsDataSource;
// construtores
public AccueilFragmentState() {
}
// getters e setters
...
}
- linha 11: permite recuperar o elemento selecionado na lista de médicos;
- linhas 13-15: permite recuperar a data selecionada no calendário;
- linha 17: permite recuperar a fonte de dados da lista de médicos;
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementação dos métodos da classe pai -------------------------------------
@Override
public CoreState saveFragment() {
// guardamos a vista
AccueilFragmentState state = new AccueilFragmentState();
state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
state.setDayOfMonth(edtJourRv.getDayOfMonth());
state.setMonth(edtJourRv.getMonth());
state.setYear(edtJourRv.getYear());
state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_ACCUEIL;
}
@Override
protected void initFragment(CoreState previousState) {
// recuperar os médicos da sessão
medecins = session.getMédecins();
// 1.ª consulta?
if (previousState == null) {
// construção da tabela apresentada pelo spinner
spinnerMedecinsDataSource = new String[medecins.size()];
int i = 0;
for (Medecin medecin : medecins) {
spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
i++;
}
} else {
// não é a primeira consulta
AccueilFragmentState state = (AccueilFragmentState) previousState;
spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
}
// o calendário
calendrier = Calendar.getInstance();
}
@Override
protected void initView(CoreState previousState) {
// associamos o spinner dos médicos à sua fonte de dados
ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerMedecins.setAdapter(dataAdapterMedecins);
// data mínima do calendário até hoje
edtJourRv.setMinDate(calendrier.getTimeInMillis());
// 1.ª consulta?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restaura-se o estado da sessão atual
AccueilFragmentState state = (AccueilFragmentState) previousState;
// seleção de médicos no spinner
spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
// calendário
edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// chamada depois de todas as tarefas estarem concluídas ou canceladas
// estado do menu
initMenu();
// próxima vista?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
}
}
// métodos privados ------------------------------------------------
private void initMenu() {
// estado do menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2-9: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
- linha 6: a posição selecionada na lista de médicos;
- linhas 7-9: o dia do mês, o mês e o ano da data selecionada no calendário;
- linha 10: a fonte de dados do spinner dos médicos;
- linhas 14-17: o n.º do fragmento é [IMainActivity.VUE_ACCUEIL];
- linhas 19-39: executadas quando o fragmento é gerado pela primeira vez (previousState==null) ou regenerado nas vezes seguintes (previousState !=null);
- linhas 25-31: no caso de uma primeira visita, a fonte de dados do spinner dos médicos é criada;
- linhas 33-35: para as restantes visitas, a fonte de dados do spinner é recuperada do estado anterior do fragmento;
- linhas 41-54: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
- linhas 50-53: na primeira visita, é apresentado o menu sem a ação [Annuler] (linhas 88-92);
- linhas 43-48: para todas as visitas, seja a primeira ou não, associa-se o spinner dos médicos à sua fonte (linhas 44-46) e define-se a data mínima do calendário como a data de hoje (linha 48);
- linhas 56-60: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Nesse caso, provém-se da vista [CONFIG]. Coloca-se o menu no seu estado inicial;
- linhas 62-70: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
- linha 67: reposiciona o spinner dos médicos no último médico selecionado;
- linha 69: posiciona-se o calendário na última data escolhida;
- linhas 72-74: executadas quando todas as atualizações anteriores tiverem sido feitas. Não há mais nada a fazer;
- linhas 76-85: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 80: o menu é reposto no seu estado predefinido;
- linhas 82-84: se as tarefas tiverem sido concluídas normalmente, passa-se para a vista seguinte; caso contrário, permanece-se na mesma vista;
3.6.8. Gestão da vista Agenda
3.6.8.1. A vista
A vista inicial é a seguinte:

Os elementos da interface visual são os seguintes:
3.6.8.2. O fragmento
A vista Agenda é gerida pelo seguinte fragmento [AgendaFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.txt_titre2_agenda)
protected TextView txtTitre2;
@ViewById(R.id.listViewAgenda)
protected ListView lstCreneaux;
// agenda apresentada pelo fragmento
private AgendaMedecinJour agenda;
// informações ListView sobre os horários
private int firstPosition;
private int top;
// compromisso eliminado ou não
private boolean rdvSupprimé;
// n.º do intervalo de tempo adicionado ou eliminado
private int numCréneau;
// atualização da agenda após uma adição/eliminação
private void updateAgenda() {
...
}
...
// implementação de métodos da classe pai ------------------------------------------------------
...
}
- linha 27: o fragmento está associado ao seguinte menu [menu_agenda]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionAgenda"
android:title="@string/actionAgenda"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
</menu>
</item>
</menu>
- linhas 32-35: os elementos da interface visual;
- linhas 37-45: dados globais dos métodos;
3.6.8.2.1. Método [updateAgenda]
A (re)geração da lista de intervalos da agenda é necessária em vários pontos do código. Foi factorizada no seguinte método privado [updateAgenda]:
// atualização da agenda após uma adição/eliminação
private void updateAgenda() {
// (re)geração dos intervalos da agenda
// a agenda é capturada na sessão e armazenada num campo do fragmento
agenda = session.getAgenda();
// regeneração do ListView dos intervalos
ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour(), this);
lstCreneaux.setAdapter(adapter);
// reposiciona-se no local correto do ListView
lstCreneaux.setSelectionFromTop(firstPosition, top);
}
- linha 5: a agenda é obtida da sessão e armazenada no campo [agenda] do fragmento;
- linhas 7-9: define-se o adaptador do componente [ListView]. Este adaptador define tanto a fonte de dados do [ListView] como o modelo de visualização de cada elemento da mesma. Apresentaremos este adaptador em breve;
- linha 11: regressamos à posição anterior da agenda. Com efeito, apenas se vê uma parte dos intervalos do dia. Se adicionarmos ou eliminarmos um compromisso no último intervalo, o código acima irá atualizar a página para apresentar a nova agenda. Esta atualização faz com que fiquemos novamente posicionados no primeiro intervalo, o que não é desejável. A linha 5 resolve este problema. A descrição desta solução pode ser encontrada no URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];
A classe [ListCreneauxAdapter] serve para definir uma linha do [ListView]:

Como se pode ver acima, dependendo de o intervalo de tempo ter ou não um compromisso, a apresentação não é a mesma. O código da classe [ListCreneauxAdapter] é o seguinte:
...
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
// a tabela de intervalos horários
private CreneauMedecinJour[] creneauxMedecinJour;
// o contexto de execução
private Context context;
// o ID do layout de visualização de uma linha da lista de intervalos
private int layoutResourceId;
// ouvinte de cliques
private AgendaFragment vue;
// construtor
public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaFragment vue) {
super(context, layoutResourceId, creneauxMedecinJour);
// guardamos as informações
this.creneauxMedecinJour = creneauxMedecinJour;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.vue = vue;
// ordena-se a tabela de intervalos por ordem horária
Arrays.sort(creneauxMedecinJour, new MyComparator());
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
// ordenação da tabela de intervalos
class MyComparator implements Comparator<CreneauMedecinJour> {
...
}
}
- linha 3: a classe [ListCreneauxAdapter] deve estender um adaptador predefinido para as classes [ListView], neste caso, a classe [ArrayAdapter] que, tal como o próprio nome indica, alimenta a [ListView] com um tabuleiro de objetos, neste caso do tipo [CreneauMedecinJour]. Recorde-se o código desta entidade:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Creneau creneau;
private Rv rv;
...
}
- a classe [CreneauMedecinJour] contém um intervalo horário (linha 5) e um eventual compromisso (linha 6) ou null, caso não haja compromisso;
Voltando ao código da classe [ListCreneauxAdapter]:
- linha 15: o construtor recebe quatro parâmetros:
- a atividade Android em execução,
- o ficheiro XML que define o conteúdo de cada elemento do [ListView],
- a tabela de horários do médico,
- a própria vista;
- linha 24: a tabela dos horários está ordenada por ordem crescente dos horários;
O método [getView] é responsável por gerar a vista correspondente a uma linha do [ListView]. Esta inclui três elementos:
O código do método [getView] é o seguinte:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// seleciona-se o intervalo correto
CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
// cria-se a linha
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// o intervalo horário
TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
.getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
// o cliente
TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
String text;
if (creneauMedecin.getRv() != null) {
Client client = creneauMedecin.getRv().getClient();
text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
} else {
text = "";
}
txtClient.setText(text);
// o link
final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
if (creneauMedecin.getRv() == null) {
// adicionar
btnValider.setText(R.string.btn_ajouter);
btnValider.setTextColor(context.getResources().getColor(R.color.blue));
} else {
// eliminar
btnValider.setText(R.string.btn_supprimer);
btnValider.setTextColor(context.getResources().getColor(R.color.red));
}
// ouvinte do link
btnValider.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// passamos as informações para a vista da agenda
vue.doValider(position, btnValider.getText().toString());
}
});
// formata a linha
return row;
}
- linha 2: «position» é o número da linha que vai ser gerada no [ListView]. É também o número do intervalo horário na tabela [creneauxMedecinJour]. Ignora-se os outros dois parâmetros;
- linha 4: recupera-se o intervalo horário a apresentar na linha do [ListView];
- linha 6: a linha é construída a partir da sua definição XML
![]() |
O código de [creneau_medecin.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Creneau"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/txt_Client"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Creneau"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Creneau"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/btn_Valider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Client"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Client"
android:text="@string/btn_valider"
android:textColor="@color/blue" />
</RelativeLayout>
- linhas 8-10: o intervalo horário [1] é construído;
- linhas 12-20: a identidade do cliente [2] é criada;
- linha 23: se o intervalo de tempo não tiver qualquer compromisso;
- linhas 25-26: cria-se o link [Ajouter] na cor azul;
- linhas 29-30: caso contrário, cria-se o link [Supprimer] na cor vermelha;
- linhas 33-40: independentemente da natureza do link [Ajouter / Supprimer], é o método [doValider] da vista que irá gerir o clique no link. O método receberá dois argumentos:
- o número do intervalo em que se clicou,
- o texto do link em que se clicou;
- linha 42: devolve-se a linha que acabou de ser construída.
Note-se que é o método [doValider] do fragmento [AgendaFragment] que gere os links. Este é o seguinte:
// clique num link [Ajouter / Supprimer]
public void doValider(int numCréneau, String texte) {
// operação em curso?
if (numberOfRunningTasks != 0) {
Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
return;
}
// anotamos a posição da barra de deslocamento para voltar a ela
// ler [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// posição do primeiro elemento, visível na totalidade ou não
firstPosition = lstCreneaux.getFirstVisiblePosition();
// desvio no eixo Y deste elemento em relação à parte superior do ListView
// mede a altura da parte eventualmente oculta
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// regista-se também o número do intervalo em que se clicou
this.numCréneau = numCréneau;
// dependendo do texto do link, não se faz a mesma coisa
if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
doAjouter();
} else {
doSupprimer();
}
}
- o método [doValider] recebe duas informações:
- o número do intervalo em que se clicou;
- o texto («Adicionar» / «Eliminar») do link em que se clicou;
- linhas 4-7: o clique nos links [Supprimer / Ajouter] é inibido se houver tarefas assíncronas em curso. Trata-se de uma escolha que facilita a escrita do código. Pode ser discutida;
- linhas 11-15: registam-se as informações (firstPosition, top) do ListView dos intervalos nos campos do fragmento, para que o método privado [updateAgenda] possa regenerá-lo com a mesma posição de deslocamento;
- linha 17: regista-se o n.º do intervalo em que se clicou;
- linhas 19-23: consoante o texto do link clicado, efetua-se uma adição ou uma eliminação;
3.6.8.2.2. Método [doSupprimer]
O método [doSupprimer] assegura a eliminação do compromisso do intervalo em que se clicou:
// eliminação de um compromisso
private void doSupprimer() {
// aguarda a conclusão de duas tarefas
beginWaiting(2);
// o compromisso é eliminado em segundo plano
rdvSupprimé = false;
// identificador da consulta a eliminar
long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
// eliminação por uma tarefa assíncrona
executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// utilização do resultado
consumeRv(responseRv);
}
});
}
// utilização de uma resposta
private void consumeRv(Response<Rv> responseRv) {
// erro?
if (responseRv.getStatus() != 0) {
// mensagem
showAlert(responseRv.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// observa-se que a marcação foi eliminada
rdvSupprimé = true;
// solicita-se a agenda mais recente
executeInBackground(
mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// processa-se a resposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// consumo de uma agenda
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// erro?
if (responseAgendaMedecinJour.getStatus() != 0) {
// mensagem
showAlert(responseAgendaMedecinJour.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// coloca-se a agenda na sessão
session.setAgenda(responseAgendaMedecinJour.getBody());
// atualiza-se a agenda da vista
updateAgenda();
}
- linha 4: notifica-se a classe pai de que se vão iniciar duas tarefas assíncronas e inicia-se a espera pelo término dessas duas tarefas;
- linha 8: recupera-se o identificador do compromisso a eliminar. Com efeito, o servidor necessita desta informação;
- linhas 9-18: solicita-se a eliminação do compromisso através de uma tarefa assíncrona;
- linha 10: o método [executeInBackground] espera dois parâmetros:
- linha 10: o processo a executar e a observar é fornecido pelo método [mainActivity.supprimerRv(idRv)];
- linhas 10-17: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
- linha 15: quando se recebe a resposta, esta é passada para o método [consumeRv] da linha 21;
- linha 10: o método [executeInBackground] espera dois parâmetros:
- linhas 21-44: recebemos a resposta da tarefa assíncrona. Processamos essa resposta;
- linhas 23-30: verifica-se primeiro se o servidor sinalizou um erro no campo [status] da resposta;
- linha 25: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 27: cancelam-se todas as tarefas;
- linha 29: regressa-se à interface do utilizador;
- linha 32: se não tiver ocorrido nenhum erro, regista-se que a consulta foi eliminada;
- linhas 34-43: em vez de simplesmente eliminar a consulta da agenda atualmente apresentada pelo fragmento, solicita-se a nova agenda do médico. Com efeito, a aplicação é multiutilizador e outros utilizadores também podem ter alterado a agenda do médico. Por isso, é melhor ter a versão mais recente;
- linhas 34-43, 47-61: repete-se o que tinha sido feito no fragmento [AccueilFragment], desta vez com informações obtidas da sessão;
O método [beginWaiting] (linha 4) é o seguinte:
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// prepara-se o lançamento das tarefas
beginRunningTasks(numberOfRunningTasks);
// estado dos botões e menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: indica-se à tarefa pai que se vai iniciar as tarefas [numberOfRunningTasks];
- linha 6: ocultam-se todas as opções do menu;
- linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];
3.6.8.2.3. Método [doAnnuler]
O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelam-se as tarefas assíncronas
cancelRunningTasks();
}
- linha 7: solicita-se à classe pai que cancele as tarefas assíncronas;
3.6.8.2.4. Opção de menu [Retour à la configuration]
Ao clicar na opção de menu [Retour à la configuration], o processo é gerido da seguinte forma:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navegação para a vista de configuração
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- linha 4: acede-se à vista de configuração com a ação [NAVIGATION]. Isto significa que se pretende recuperar a vista de configuração no estado em que foi deixada;
3.6.8.2.5. Opção de menu [Retour à l'accueil]
O clique na opção de menu [Retour à l'accueil] é tratado de forma semelhante:
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// navega-se para a vista inicial
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
3.6.8.3. Gestão do ciclo de vida do fragmento
O fragmento apresenta o seguinte estado: [AgendaFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
public class AgendaFragmentState extends CoreState {
// título da vista
private String titre;
// ListView
private int firstPosition;
private int top;
// construtores
public AgendaFragmentState() {
}
public AgendaFragmentState(String titre) {
this.titre = titre;
}
// getters e setters
...
}
- linha 10: o título exibido na parte superior da vista;
- linhas 12-13: permitem apresentar o scrolling do ListView dos horários do médico;
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementação de métodos da classe pai ------------------------------------------------------
@Override
public CoreState saveFragment() {
// guardar estado
AgendaFragmentState state = new AgendaFragmentState();
state.setTitre(txtTitre2.getText().toString());
// registar a posição do scroll para voltar a ela
// ler [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// posição do primeiro elemento visível na totalidade ou parcialmente
firstPosition = lstCreneaux.getFirstVisiblePosition();
// desvio no eixo Y deste elemento em relação à parte superior do ListView
// mede a altura da parte eventualmente oculta
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// guardamos tudo isto
state.setTop(top);
state.setFirstPosition(firstPosition);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AGENDA;
}
@Override
protected void initFragment(CoreState previousState) {
// 1.ª visita?
if (previousState != null) {
// não é a primeira visita
AgendaFragmentState state = (AgendaFragmentState) previousState;
// e as informações do ListView
firstPosition = state.getFirstPosition();
top = state.getTop();
}
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// recuperamos a agenda
agenda = session.getAgenda();
// geramos o título da página
Medecin medecin = agenda.getMedecin();
txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
medecin.getNom(), session.getJourRv()));
// estado do menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// regenera-se o título da página
AgendaFragmentState state = (AgendaFragmentState) previousState;
txtTitre2.setText(state.getTitre());
}
@Override
protected void notifyEndOfUpdates() {
// regenera a lista de horários
updateAgenda();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// estado do menu
initMenu();
// Se houver um cancelamento, mas a reunião tiver sido eliminada, é necessário atualizar a agenda local
if (runningTasksHaveBeenCanceled && rdvSupprimé) {
// elimina-se o compromisso da agenda local (não foi possível aceder à agenda global)
agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
// atualiza-se a interface visual
updateAgenda();
}
}
// métodos privados ------------------------------------------------
private void initMenu() {
// estado do menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2-19: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
- linha 6: o título exibido na parte superior da vista;
- linhas 7-17: as informações (top, firstPosition) que permitirão restabelecer o scrolling do ListView;
- linhas 21-24: o número do fragmento é [IMainActivity.VUE_AGENDA];
- linhas 26-35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado nas vezes seguintes (previousState != null);
- linhas 30-34: se não for a primeira visita ao fragmento, recuperam-se as informações (top, firstPosition) que permitirão restabelecer o scrolling a partir do ListView;
- linhas 38-40: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída nas vezes seguintes (previousState != null). Não há nada a fazer aqui, porque o ListView dos intervalos será gerado pelo método privado [updateAgenda] (linhas 61-65);
- linhas 42-52: executadas quando se chega ao fragmento através de uma operação [SUBMIT]. Vem-se, então, da vista [ACCUEIL];
- linha 45: recupera-se a agenda ativada pela sessão por [AccueilFragment];
- linhas 47-49: gera-se o título da vista;
- o ListView dos intervalos de tempo será gerado pelo método privado [updateAgenda] (linhas 61-65);
- linhas 54-59: executadas quando se chega ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
- linhas 57-58: regenera-se o título da vista;
- o ListView dos intervalos será gerado pelo método privado [updateAgenda] (linhas 61-65);
- linhas 72-74: executadas quando todas as atualizações anteriores tiverem sido efetuadas. Atualiza-se o ListView dos intervalos, uma vez que esta atualização é necessária independentemente da forma como se acede ao fragmento;
- linhas 67-77: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 70: o menu é reposto no seu estado predefinido (linhas 82-86);
- linha 72: havia duas tarefas assíncronas. Verifica-se se a primeira (a eliminação do compromisso) foi bem-sucedida, apesar de ter sido cancelada;
- linha 74: se sim, elimina-se o compromisso da agenda local
- linha 75: e atualiza-se a visualização do mesmo;
3.6.9. Gestão da vista de adição de um compromisso
3.6.9.1. A vista
A vista de adição de um compromisso é a seguinte:

Os elementos da interface visual são os seguintes:
3.6.9.2. O fragmento
A vista de adição de um compromisso é gerida pelo seguinte fragmento [AjoutRvFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {
// elementos da interface visual
@ViewById(R.id.spinnerClients)
protected Spinner spinnerClients;
@ViewById(R.id.txt_titre2_ajoutRv)
protected TextView txtTitre2;
// os clientes
private List<Client> clients;
// dados locais
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementação dos métodos da classe pai ----------------------------------
...
}
- linha 26: o fragmento está associado ao seguinte menu [menu_ajout_rv]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
<item
android:id="@+id/navigationToAgenda"
android:title="@string/navigationToAgenda"/>
</menu>
</item>
</menu>
- linhas 30-33: os elementos da interface visual;
- linha 36: a lista de clientes;
- linha 43: a fonte de dados do spinner dos clientes;
O clique no link [Valider] é gerido pelo seguinte método [doValider]:
// os clientes
private List<Client> clients;
// dados locais
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
...
// validação da página
@OptionsItem(R.id.actionValider)
protected void doValider() {
// recuperar o cliente selecionado
Client client = clients.get(spinnerClients.getSelectedItemPosition());
// início da espera por 2 tarefas assíncronas
beginWaiting(2);
// adiciona-se o RV
rdvAjouté = false;
executeInBackground(
mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// processa-se a resposta
consumeRv(responseRv);
}
});
}
// consumo de um objeto Response<Rv>
void consumeRv(Response<Rv> responseRv) {
// erro?
if (responseRv.getStatus() != 0) {
// mensagem
showAlert(responseRv.getMessages());
// anulação
doAnnuler();
// regresso ao UI
return;
}
// observa-se que a marcação foi adicionada
rdvAjouté = true;
// o compromisso é guardado
this.rv = responseRv.getBody();
// solicita-se a nova agenda
executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// processa-se a resposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// consumo de um objeto Response<AgendaMedecinJour>
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// erro?
if (responseAgendaMedecinJour.getStatus() != 0) {
// mensagem
showAlert(responseAgendaMedecinJour.getMessages());
// cancelamento
doAnnuler();
// regresso ao UI
return;
}
// coloca-se a agenda na sessão
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- linha 13: quando o método [doValider] é iniciado, os campos 2, 5, 6 e 9 foram inicializados durante o ciclo de vida do fragmento. Veremos como;
- linha 15: recupera-se a entidade [Client] correspondente ao elemento selecionado no spinner dos clientes;
- linha 17: indica-se à classe pai que se vão iniciar duas tarefas assíncronas e prepara-se a espera;
- linha 19: inicialmente, a consulta ainda não foi adicionada à agenda do médico;
- linhas 20-30: solicita-se ao servidor a adição de um compromisso;
- linha 20: o método [executeInBackground] espera dois parâmetros:
- linha 20: o processo a executar e a observar é fornecido pelo método [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
- linhas 22-29: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
- linha 27: quando se recebe a resposta, esta é passada para o método [consumeRV] da linha 33;
- linha 20: o método [executeInBackground] espera dois parâmetros:
- linhas 33-56: recebemos a resposta do servidor. Analisamo-la;
- linhas 35-42: verifica-se primeiro se o servidor sinalizou um erro no campo [status] da resposta;
- linha 37: se houver erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 39: cancelam-se todas as tarefas;
- linha 41 : regressa-se à interface do utilizador;
- linha 44: se não tiver ocorrido nenhum erro, regista-se que o compromisso foi adicionado;
- linha 46: guarda-se a consulta adicionada num campo do fragmento;
- linhas 47-55: tal como foi feito aquando da eliminação de um compromisso, após a adição do compromisso solicita-se ao servidor a agenda mais recente do médico;
- linhas 47-56, 59-71: trata-se de um código que já apareceu várias vezes;
O método [beginWaiting] (linha 17) é o seguinte:
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// prepara-se o lançamento das tarefas
beginRunningTasks(numberOfRunningTasks);
// estado dos botões e menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: indica-se à tarefa principal que se vai iniciar as tarefas [numberOfRunningTasks];
- linha 6: ocultam-se todas as opções do menu;
- linha 7: para, em seguida, tornar visível a opção [Actions/Annuler];
O clique na opção de menu [Annuler] é gerido pelo método [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelam-se as tarefas assíncronas
cancelRunningTasks();
}
- linha 7: solicita-se à classe pai que cancele as tarefas assíncronas;
As navegações para trás são asseguradas pelos três métodos seguintes:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navegação para a vista de configuração
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// navega-se para a vista de configuração
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAgenda)
protected void navigationToAgenda() {
// navega-se para a vista da agenda
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}
3.6.9.3. Gestão do ciclo de vida do fragmento
O fragmento apresenta o seguinte estado: [AjoutRvFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
// estado do fragmento AjoutRvFragment
public class AjoutRvFragmentState extends CoreState {
// posição do cliente selecionado
private int selectedClientPosition;
// título da vista
private String titre;
// fonte de dados do spinner de clientes
private String[] spinnerClientsDataSource;
// getters e setters
...
}
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementação dos métodos da classe pai ----------------------------------
@Override
public CoreState saveFragment() {
// guardar vista
AjoutRvFragmentState state = new AjoutRvFragmentState();
state.setTitre(txtTitre2.getText().toString());
state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
state.setSpinnerClientsDataSource(spinnerClientsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AJOUT_RV;
}
@Override
protected void initFragment(CoreState previousState) {
// recuperação dos clientes na sessão
clients = session.getClients();
// Primeira visita?
if (previousState == null) {
// construção da tabela apresentada pelo spinner
spinnerClientsDataSource = new String[clients.size()];
int i = 0;
for (Client client : clients) {
spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
i++;
}
} else {
// não é a primeira visita
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
spinnerClientsDataSource = state.getSpinnerClientsDataSource();
}
}
@Override
protected void initView(CoreState previousState) {
// associação do spinner à sua fonte de dados
ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
spinnerClientsDataSource);
dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerClients.setAdapter(dataAdapterClients);
// 1.ª visita?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// recupera-se o número do horário a reservar na sessão
int position = session.getPosition();
// recupera-se a agenda do médico na sessão
AgendaMedecinJour agenda = session.getAgenda();
// recupera-se o médico e o horário em que se vai marcar uma consulta
medecin = agenda.getMedecin();
creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
// constrói-se o título 2 da página
String jour = session.getJourRv();
txtTitre2.setText(String.format(Locale.FRANCE,
"Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
creneau.getMfin()));
// seleção do cliente
spinnerClients.setSelection(0);
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restabelecimento do estado anterior
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
// título
txtTitre2.setText(state.getTitre());
// indicador de carregamento
spinnerClients.setSelection(state.getSelectedClientPosition());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// estado do menu
initMenu();
// próxima vista?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
// houve um cancelamento - compromisso já adicionado?
if (rdvAjouté) {
// estamos a alterar a agenda local (não recebemos a agenda global)
AgendaMedecinJour agenda = session.getAgenda();
agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
// exibir a agenda
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
}
// métodos privados -------------------
private void initMenu() {
// estado do menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2-10: quando a sua classe pai o solicita, o fragmento guarda o estado dos seguintes elementos:
- linha 6: o título na parte superior da vista;
- linha 7: a posição do elemento selecionado no spinner de clientes;
- linha 8: a fonte de dados do seletor de clientes;
- linhas 12-15: o número do fragmento é [IMainActivity.VUE_AJOUT_RV];
- linhas 17-35: executadas quando o fragmento é gerado pela primeira vez (previousState==null) ou regenerado nas vezes seguintes (previousState !=null);
- linha 20: recupera-se a lista de clientes da sessão para a inserir num campo do fragmento;
- linhas 22-30: no caso de uma primeira visita, a fonte de dados do spinner dos clientes é criada;
- linhas 32-33: para as restantes visitas, a fonte de dados do spinner de clientes é recuperada do estado anterior do fragmento;
- linhas 37-49: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída nas vezes seguintes (previousState !=null);
- linhas 40-43: em todos os casos, o spinner dos clientes é associado à sua fonte de dados;
- linhas 45-48: na primeira visita, o menu é apresentado sem a ação [Annuler] (linhas 107-111);
- linhas 51-70: executadas quando se acede ao fragmento através de uma operação [SUBMIT]. Nesse caso, provém-se da vista [AGENDA];
- linha 54: recupera-se o número do intervalo de tempo no qual se vai inserir um compromisso;
- linhas 56-59: recuperam-se as entidades [Medecin] e [Creneau] necessárias para a adição deste compromisso e colocam-se nos campos do fragmento;
- linhas 61-65: com estas informações, é possível construir o título da vista;
- linha 67: o spinner dos clientes é posicionado no seu primeiro elemento;
- linha 69: o menu é colocado no seu estado inicial (sem a opção [Annuler]);
- linhas 72-80: executadas quando se acede ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
- linha 77: o título da vista é regenerado;
- linha 79: reposiciona o seletor de clientes no último cliente selecionado;
- linhas 82-84: executadas quando todas as atualizações anteriores tiverem sido feitas. Aqui não há mais nada a fazer;
- linhas 86-104: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 89: o menu é reposto no seu estado predefinido;
- linhas 91-94: se as tarefas tiverem sido concluídas normalmente, regressa-se à vista [AGENDA] através de um [SUBMIT] (aqui, poderia também ter sido uma ação do tipo NAVIGATION);
- linhas 96-103: se as tarefas tiverem terminado com uma anulação, verifica-se na mesma se o compromisso foi adicionado (o que significaria que foi a obtenção da nova agenda que falhou);
- linhas 98-99: se o compromisso tiver sido adicionado;
- linhas 98-99: o compromisso devolvido pelo servidor é adicionado à agenda atual, a que está ativa na sessão;
- linha 101: regressa-se à vista [AGENDA] através de um [SUBMIT] (neste caso, poderia também ter sido uma ação do tipo NAVIGATION);
3.7. Exécution
Faça os seguintes testes:
- utilize a aplicação em condições normais e verifique se funciona;
- gire o dispositivo para cada uma das vistas e verifique se todas são restauradas corretamente;
- definir um tempo de espera de alguns segundos em [IMainActivity];
- em seguida, cancele as tarefas e verifique se o resultado obtido é o esperado;
- gire o dispositivo durante os períodos de espera e verifique se as tarefas são efetivamente canceladas e se não ocorre nenhuma falha;
- alterar a adjacência dos fragmentos em [IMainActivity] e verificar se a aplicação continua a funcionar;












































