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

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

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

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

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

A comunicação assíncrona entre o cliente e o servidor será gerida utilizando a biblioteca RxAndroid.
3.4. A base de dados
Não desempenha um papel fundamental neste documento. Apresentamo-la apenas para fins informativos. Iremos chamá-la de [ dbrdvmedecins]. Trata-se de uma base de dados MySQL5 com quatro tabelas:
![]() |
3.4.1. A tabela [MEDECINS]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: o número de identificação do médico — a chave primária da tabela
- VERSION: um número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- LAST_NAME: o apelido do médico
- FIRST_NAME: o nome próprio do médico
- TITLE: o seu título (Sra., Sra., Sr.)
3.4.2. A tabela [CLIENTS]
Os clientes dos vários médicos estão armazenados na tabela [CLIENTS]:
![]() | ![]() |
- ID: o número de identificação do cliente — a chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- LAST NAME: o apelido do cliente
- NOME: o nome do cliente
- TÍTULO: o seu título (Sra., Sra., Sr.)
3.4.3. A tabela [SLOTS]
Apresenta os horários disponíveis para marcações:
![]() |
![]() | ![]() | ![]() |
- ID: Número de identificação do intervalo de tempo - chave primária da tabela (linha 8)
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- DOCTOR_ID: número de identificação do médico a quem este intervalo de tempo pertence – chave estrangeira na coluna DOCTORS(ID).
- START_TIME: hora de início do intervalo de tempo
- MSTART: Minuto de início do intervalo de tempo
- HFIN: hora de fim do intervalo
- MFIN: minutos de fim do intervalo
A segunda linha da tabela [SLOTS] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
3.4.4. A tabela [RV]
Apresenta a lista de consultas marcadas para cada médico:
![]() | ![]() |
- ID: identificador único da consulta – chave primária
- DAY: dia da consulta
- SLOT_ID: intervalo horário da consulta – chave estrangeira no campo [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
- CUSTOMER_ID: o ID do cliente para quem a reserva é feita – uma chave estrangeira no campo [ID] da tabela [CUSTOMERS]
Esta tabela tem uma restrição de unicidade nos valores das colunas associadas (DAY, SLOT_ID):
Se uma linha na tabela [RV] tiver o valor (DAY1, SLOT_ID1) para as colunas (DAY, SLOT_ID), este valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que foram marcadas duas consultas ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança uma SQLException quando isto ocorre.
A linha com ID igual a 3 (ver [1] acima) significa que foi marcada uma consulta para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [SLOTS] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Sra. Brigitte BISTROU.
3.4.5. Gerar a base de dados
Para criar as tabelas e preenchê-las, pode utilizar o script [dbrdvmedecins.sql], que se encontra no arquivo de exemplos |AQUI|.
![]() |
Com o [WampServer] (ver secção 6.15), proceda da seguinte forma:
![]() | ![]() |
- Em [1], clique no ícone [WampServer] e selecione a opção [PhpMyAdmin] [2],
- em [3], na janela que se abre, selecione o link [Bases de dados],
![]() |
- em [4-6], importe um ficheiro SQL,
![]() | ![]() | ![]() |
- em [7], selecione o script SQL e, em [8], execute-o,
- em [9], as tabelas da base de dados foram criadas. Siga uma das ligações,
![]() |
- em [10], o conteúdo da tabela.
Não voltaremos a esta base de dados, mas convidamos o leitor a acompanhar a sua evolução ao longo dos testes, especialmente quando a aplicação não estiver a funcionar.
3.5. O servidor Web / JSON

Aqui, focamo-nos no servidor [1]. Não o iremos desenvolver mais. Foi detalhado no documento [Spring MVC e Thymeleaf por Exemplo]. Os leitores interessados podem consultá-lo. Foi desenvolvido tal como o servidor do Exemplo 15. O seu código-fonte está incluído nos exemplos. Aqui, iremos utilizar o seu binário:
![]() |
- [rdvmedecins-server-all-1.0.jar] é o ficheiro binário do servidor;
3.5.1. Implementação
Numa janela de comando, navegue até à pasta que contém o binário do servidor:
...\rdvmedecins>dir
Le volume dans le lecteur D s’appelle Données
Le numéro de série du volume est 7A34-AE5F
Répertoire de D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins
09/06/2016 10:50 <DIR> .
09/06/2016 10:50 <DIR> ..
06/07/2014 16:36 7 631 dbrdvmedecins.sql
08/06/2016 16:31 <DIR> rdvmedecins-client
08/06/2016 16:22 <DIR> rdvmedecins-server
08/06/2016 16:23 29 618 709 rdvmedecins-server-all-1.0.jar
Em seguida, para iniciar o servidor, introduza o seguinte comando (o SGBD MySQL já deve estar em execução):
...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.0)
10:55:48.617 [main] INFO rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting service Tomcat
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFOS: Starting Servlet Engine: Apache Tomcat/8.0.33
juin 09, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFOS: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO rdvmedecins.boot.Boot - Started Boot in 8.231 seconds
O servidor apresenta inúmeros registos. Incluímos apenas aqueles relevantes para a compreensão do processo acima:
- linhas 14–18: Um servidor Tomcat incorporado é iniciado na porta 8080 da máquina. Este servidor executa a aplicação web de gestão de consultas. Esta aplicação é, na verdade, um serviço web/JSON: é consultada através de URLs e responde enviando uma cadeia JSON;
- linha 24: o serviço web é protegido utilizando a estrutura [Spring Security]. As URLs do serviço web são acedidas mediante autenticação;
- Linhas 29–44: as URLs expostas pelo serviço web;
Iremos abordar estes pontos com mais detalhe.
3.5.2. Proteção do serviço web
As URLs expostas pelo serviço web são protegidas. O servidor espera o seguinte cabeçalho na solicitação HTTP do cliente:
O código esperado é a codificação Base64 [http://fr.wikipedia.org/wiki/Base64] da cadeia de caracteres «username:password». No seu estado inicial, o serviço web apenas aceita um utilizador chamado «admin» com a palavra-passe «admin». Para este utilizador específico, o cabeçalho acima torna-se a seguinte linha:
Para enviar este cabeçalho HTTP, utilizamos o cliente HTTP [Advanced Rest Client], que é um plugin do navegador Chrome (ver secção 6.13). Iremos testar manualmente os vários URLs expostos pelo serviço web para compreender:
- os parâmetros esperados pela URL;
- a natureza exata da sua resposta;
3.5.3. Lista de médicos
A URL [/getAllMedecins] recupera a lista de médicos:
![]() |
- em [1], a URL que está a ser consultada;
- em [2], o método HTTP utilizado para este pedido;
- em [3], o cabeçalho de segurança HTTP do utilizador (admin, admin);
- em [4], a solicitação HTTP é enviada;
A resposta do servidor é a seguinte:
![]() |
- em [5], a resposta JSON formatada do servidor;
![]() |
- em [6], a mesma resposta em formato bruto;
O formato em [5] facilita a visualização da estrutura da resposta. Todas as respostas do serviço web são instâncias da seguinte classe [Response]:
package rdvmedecins.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- linha 9: o estado da resposta. Um valor de 0 significa que não houve erro; caso contrário, ocorreu um erro;
- linha 11: uma lista de mensagens de erro, caso tenha ocorrido um erro;
- linha 13: a resposta realmente esperada pelo cliente;
A resposta ao URL [/getAllMedecins] é uma cadeia JSON de um objeto do tipo [Response<List<Medecin>>]. A classe [Medecin] é a seguinte:
package rdvmedecins.android.dao.entities;
public class Medecin extends Personne {
// default builder
public Medecin() {
}
// builder with parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
Linha 3: A classe [Doctor] estende a seguinte classe [Person]:
package rdvmedecins.android.dao.entities;
public class Personne extends AbstractEntity {
// attributes of a person
private String titre;
private String nom;
private String prenom;
// default builder
public Personne() {
}
// builder with parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters and setters
...
}
Linha 3: A classe [Person] estende a seguinte classe [AbstractEntity]:
package rdvmedecins.android.dao.entities;
import java.io.Serializable;
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
protected Long id;
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters and setters
...
}
Em última análise, a estrutura de um objeto [Doctor] é a seguinte:
[Long id; Long version; String titre; String nom; String prenom;]
e a de [Response<List<Doctor>>] é a seguinte:
Daqui em diante, utilizaremos estas definições abreviadas para descrever a resposta do servidor. Além disso, por enquanto, deixaremos de incluir capturas de ecrã. Basta rever o que acabámos de abordar. Voltaremos às capturas de ecrã quando chegar a altura de efetuar um pedido POST. Apresentaremos também um exemplo de execução no seguinte formato:
3.5.4. Lista de clientes
| |
|
Exemplo:
3.5.5. Lista de horários disponíveis para consultas médicas
|
- [idMedecin]: ID do médico para o qual pretende os horários de consulta;
- [startTime]: hora de início da consulta;
- [start_time]: hora de início da consulta;
- [hfin]: hora de fim da consulta;
- [endmin] : minutos de término da consulta;
Para um intervalo de tempo entre as 10:20 e as 10:40, temos [starts, starts, ends, ends] = [10, 20, 10, 40].
Exemplo:
3.5.6. Lista de consultas de um médico
|
- [idMedic] : identificador do médico cujas consultas são solicitadas;
- URL [day]: dia das consultas no formato «aaaa-mm-dd»;
- Resposta [dia]: igual ao anterior, mas na forma de uma data Java;
- [client]: o cliente para a consulta. A sua estrutura foi descrita anteriormente;
- [idClient]: o identificador do cliente;
- [slot]: o horário da consulta. A sua estrutura foi descrita anteriormente;
- [slotId]: o identificador do horário;
Exemplo:
3.5.7. Agenda de um médico
|
- [doctorId]: identificador do médico cujas consultas são pretendidas;
- URL [dia] : dia das consultas no formato «aaaa-mm-dd» ;
- [calendar]: calendário do médico;
- [doctor]: o médico em questão. A sua estrutura foi definida anteriormente;
- Resposta [dia]: o dia do calendário na forma de uma data Java;
- [doctorDaySlots]: uma matriz de elementos do tipo [DoctorDaySlot];
- [slot]: um horário. A sua estrutura foi descrita anteriormente;
- [appointment]: uma consulta. A sua estrutura foi descrita anteriormente;
Exemplo:
|
Destacámos o caso em que existe uma consulta nesse horário e o caso em que não existe nenhuma.
3.5.8. Procurar um médico pelo seu ID
|
- [doctorId]: o ID do médico;
Exemplo 1:
Exemplo 2:
3.5.9. Obter um cliente pelo ID
|
- [idClient]: o ID do cliente;
Exemplo 1:
Exemplo 2:
3.5.10. Marque um horário utilizando o seu ID
|
- [slotId]: o ID do slot;
Exemplo 1:
Note que a resposta não inclui o médico responsável pelo horário, apenas o seu ID.
Exemplo 2:
3.5.11. Obter uma marcação pelo seu ID
|
- [idRv]: o ID da marcação;
Exemplo 1:
Note que a resposta não inclui o cliente nem o horário da consulta, mas apenas os seus identificadores.
Exemplo 2:
3.5.12. Adicionar um compromisso
A URL [/addAppointment] permite-lhe adicionar um compromisso. As informações necessárias para esta adição (o dia, o intervalo de tempo e o cliente) são enviadas através de um pedido HTTP POST. Mostramos como efetuar este pedido utilizando a ferramenta [Advanced Rest Client].

- em [1], a URL que está a ser consultada;
- em [2], é consultada através de uma solicitação POST;
- em [3-4], especificamos ao servidor que os valores a serem enviados estão no formato JSON;
- em [4], o cabeçalho de autenticação HTTP;
- em [5], a informação enviada através da solicitação POST. Trata-se de uma string JSON contendo:
- [day]: o dia da marcação no formato «aaaa-mm-dd»,
- [idClient]: o ID do cliente para quem a consulta está a ser marcada,
- [idCreneau]: o identificador do intervalo de tempo da consulta. Uma vez que um intervalo de tempo pertence a um médico específico, isto também se refere ao médico;
- em [6], a solicitação é enviada;
A cadeia JSON que é enviada é a do seguinte objeto [PostAjouterRv]:
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
// manufacturers
public PostAjouterRv() {
}
public PostAjouterRv(String jour, long idCreneau, long idClient) {
this.jour = jour;
this.idClient = idClient;
this.idCreneau = idCreneau;
}
// getters and setters
...
}
A resposta do servidor é do tipo [Response<Rv>] [int status; List<String> messages; Rv rv], em que [rv] é o compromisso adicionado.
A resposta do servidor ao pedido acima é a seguinte:
![]() |
Note que algumas informações não estão incluídas [idClient, idCreneau], mas podem ser encontradas nos campos [client] e [creneau]. A informação importante é o ID do compromisso adicionado (209). O serviço web poderia ter simplesmente devolvido esta única informação.
3.5.13. Eliminar um compromisso
Esta operação também é realizada através de um pedido POST:
|
O valor enviado é a cadeia JSON de um objeto do tipo [PostSupprimerRv], conforme se segue:
public class PostSupprimerRv {
// pOST DATA
private long idRv;
// manufacturers
public PostSupprimerRv() {
}
public PostSupprimerRv(long idRv) {
this.idRv = idRv;
}
// getters and setters
...
}
- Linha 4: [idRv] é o ID do compromisso a ser eliminado.
Exemplo 1:
A marcação n.º 209 foi eliminada com sucesso porque [status=0].
Exemplo 2:
3.6. O cliente Android

Agora que o servidor [1] foi descrito em detalhe e está em funcionamento, vamos examinar o cliente Android [2].
3.6.1. Arquitetura do projeto Android Studio
O projeto utiliza a arquitetura do projeto [client-android-skel] (ver secção 1.17). Na arquitetura do cliente Android apresentada acima, existem três camadas distintas:
- a camada [DAO], responsável pela comunicação com o serviço web;
- a camada [views], responsável pela comunicação com o utilizador;
- a [Activity], que atua como elo de ligação entre os dois blocos anteriores. As views não têm conhecimento da camada [DAO]. Comunicam apenas com a Activity.
Esta arquitetura está refletida no projeto do Android Studio para o cliente Android:
![]() |
- o pacote [activity] implementa a atividade;
- o pacote [architecture] inclui os elementos arquitetónicos que desenvolvemos anteriormente;
- o pacote [dao] implementa a camada [DAO];
- o pacote [fragments] implementa as [views];
3.6.2. Personalização do projeto
![]() |
A pasta [architecture/custom] contém os elementos personalizáveis da arquitetura.
A interface [IMainActivity] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 4;
// view n°s
int VUE_CONFIG = 0;
int VUE_ACCUEIL = 1;
int VUE_AGENDA = 2;
int VUE_AJOUT_RV = 3;
}
- linhas 25, 28: personalização da camada [DAO];
- linha 31: esta aplicação faz pedidos autenticados ao servidor;
- linha 40: é necessária uma imagem de carregamento;
- linha 43: a aplicação tem quatro fragmentos;
- linhas 46–49: os números dos quatro fragmentos;
- linha 37: não há separadores;
A classe base [CoreState] para os estados dos fragmentos será a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = AccueilFragmentState.class),
@JsonSubTypes.Type(value = AgendaFragmentState.class),
@JsonSubTypes.Type(value = AjoutRvFragmentState.class),
@JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- linhas 15–18: os quatro fragmentos têm um estado:
![]() |
Por fim, a sessão contém os dados partilhados entre fragmentos:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import java.util.List;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// list of doctors
private List<Medecin> médecins;
// customer list
private List<Client> clients;
// a doctor's diary for a given day
private AgendaMedecinJour agenda;
// position of clicked item in diary
private int position;
// rv day in English notation "yyyy-MM-dd"
private String dayRv;
// rv day in French notation "dd-MM-yyyy"
private String jourRv;
// getters and setters
...
}
- Linhas 17–28: A sessão armazena seis informações. Explicaremos as suas funções quando necessário.
3.6.3. A camada [DAO]
![]() |
![]() | ![]() |
- em [1], as entidades encapsuladas nas respostas do servidor. Estas foram apresentadas na Secção 3.5;
- em [2], os componentes do cliente que tratam da comunicação com o servidor;
Não iremos revisitar os componentes em [1]. Estes já foram apresentados. O leitor é convidado a consultar a Secção 3.5, se necessário. Iremos examinar a implementação do pacote [service]. Isto também nos levará a discutir a implementação da comunicação segura entre o cliente e o servidor.
3.6.3.1. Implementação da comunicação cliente/servidor
![]() |
A classe [WebClient] é um componente AA que descreve:
- as URLs expostas pelo serviço web;
- os seus parâmetros;
- as suas respostas;
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
public void setRestTemplate(RestTemplate restTemplate);
// list of doctors
@Get("/getAllMedecins")
public Response<List<Medecin>> getAllMedecins();
// customer list
@Get("/getAllClients")
public Response<List<Client>> getAllClients();
// list of physician slots
@Get("/getAllCreneaux/{idMedecin}")
public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
// list of doctor's appointments
@Get("/getRvMedecinJour/{idMedecin}/{jour}")
public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
// Customer
@Get("/getClientById/{id}")
public Response<Client> getClientById(@Path long id);
// Doctor
@Get("/getMedecinById/{id}")
public Response<Medecin> getMedecinById(@Path long id);
// Rv
@Get("/getRvById/{id}")
public Response<Rv> getRvById(@Path long id);
// Niche
@Get("/getCreneauById/{id}")
public Response<Creneau> getCreneauById(@Path long id);
// add a RV
@Post("/ajouterRv")
public Response<Rv> ajouterRv(@Body PostAjouterRv post);
// delete an appointment
@Post("/supprimerRv")
public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
// get a doctor's schedule
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
}
- linhas 19–60: todas as URLs discutidas na secção 3.5 estão presentes;
- linha 16: o componente [RestTemplate] do [Spring Android] no qual se baseia a comunicação cliente/servidor;
3.6.3.2. A interface [IDao]
![]() |
A interface [IDao] da camada [DAO] é a seguinte:
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import rx.Observable;
import java.util.List;
public interface IDao {
// Web service url
public void setUrlServiceWebJson(String url);
// user
public void setUser(String user, String mdp);
// customer timeout
public void setTimeout(int timeout);
// customer list
public Observable<List<Client>> getAllClients();
// list of doctors
public Observable<List<Medecin>> getAllMedecins();
// list of physician slots
public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
// list of doctor's appointments on a given day
public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
// find a customer identified by its id
public Observable<Client> getClientById(long id);
// find a doctor identified by his id
public Observable<Medecin> getMedecinById(long id);
// find an Rv identified by its id
public Observable<Rv> getRvById(long id);
// find a time slot identified by its id
public Observable<Creneau> getCreneauById(long id);
// add a RV to the list
public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
// delete a RV
public Observable<Rv> supprimerRv(long idRv);
// job
public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
- linha 10: para definir a URL do serviço web / JSON;
- linha 13: para definir o utilizador para a comunicação cliente/servidor. [user] é o ID do utilizador, [password] é a palavra-passe;
- linha 16: para definir um tempo limite máximo para a resposta do servidor;
- linhas 18–49: cada URL exposta pelo serviço web corresponde a um método. Utilizam as mesmas assinaturas de método que o componente AA [WebClient];
- linha 52: para controlar o modo de depuração da camada [DAO];
3.6.3.3. A classe [Dao]
![]() |
A implementação [DAO] da interface [IDao] anterior é a seguinte:
package client.android.dao.service;
import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
...
}
@Override
public void setUrlServiceWebJson(String url) {
...
}
@Override
public void setUser(String user, String mdp) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// implementation of the IDao interface --------------------------------------------------------------------
@Override
public Observable<Response<List<Client>>> getAllClients() {
// log
log("getAllClients");
// result
return getResponse(new IRequest<Response<List<Client>>>() {
@Override
public Response<List<Client>> getResponse() {
return webClient.getAllClients();
}
});
}
@Override
public Observable<Response<List<Medecin>>> getAllMedecins() {
// log
log("getAllMedecins");
// result
return getResponse(new IRequest<Response<List<Medecin>>>() {
@Override
public Response<List<Medecin>> getResponse() {
return webClient.getAllMedecins();
}
});
}
@Override
public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
// log
log("getAllCreneaux");
// result
return getResponse(new IRequest<Response<List<Creneau>>>() {
@Override
public Response<List<Creneau>> getResponse() {
return webClient.getAllCreneaux(idMedecin);
}
});
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
// log
log("getRvMedecinJour");
// result
return getResponse(new IRequest<Response<List<Rv>>>() {
@Override
public Response<List<Rv>> getResponse() {
return webClient.getRvMedecinJour(idMedecin, jour);
}
});
}
@Override
public Observable<Response<Client>> getClientById(final long id) {
// log
log("getClientById");
// result
return getResponse(new IRequest<Response<Client>>() {
@Override
public Response<Client> getResponse() {
return webClient.getClientById(id);
}
});
}
@Override
public Observable<Response<Medecin>> getMedecinById(final long id) {
// log
log("getMedecinById");
// result
return getResponse(new IRequest<Response<Medecin>>() {
@Override
public Response<Medecin> getResponse() {
return webClient.getMedecinById(id);
}
});
}
@Override
public Observable<Response<Rv>> getRvById(final long id) {
// log
log("getRvById");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.getRvById(id);
}
});
}
@Override
public Observable<Response<Creneau>> getCreneauById(final long id) {
// log
log("getCreneauById");
// result
return getResponse(new IRequest<Response<Creneau>>() {
@Override
public Response<Creneau> getResponse() {
return webClient.getCreneauById(id);
}
});
}
@Override
public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
// log
log("ajouterRv");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
}
});
}
@Override
public Observable<Response<Rv>> supprimerRv(final long idRv) {
// log
log("supprimerRv");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.supprimerRv(new PostSupprimerRv(idRv));
}
});
}
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// result
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
}
- linhas 18–72: estas são as linhas predefinidas na classe [Dao] do projeto [client-android-skel];
- linhas 74–216: implementação da interface [IDao]. Os métodos que consultam os URLs expostos pelo serviço web delegam esta consulta ao componente AA [WebClient] (linhas 22–23);
- linhas 58–63: se as trocas cliente/servidor forem autenticadas usando autenticação básica, um interceptor é adicionado ao componente [RestTemplate]. Isto fará com que qualquer pedido HTTP enviado pelo componente [RestTemplate] seja interceptado pela classe [MyAuthInterceptor] (linhas 25–26);
A classe [MyAuthInterceptor] é a seguinte:
package rdvmedecins.android.dao.security;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// user
private String user;
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
headers.setAuthorization(auth);
return execution.execute(request, body);
}
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
- linha 15: a classe [MyAuthInterceptor] é um componente AA do tipo [singleton];
- linha 16: a classe [MyAuthInterceptor] estende a interface [ClientHttpRequestInterceptor] do Spring. Esta interface possui um método, o método [intercept] na linha 22. Estendemos esta interface para interceptar qualquer pedido HTTP do cliente. O método [intercept] recebe três parâmetros;
- [HttpRequest request]: a solicitação HTTP interceptada,
- [byte[] body]: o seu corpo, se tiver um (valores enviados, por exemplo),
- [ClientHttpRequestExecution execution]: o componente Spring que executa a solicitação;
Interceptamos todas as solicitações HTTP do cliente Android para adicionar o cabeçalho de autenticação HTTP apresentado na Secção 3.5.
- linha 23: recuperamos os cabeçalhos HTTP da solicitação interceptada;
- linha 24: criamos o cabeçalho de autenticação HTTP. O método de autenticação utilizado (codificação Base64 da string «user:mdp») é fornecido pela classe Spring [HttpBasicAuthentication];
- linha 25: o cabeçalho de autenticação que acabámos de criar é adicionado aos cabeçalhos atuais da solicitação interceptada;
- linha 26: continuamos a executar a solicitação interceptada. Resumindo, a solicitação interceptada foi enriquecida com o cabeçalho de autenticação;
As implementações dos métodos na interface [IDao] seguem todas o mesmo padrão. Tomemos o exemplo do método [getAgendaMedecinJour]:
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// result
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
- Linha 2: O método espera dois parâmetros:
- [idMedecin]: o ID do médico cuja agenda se pretende consultar;
- [day]: o dia para o qual queremos a agenda;
- linha 6: chamamos o método [getResponse] da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], onde T é o tipo devolvido pelo método [getAgendaMedecinJour] na linha 2, neste caso [Response<AgendaMedecinJour>]. A interface [IRequest] tem apenas um método: [getResponse] (linha 8);
- linhas 8–10: implementação do método [IRequest.getResponse]. Este método deve devolver o resultado esperado pelo método [getAgendaMedecinJour] na linha 2, do tipo [Response<AgendaMedecinJour>];
- linha 9: a resposta é devolvida pelo método [webClient.getAgendaMedecinJour]:
// get a doctor's schedule
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
Os parâmetros utilizados na linha 9 são os que foram passados para o método [getAgendaMedecinJour] na linha 2. Por este motivo, estes parâmetros devem ter o atributo final;
3.6.4. O [MainActivity]
Servidor ![]() |
![]() |
A classe [MainActivity] é a seguinte:
package client.android.activity;
import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.AccueilFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AjoutRvFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;
import java.util.List;
@EActivity
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// parent class ---------------------------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return IMainActivity.VUE_CONFIG;
}
// interface IDao -----------------------------------------------------
...
@Override
public Observable<Response<List<Client>>> getAllClients() {
return dao.getAllClients();
}
@Override
public Observable<Response<List<Medecin>>> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
return dao.getAllCreneaux(idMedecin);
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
return dao.getRvMedecinJour(idMedecin, jour);
}
@Override
public Observable<Response<Client>> getClientById(long id) {
return dao.getClientById(id);
}
@Override
public Observable<Response<Medecin>> getMedecinById(long id) {
return dao.getMedecinById(id);
}
@Override
public Observable<Response<Rv>> getRvById(long id) {
return dao.getRvById(id);
}
@Override
public Observable<Response<Creneau>> getCreneauById(long id) {
return dao.getCreneauById(id);
}
@Override
public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
return dao.ajouterRv(jour, idCreneau, idClient);
}
@Override
public Observable<Response<Rv>> supprimerRv(long idRv) {
return dao.supprimerRv(idRv);
}
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
return dao.getAgendaMedecinJour(idMedecin, jour);
}
}
- linhas 21–66: estas linhas são fornecidas por predefinição no modelo [client-android-skel];
- linhas 66–119: implementação da interface [IDao]. Todos os métodos delegam o trabalho à camada [DAO] na linha 26;
- linhas 42-46: o método [getFragments] devolve a matriz dos quatro fragmentos da aplicação;
- linhas 58-61: a vista de configuração é a primeira vista a ser exibida quando a aplicação é iniciada;
3.6.5. A Sessão
![]() |
A classe [Session] é utilizada para armazenar informações que precisam de ser transmitidas entre fragmentos. É a seguinte:
package rdvmedecins.android.architecture;
import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Medecin;
import org.androidannotations.annotations.EBean;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// list of doctors
private List<Medecin> médecins;
// customer list
private List<Client> clients;
// agenda
private AgendaMedecinJour agenda;
// position of clicked item in diary
private int position;
// rv day in English notation "yyyy-MM-dd"
private String dayRv;
// rv day in French notation "dd-MM-yyyy"
private String jourRv;
// getters and setters
...
}
- linha 10: a classe [Session] é um componente AA instanciado como uma única instância;
- linhas 12–15: Neste estudo de caso, vamos assumir que as listas de médicos e clientes não se alteram. Iremos recuperá-las quando a aplicação for iniciada e armazená-las na sessão para que os fragmentos possam utilizá-las;
- linhas 20–23: a data pretendida para uma consulta. É tratada em dois formatos: na notação francesa (linha 23) no cliente Android e na notação inglesa (linha 21) para comunicação com o servidor;
- linha 19: a posição do elemento clicado (ligação para adicionar/eliminar) no calendário;
3.6.6. Gestão da Visualização de Configuração
3.6.6.1. A vista
A vista de configuração é a vista exibida quando a aplicação é iniciada:

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 {
// visual interface elements
@ViewById(R.id.edt_urlServiceRest)
protected EditText edtUrlServiceRest;
@ViewById(R.id.txt_errorUrlServiceRest)
protected TextView txtErrorUrlServiceRest;
@ViewById(R.id.txt_errorUtilisateur)
protected TextView txtErrorUtilisateur;
@ViewById(R.id.edt_utilisateur)
protected EditText edtUtilisateur;
@ViewById(R.id.edt_mdp)
protected EditText edtMdp;
// seizures
private String urlServiceRest;
private String utilisateur;
private String mdp;
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
..
// implementation methods parent class -------------------------------------------
...
}
- linha 25: o fragmento está associado ao seguinte menu [menu_config]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
</menu>
- linhas 28–38: os elementos da interface visual;
- linhas 41-43: os três campos do formulário;
Ao clicar na opção de menu [Validate], o método [doValidate] é executado:
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
// hide any previous error messages
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
// test the validity of entries
if (!isPageValid()) {
return;
}
// enter the URL of the web service
mainActivity.setUrlServiceWebJson(urlServiceRest);
// user information
mainActivity.setUser(utilisateur, mdp);
// start of wait - 2 asynchronous tasks will be launched
beginWaiting(2);
// doctors
executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
@Override
public void call(Response<List<Medecin>> responseMedecins) {
// we consume the answer
consumeMedecins(responseMedecins);
}
});
// customers
executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
@Override
public void call(Response<List<Client>> responseClients) {
// we consume the answer
consumeClients(responseClients);
}
});
}
private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
// log
if (isDebugEnabled) {
Log.d(className, "consume médecins");
}
// mistake?
if (responseMedecins.getStatus() != 0) {
// message
showAlert(responseMedecins.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// doctors are saved in the session
session.setMédecins(responseMedecins.getBody());
}
private void consumeClients(Response<List<Client>> responseClients) {
// log
if (isDebugEnabled) {
Log.d(className, "consume clients");
}
// mistake?
if (responseClients.getStatus() != 0) {
// message
showAlert(responseClients.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// customers are stored in the session
session.setClients(responseClients.getBody());
}
- linhas 8–10: verifica-se a validade das três entradas do formulário. Se o formulário for inválido, o processo termina aí;
- linhas 11–14: as entradas exigidas pela camada [DAO] são passadas para a atividade;
- linha 16: a classe pai é notificada de que duas tarefas assíncronas serão iniciadas e a espera é preparada;
- linhas 17–24: a lista de médicos é solicitada;
- linha 18: o método [executeInBackground] espera dois parâmetros:
- linha 18: o processo a ser executado e observado é fornecido pelo método [mainActivity.getAllMedecins()];
- linhas 18–24: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<List<Medecin>>]
- linha 22: quando a resposta é recebida, é passada para o método [consumeMedecins] na linha 36;
- linhas 25–33: após iniciar uma primeira tarefa assíncrona, iniciamos uma segunda para solicitar a lista de clientes. Teremos, portanto, duas tarefas a decorrer em paralelo;
- linhas 36–52: recebemos a resposta da tarefa dos médicos. Processamo-la;
- linhas 42–49: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
- linha 44: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 46: cancelamos todas as tarefas;
- linha 48: voltamos à interface do utilizador;
- linha 51: se não houve erro, a lista de médicos é carregada na sessão;
A validade da entrada (linha 8) é verificada utilizando o seguinte método:
private boolean isPageValid() {
// check the validity of the data entered
boolean erreur;
URI service;
// validity of the URL of the REST service
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
try {
service = new URI(urlServiceRest);
erreur = service.getHost() == null || service.getPort() == -1;
} catch (Exception ex) {
// we note the error
erreur = true;
}
if (erreur) {
// error display
txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
}
// user
utilisateur = edtUtilisateur.getText().toString().trim();
if (utilisateur.length() == 0) {
// error is displayed
txtErrorUtilisateur.setVisibility(View.VISIBLE);
// we note the error
erreur = true;
}
// password
mdp = edtMdp.getText().toString().trim();
// return
return !erreur;
}
O método [beginWaiting] (linha 16) é o seguinte:
// beginning of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch tasks
beginRunningTasks(numberOfRunningTasks);
// status of buttons and menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
- linha 6: todas as opções do menu são ocultadas;
- linha 7: em seguida, torna a opção [Ações/Cancelar] visível;
O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- linha 8: solicitamos à classe pai que cancele as tarefas assíncronas;
3.6.6.3. Gestão do ciclo de vida do fragmento
O fragmento apresenta o seguinte estado [ConfigFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// visibility of two error messages
private boolean txtErrorUrlServiceRestVisible;
private boolean txtErrorUtilisateurVisible;
// getters and setters
...
}
- Quando a classe pai o solicitar, o fragmento guardará a visibilidade das suas duas mensagens de erro;
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementation methods parent class -------------------------------------------
@Override
public CoreState saveFragment() {
// save fragment status
ConfigFragmentState state = new ConfigFragmentState();
state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_CONFIG;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
if (previousState == null) {
// 1st visit
// hide error messages
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore error msg visibility
ConfigFragmentState state = (ConfigFragmentState) previousState;
// not the 1st visit - error messages are returned
txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
}
}
// méthodes privées ------------------------------------------------
private void initMenu(){
// menu status
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2–9: quando solicitado pela sua classe pai, o fragmento guarda o estado das suas duas mensagens de erro;
- linhas 11-14: o ID do fragmento é [IMainActivity.VUE_CONFIG];
- linhas 16–19: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (previousState != null). Aqui, não há nada a fazer;
- linhas 21–31: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída em ocasiões subsequentes (previousState != null);
- linhas 24–29: na primeira visita, as mensagens de erro são ocultadas e o menu é exibido sem a ação [Cancel] (linhas 62–66);
- linhas 33–35: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Isto nunca acontece aqui;
- linhas 37–44: executado quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE]. O estado das mensagens de erro é restaurado a partir do estado anterior;
- linhas 47–49: executadas quando todas as atualizações anteriores tiverem sido feitas. Não há mais nada a fazer;
- linhas 51–59: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linhas 53–54: redefina o menu para o seu estado padrão;
- linhas 56–58: se as tarefas foram concluídas com sucesso, passa-se para a próxima vista; caso contrário, permanece-se na mesma vista;
3.6.7. Gestão da Vista Inicial
3.6.7.1. A vista
A vista inicial é a seguinte:

Os elementos da interface visual são os seguintes:
3.6.7.2. O fragmento
O ecrã inicial é gerido pelo seguinte fragmento [HomeFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {
// visual interface elements
@ViewById(R.id.spinnerMedecins)
protected Spinner spinnerMedecins;
@ViewById(R.id.edt_JourRv)
protected DatePicker edtJourRv;
// local data
private List<Medecin> medecins;
private Calendar calendrier;
private String[] spinnerMedecinsDataSource;
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementation methods parent class -------------------------------------
...
}
- linha 26: o fragmento está associado ao seguinte menu [menu_accueil]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
</menu>
</item>
</menu>
- linhas 31–34: os elementos da interface visual;
- Linha 37: a lista de médicos;
- linha 38: um calendário;
- linha 39: a fonte de dados para o spinner dos médicos;
O clique no link [Validate] é tratado pelo seguinte método [doValidate]:
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
// note the id of the selected doctor
Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
// the day is saved in the session
String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
session.setJourRv(jourRv);
// switch to date format yyyy-MM-dd
String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
session.setDayRv(dayRv);
// start wait - 1 asynchronous task will be launched
beginWaiting(1);
// we ask for the doctor's diary
executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// we consume the answer
consumeAgenda(responseAgendaMedecinJour);
}
});
}
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// mistake?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// put the agenda in the session
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- linha 5: recuperar o ID do médico selecionado;
- linhas 7-8: guardamos a data selecionada na sessão no formato francês;
- linhas 10-11: definimos a data selecionada na sessão, no formato inglês;
- linha 13: notificamos a classe pai de que estamos prestes a iniciar uma tarefa assíncrona e preparamo-nos para a espera;
- linhas 15–22: a agenda do médico é recuperada;
- linha 15: o método [executeInBackground] espera dois parâmetros:
- linha 15: o processo a ser executado e observado é fornecido pelo método [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
- linhas 15–22: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<AgendaMedecinJour>]
- linha 20: quando a resposta é recebida, é passada para o método [consumeAgenda] na linha 25;
- linha 15: o método [executeInBackground] espera dois parâmetros:
- linhas 25–37: recebemos a agenda do médico. Processamo-la;
- linhas 27–34: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
- linha 29: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 31: cancelamos todas as tarefas;
- linha 33: voltamos à interface do utilizador;
- linha 36: se não houver erros, o calendário é colocado em foco;
O método [beginWaiting] (linha 13) é o seguinte:
// beginning of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch tasks
beginRunningTasks(numberOfRunningTasks);
// status of buttons and menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
- linha 6: todas as opções do menu são ocultadas;
- linha 7: em seguida, torna a opção [Ações/Cancelar] visível;
O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- linha 8: solicitamos à classe pai que cancele as tarefas assíncronas;
Clicar na opção de menu [Voltar às definições] é tratado da seguinte forma:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- Linha 4: Navegamos para a vista de configuração utilizando a ação [NAVIGATION]. Isto significa que queremos restaurar a vista de configuração para o estado em que a deixámos;
3.6.7.3. Gestão do ciclo de vida do fragmento
O fragmento tem o seguinte [HomeFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
public class AccueilFragmentState extends CoreState {
// fragment status [Home]
// selected doctor's position
private int selectedMedecinPosition;
// selected date
private int year;
private int month;
private int dayOfMonth;
// doctors' spinner data source
private String[] spinnerMedecinsDataSource;
// manufacturers
public AccueilFragmentState() {
}
// getters and setters
...
}
- linha 11: devolve o item selecionado da lista de médicos;
- linhas 13–15: devolve a data selecionada do calendário;
- linha 17: recupera a fonte de dados para a lista de médicos;
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementation methods parent class -------------------------------------
@Override
public CoreState saveFragment() {
// save the view
AccueilFragmentState state = new AccueilFragmentState();
state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
state.setDayOfMonth(edtJourRv.getDayOfMonth());
state.setMonth(edtJourRv.getMonth());
state.setYear(edtJourRv.getYear());
state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_ACCUEIL;
}
@Override
protected void initFragment(CoreState previousState) {
// we get the doctors back in session
medecins = session.getMédecins();
// 1st visit?
if (previousState == null) {
// we build the table displayed by the spinner
spinnerMedecinsDataSource = new String[medecins.size()];
int i = 0;
for (Medecin medecin : medecins) {
spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
i++;
}
} else {
// no 1st visit
AccueilFragmentState state = (AccueilFragmentState) previousState;
spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
}
// the calendar
calendrier = Calendar.getInstance();
}
@Override
protected void initView(CoreState previousState) {
// we associate the doctors' spinner with its data source
ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerMedecins.setAdapter(dataAdapterMedecins);
// minimum calendar date to today
edtJourRv.setMinDate(calendrier.getTimeInMillis());
// 1st visit?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore the state currently in session
AccueilFragmentState state = (AccueilFragmentState) previousState;
// selection in doctors' spinner
spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
// calendar
edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called after all tasks have been completed or cancelled
// menu status
initMenu();
// next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
}
}
// méthodes privées ------------------------------------------------
private void initMenu() {
// menu status
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2–9: quando solicitado pela sua classe pai, o fragmento guarda o estado dos seguintes elementos:
- linha 6: a posição selecionada na lista de médicos;
- linhas 7–9: o dia do mês, o mês e o ano da data selecionada no calendário;
- linha 10: a fonte de dados para o spinner dos médicos;
- linhas 14-17: o ID do fragmento é [IMainActivity.VUE_ACCUEIL];
- linhas 19–39: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (previousState != null);
- linhas 25–31: para uma primeira visita, a fonte de dados para o spinner de médicos é construída;
- linhas 33–35: para visitas subsequentes, a fonte de dados do spinner é recuperada do estado anterior do fragmento;
- linhas 41-54: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState==null) ou reconstruída em visitas subsequentes (previousState !=null);
- linhas 50–53: para a primeira visita, o menu é exibido sem a ação [Cancelar] (linhas 88–92);
- linhas 43–48: para todas as visitas, sejam elas a primeira ou não, o spinner dos médicos é associado à sua fonte (linhas 44–46) e a data mínima no calendário é definida para a data de hoje (linha 48);
- linhas 56–60: executadas quando o fragmento é alcançado através de uma operação [SUBMIT]. O utilizador vem da vista [CONFIG]. O menu é reposto no seu estado inicial;
- linhas 62–70: executadas quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE];
- linha 67: o spinner de médicos é reiniciado para o último médico selecionado;
- linha 69: o calendário é definido para a última data selecionada;
- linhas 72–74: executadas assim que todas as atualizações anteriores tiverem sido concluídas. Não há mais nada a fazer;
- linhas 76–85: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 80: redefina o menu para o seu estado padrão;
- linhas 82–84: se as tarefas foram concluídas normalmente, passa-se para a próxima vista; caso contrário, permanece-se na mesma vista;
3.6.8. Gestão da Vista do Calendário
3.6.8.1. A vista
O ecrã inicial tem o seguinte aspeto:

Os elementos da interface visual são os seguintes:
3.6.8.2. O fragmento
A vista Calendário é gerida pelo seguinte fragmento [AgendaFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {
// visual interface elements
@ViewById(R.id.txt_titre2_agenda)
protected TextView txtTitre2;
@ViewById(R.id.listViewAgenda)
protected ListView lstCreneaux;
// agenda displayed by the fragment
private AgendaMedecinJour agenda;
// info ListView slots
private int firstPosition;
private int top;
// appointment deleted or not
private boolean rdvSupprimé;
// slot number added or deleted
private int numCréneau;
// update schedule after adding/deleting
private void updateAgenda() {
...
}
...
// implementation methods parent class ------------------------------------------------------
...
}
- linha 27: o fragmento está associado ao seguinte menu [menu_agenda]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionAgenda"
android:title="@string/actionAgenda"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
</menu>
</item>
</menu>
- linhas 32–35: elementos da interface visual;
- linhas 37-45: dados globais para os métodos;
3.6.8.2.1. Método [updateAgenda]
A (re)geração da lista de intervalos do calendário é necessária em vários pontos do código. Foi incorporada no seguinte método privado [updateAgenda]:
// update schedule after adding/deleting
private void updateAgenda() {
// (re)generation of calendar slots
// the agenda is taken from the session and stored in a fragment field
agenda = session.getAgenda();
// regeneration of ListView slots
ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour(), this);
lstCreneaux.setAdapter(adapter);
// we reposition ourselves at the right spot on the ListView
lstCreneaux.setSelectionFromTop(firstPosition, top);
}
- linha 5: o calendário é recuperado da sessão e armazenado no campo [calendar] do fragmento;
- linhas 7–9: definimos o adaptador para o componente [ListView]. Este adaptador define tanto a fonte de dados para o [ListView] como o modelo de exibição para cada um dos seus itens. Apresentaremos este adaptador em breve;
- linha 11: voltamos à posição anterior no calendário. Isto porque apenas vemos uma parte dos intervalos de tempo do dia. Se adicionarmos ou removemos um compromisso no último intervalo, o código acima atualizará a página para exibir o novo calendário. Esta atualização faz com que a vista volte ao primeiro intervalo, o que não é desejável. A linha 5 resolve este problema. Pode encontrar uma descrição desta solução no URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];
A classe [ListCreneauxAdapter] é utilizada para definir uma linha no [ListView]:

Conforme mostrado acima, a exibição difere dependendo de o intervalo de tempo ter ou não um compromisso. O código para a classe [ListCreneauxAdapter] é o seguinte:
...
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
// time slot table
private CreneauMedecinJour[] creneauxMedecinJour;
// execution context
private Context context;
// the layout id for displaying a line in the slot list
private int layoutResourceId;
// click listener
private AgendaFragment vue;
// manufacturer
public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaFragment vue) {
super(context, layoutResourceId, creneauxMedecinJour);
// memorize information
this.creneauxMedecinJour = creneauxMedecinJour;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.vue = vue;
// sort the table of slots in schedule order
Arrays.sort(creneauxMedecinJour, new MyComparator());
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
// sorting the slot table
class MyComparator implements Comparator<CreneauMedecinJour> {
...
}
}
- Linha 3: A classe [ListCreneauxAdapter] deve estender um adaptador predefinido para [ListView]s, neste caso a classe [ArrayAdapter], que, como o próprio nome sugere, preenche a [ListView] com uma matriz de objetos, neste caso do tipo [CreneauMedecinJour]. Vamos rever o código desta entidade:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
...
}
- A classe [CreneauMedecinJour] contém um intervalo de tempo (linha 5) e uma consulta potencial (linha 6) ou nulo, caso não haja consulta;
De volta ao código da classe [ListCreneauxAdapter]:
- linha 15: o construtor recebe quatro parâmetros:
- a atividade Android atual,
- o ficheiro XML que define o conteúdo de cada elemento [ListView],
- a matriz dos horários disponíveis do médico,
- a própria vista;
- Linha 24: A matriz de horários é ordenada por ordem crescente de hora;
O método [getView] é responsável por gerar a vista correspondente a uma linha no [ListView]. Esta vista é composta por três elementos:
O código para o método [getView] é o seguinte:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// we position ourselves in the right niche
CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
// create the line
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// the time slot
TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
.getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
// the customer
TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
String text;
if (creneauMedecin.getRv() != null) {
Client client = creneauMedecin.getRv().getClient();
text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
} else {
text = "";
}
txtClient.setText(text);
// the link
final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
if (creneauMedecin.getRv() == null) {
// add
btnValider.setText(R.string.btn_ajouter);
btnValider.setTextColor(context.getResources().getColor(R.color.blue));
} else {
// delete
btnValider.setText(R.string.btn_supprimer);
btnValider.setTextColor(context.getResources().getColor(R.color.red));
}
// link listener
btnValider.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// we skip the news on the calendar view
vue.doValider(position, btnValider.getText().toString());
}
});
// we return the line
return row;
}
- linha 2: position é o número da linha a ser gerada no [ListView]. É também o número do intervalo na matriz [creneauxMedecinJour]. Ignoramos os outros dois parâmetros;
- linha 4: recuperamos o intervalo de tempo a ser exibido na linha do [ListView];
- linha 6: a linha é construída com base na sua definição XML
![]() |
O código do ficheiro [creneau_medecin.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Creneau"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/txt_Client"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Creneau"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Creneau"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/btn_Valider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Client"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Client"
android:text="@string/btn_valider"
android:textColor="@color/blue" />
</RelativeLayout>
- linhas 8–10: o intervalo de tempo [1] é construído;
- linhas 12–20: o ID do cliente [2] é criado;
- linha 23: se o intervalo de tempo não tiver nenhuma marcação;
- linhas 25-26: o link azul [Adicionar] é criado;
- linhas 29-30: caso contrário, o link vermelho [Apagar] é criado;
- linhas 33-40: independentemente do tipo de link [Adicionar / Eliminar], o método [doValider] da vista irá tratar o clique no link. O método receberá dois argumentos:
- o número do intervalo em que se clicou,
- o rótulo do link que foi clicado;
- linha 42: devolvemos a linha que acabámos de criar.
Note que é o método [doValider] do fragmento [AgendaFragment] que trata dos links. É o seguinte:
// click on a link [Add / Remove]
public void doValider(int numCréneau, String texte) {
// operation in progress?
if (numberOfRunningTasks != 0) {
Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
return;
}
// note the scroll position to return to it
// read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of 1st element fully visible or not
firstPosition = lstCreneaux.getFirstVisiblePosition();
// y offset of this element relative to the top of the ListView
// measures the height of any hidden part
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// we also note the number of the clicked slot
this.numCréneau = numCréneau;
// depending on the text of the link, we do not do the same thing
if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
doAjouter();
} else {
doSupprimer();
}
}
- O método [doValider] recebe duas informações:
- o número do slot em que se clicou;
- o texto (Adicionar / Eliminar) do link em que se clicou;
- linhas 4–7: clicar nos links [Eliminar / Adicionar] é desativado se houver tarefas assíncronas em curso. Esta é uma escolha de design que simplifica a escrita do código. Está aberta a discussão;
- linhas 11–15: armazenamos as informações (firstPosition, top) do slot ListView em campos dentro do fragmento para que o método privado [updateAgenda] possa regenerá-lo com a mesma posição de rolagem;
- linha 17: armazenamos o número do slot clicado;
- linhas 19–23: dependendo do texto do link clicado, adicionamos ou removemos um item;
3.6.8.2.2. Método [doDelete]
O método [doSupprimer] garante a remoção do compromisso do slot clicado:
// deleting an appointment
private void doSupprimer() {
// waiting for two tasks to be completed
beginWaiting(2);
// delete the Rdv in the background
rdvSupprimé = false;
// rv identifier to be deleted
long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
// deletion by an asynchronous task
executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// income consumption
consumeRv(responseRv);
}
});
}
// consumption of an answer
private void consumeRv(Response<Rv> responseRv) {
// mistake?
if (responseRv.getStatus() != 0) {
// message
showAlert(responseRv.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// we note that the appointment has been cancelled
rdvSupprimé = true;
// the most recent agenda is requested
executeInBackground(
mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// we consume the answer
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// diary consumption
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// mistake?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// put the agenda in the session
session.setAgenda(responseAgendaMedecinJour.getBody());
// update the view's agenda
updateAgenda();
}
- linha 4: notificamos a classe pai de que vamos iniciar duas tarefas assíncronas e começamos a aguardar a conclusão dessas duas tarefas;
- linha 8: recuperamos o ID do compromisso a ser eliminado. O servidor necessita desta informação;
- linhas 9–18: solicitamos a eliminação do compromisso através de uma tarefa assíncrona;
- linha 10: o método [executeInBackground] espera dois parâmetros:
- linha 10: o processo a ser executado e observado é fornecido pelo método [mainActivity.deleteRv(idRv)];
- linhas 10–17: o segundo parâmetro é uma instância do tipo [Action1<T>], onde T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
- linha 15: quando a resposta é recebida, é passada para o método [consumeRv] na linha 21;
- linha 10: o método [executeInBackground] espera dois parâmetros:
- linhas 21–44: recebemos a resposta da tarefa assíncrona. Processamo-la;
- linhas 23–30: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
- linha 25: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 27: cancelamos todas as tarefas;
- linha 29: regressamos à interface do utilizador;
- linha 32: se não houve erro, indicamos que a consulta foi eliminada;
- linhas 34–43: em vez de simplesmente eliminar o compromisso do calendário atualmente exibido pelo fragmento, solicitamos o novo calendário do médico. Uma vez que a aplicação é multiutilizador, outros utilizadores também podem ter alterado o calendário do médico. Por isso, é melhor utilizar a versão mais recente;
- linhas 34–43, 47–61: repetimos o que foi feito no fragmento [AccueilFragment], desta vez utilizando informações recuperadas da sessão;
O método [beginWaiting] (linha 4) é o seguinte:
// beginning of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch tasks
beginRunningTasks(numberOfRunningTasks);
// status of buttons and menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
- linha 6: todas as opções do menu estão ocultas;
- linha 7: em seguida, tornamos a opção [Ações/Cancelar] visível;
3.6.8.2.3. Método [doCancel]
O clique na opção de menu [Cancel] é tratado pelo método [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- linha 7: solicitamos à classe pai que cancele as tarefas assíncronas;
3.6.8.2.4. Opção de menu [Voltar à configuração]
Ao clicar na opção de menu [Voltar à configuração], o seguinte ocorre:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- Linha 4: Navegamos para a vista de configuração utilizando a ação [NAVIGATION]. Isto significa que queremos restaurar a vista de configuração para o estado em que a deixámos;
3.6.8.2.5. Opção de menu [Voltar à página inicial]
Clicar na opção de menu [Voltar à página inicial] é tratado de forma semelhante:
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// navigate to home view
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
3.6.8.3. Gestão do ciclo de vida do fragmento
O fragmento tem o seguinte estado [AgendaFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
public class AgendaFragmentState extends CoreState {
// title view
private String titre;
// ListView
private int firstPosition;
private int top;
// manufacturers
public AgendaFragmentState() {
}
public AgendaFragmentState(String titre) {
this.titre = titre;
}
// getters and setters
...
}
- linha 10: o título exibido na parte superior da vista;
- linhas 12-13: permite a rolagem da ListView que exibe os horários disponíveis do médico;
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementation methods parent class ------------------------------------------------------
@Override
public CoreState saveFragment() {
// save status
AgendaFragmentState state = new AgendaFragmentState();
state.setTitre(txtTitre2.getText().toString());
// note the scroll position to return to it
// read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of 1st element fully visible or not
firstPosition = lstCreneaux.getFirstVisiblePosition();
// y offset of this element relative to the top of the ListView
// measures the height of any hidden part
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// we memorize it all
state.setTop(top);
state.setFirstPosition(firstPosition);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AGENDA;
}
@Override
protected void initFragment(CoreState previousState) {
// 1st visit?
if (previousState != null) {
// not the 1st visit
AgendaFragmentState state = (AgendaFragmentState) previousState;
// and information from ListView
firstPosition = state.getFirstPosition();
top = state.getTop();
}
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// get the agenda
agenda = session.getAgenda();
// generate the page title
Medecin medecin = agenda.getMedecin();
txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
medecin.getNom(), session.getJourRv()));
// menu status
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// regenerate the page title
AgendaFragmentState state = (AgendaFragmentState) previousState;
txtTitre2.setText(state.getTitre());
}
@Override
protected void notifyEndOfUpdates() {
// regenerate the slot list
updateAgenda();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu status
initMenu();
// if cancelled but appointment deleted, update local calendar
if (runningTasksHaveBeenCanceled && rdvSupprimé) {
// we delete the appointment from the local calendar (we were unable to access the global calendar)
agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
// update the visual interface
updateAgenda();
}
}
// méthodes privées ------------------------------------------------
private void initMenu() {
// menu status
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2–19: quando solicitado pela sua classe pai, o fragmento guarda o estado dos seguintes elementos:
- linha 6: o título exibido na parte superior da vista;
- linhas 7–17: as informações (top, firstPosition) que permitirão restaurar a rolagem da ListView;
- linhas 21–24: o ID do fragmento é [IMainActivity.VUE_AGENDA];
- linhas 26–35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em visitas subsequentes (previousState != null);
- linhas 30–34: se esta não for a primeira visita ao fragmento, recuperamos as informações (top, firstPosition) necessárias para restaurar o estado de rolagem do ListView;
- linhas 38–40: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída em visitas subsequentes (previousState != null). Não há nada a fazer aqui, porque a ListView dos slots será gerada pelo método privado [updateAgenda] (linhas 61–65);
- linhas 42–52: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Estamos a vir da vista [HOME];
- linha 45: recuperamos a agenda definida por [AccueilFragment];
- linhas 47–49: o título da vista é gerado;
- a ListView dos intervalos de tempo será gerada pelo método privado [updateAgenda] (linhas 61-65);
- linhas 54–59: executadas quando o fragmento é acedido através de uma operação [NAVIGATION] ou [RESTORE];
- linhas 57-58: o título da vista é regenerado;
- a ListView dos intervalos de tempo será gerada pelo método privado [updateAgenda] (linhas 61–65);
- linhas 72–74: executadas quando todas as atualizações anteriores tiverem sido concluídas. A ListView dos intervalos de tempo é atualizada porque esta atualização é necessária independentemente da forma como o fragmento é acedido;
- linhas 67–77: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 70: o menu é reposto no seu estado padrão (linhas 82–86);
- linha 72: havia duas tarefas assíncronas. Verificamos se a primeira (eliminar o compromisso) foi bem-sucedida, apesar de um cancelamento;
- linha 74: se sim, o compromisso é eliminado do calendário local
- linha 75: e atualizamos a exibição do calendário;
3.6.9. Tratamento da vista de adição de compromisso
3.6.9.1. A vista
A tela para adicionar um compromisso é a seguinte:

Os elementos da interface visual são os seguintes:
3.6.9.2. O fragmento
A visualização para adicionar um compromisso é gerida pelo seguinte fragmento [AjoutRvFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {
// visual interface elements
@ViewById(R.id.spinnerClients)
protected Spinner spinnerClients;
@ViewById(R.id.txt_titre2_ajoutRv)
protected TextView txtTitre2;
// our customers
private List<Client> clients;
// local data
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementation methods parent class ----------------------------------
...
}
- linha 26: o fragmento está associado ao seguinte menu [menu_ajout_rv]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
<item
android:id="@+id/navigationToAgenda"
android:title="@string/navigationToAgenda"/>
</menu>
</item>
</menu>
- linhas 30–33: os elementos da interface visual;
- linha 36: a lista de clientes;
- linha 43: a fonte de dados para o spinner do cliente;
O clique no link [Validate] é tratado pelo seguinte método [doValidate]:
// our customers
private List<Client> clients;
// local data
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
...
// validation page
@OptionsItem(R.id.actionValider)
protected void doValider() {
// the selected customer is retrieved
Client client = clients.get(spinnerClients.getSelectedItemPosition());
// start waiting for 2 asynchronous tasks
beginWaiting(2);
// we add the RV
rdvAjouté = false;
executeInBackground(
mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// we consume the answer
consumeRv(responseRv);
}
});
}
// consumption of a Response<Rv> object
void consumeRv(Response<Rv> responseRv) {
// mistake?
if (responseRv.getStatus() != 0) {
// message
showAlert(responseRv.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// note that the rdv has been added
rdvAjouté = true;
// memorize the appointment
this.rv = responseRv.getBody();
// we ask for the new agenda
executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// we consume the answer
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// consumption of a Response<AgendaMedecinJour> object
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// mistake?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancellation
doAnnuler();
// back to UI
return;
}
// put the agenda in the session
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- linha 13: quando o método [doValider] começa, os campos 2, 5, 6 e 9 já foram inicializados durante o ciclo de vida do fragmento. Vamos ver como;
- linha 15: recuperamos a entidade [Client] correspondente ao elemento selecionado no spinner do cliente;
- linha 17: notificamos a classe pai de que vamos lançar duas tarefas assíncronas e nos preparamos para a espera;
- linha 19: inicialmente, a consulta ainda não foi adicionada ao calendário do médico;
- linhas 20–30: solicitamos que o servidor adicione uma consulta;
- linha 20: o método [executeInBackground] espera dois parâmetros:
- linha 20: o processo a ser executado e observado é fornecido pelo método [mainActivity.addRv(session.getDayRv(), slot.getId(), client.getId())];
- linhas 22–29: o segundo parâmetro é uma instância do tipo [Action1<T>], em que T é o tipo devolvido pelo processo observado, neste caso [Response<Rv>]
- linha 27: quando a resposta é recebida, é passada para o método [consumeRV] na linha 33;
- linha 20: o método [executeInBackground] espera dois parâmetros:
- linhas 33–56: recebemos a resposta do servidor. Processamo-la;
- linhas 35–42: primeiro, verificamos se o servidor reportou um erro no campo [status] da resposta;
- linha 37: se houver um erro, exibimos as mensagens que o servidor colocou no campo [messages] da resposta;
- linha 39: cancelamos todas as tarefas;
- linha 41 : voltamos à interface do utilizador;
- linha 44: se não houve erro, indicamos que o compromisso foi adicionado;
- linha 46: o compromisso adicionado é armazenado num campo do fragmento;
- linhas 47–55: tal como foi feito ao eliminar um compromisso, após adicionar o compromisso, solicitamos a agenda mais recente do médico ao servidor;
- linhas 47–56, 59–71: este código já foi encontrado várias vezes anteriormente;
O método [beginWaiting] (linha 17) é o seguinte:
// beginning of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch tasks
beginRunningTasks(numberOfRunningTasks);
// status of buttons and menus
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- linha 4: informamos à tarefa pai que vamos iniciar [numberOfRunningTasks] tarefas;
- linha 6: todas as opções do menu são ocultadas;
- linha 7: em seguida, torna a opção [Ações/Cancelar] visível;
O clique na opção de menu [Cancelar] é tratado pelo método [doCancel]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- linha 7: solicitamos à classe pai que cancele as tarefas assíncronas;
A navegação para trás é tratada pelos três métodos seguintes:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAgenda)
protected void navigationToAgenda() {
// navigate to the calendar view
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}
3.6.9.3. Gestão do ciclo de vida do fragmento
O fragmento tem o seguinte estado [AjoutRvFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
// fragment status AjoutRvFragment
public class AjoutRvFragmentState extends CoreState {
// selected customer position
private int selectedClientPosition;
// title view
private String titre;
// customer spinner data source
private String[] spinnerClientsDataSource;
// getters and setters
...
}
O ciclo de vida do fragmento é implementado da seguinte forma:
// implementation methods parent class ----------------------------------
@Override
public CoreState saveFragment() {
// save view
AjoutRvFragmentState state = new AjoutRvFragmentState();
state.setTitre(txtTitre2.getText().toString());
state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
state.setSpinnerClientsDataSource(spinnerClientsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AJOUT_RV;
}
@Override
protected void initFragment(CoreState previousState) {
// retrieve clients in session
clients = session.getClients();
// 1st visit?
if (previousState == null) {
// we build the table displayed by the spinner
spinnerClientsDataSource = new String[clients.size()];
int i = 0;
for (Client client : clients) {
spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
i++;
}
} else {
// no 1st visit
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
spinnerClientsDataSource = state.getSpinnerClientsDataSource();
}
}
@Override
protected void initView(CoreState previousState) {
// association spinner to its data source
ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
spinnerClientsDataSource);
dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerClients.setAdapter(dataAdapterClients);
// 1st visit?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// retrieve the number of the slot to be reserved in the session
int position = session.getPosition();
// the doctor's agenda is retrieved from the session
AgendaMedecinJour agenda = session.getAgenda();
// we get the doctor and the time slot we're going to schedule an appointment for
medecin = agenda.getMedecin();
creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
// build page title 2
String jour = session.getJourRv();
txtTitre2.setText(String.format(Locale.FRANCE,
"Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
creneau.getMfin()));
// customer selection
spinnerClients.setSelection(0);
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore previous state
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
// title
txtTitre2.setText(state.getTitre());
// spinner
spinnerClients.setSelection(state.getSelectedClientPosition());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu status
initMenu();
// next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
// there has been a cancellation - appointment already added?
if (rdvAjouté) {
// we modify the local agenda (we didn't get the global agenda)
AgendaMedecinJour agenda = session.getAgenda();
agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
// the agenda is displayed
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
}
// private methods -------------------
private void initMenu() {
// menu status
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- linhas 2–10: quando solicitado pela sua classe pai, o fragmento guarda o estado dos seguintes elementos:
- linha 6: o título na parte superior da vista;
- linha 7: a posição do item selecionado no spinner do cliente;
- linha 8: a fonte de dados do spinner do cliente;
- linhas 12–15: o ID do fragmento é [IMainActivity.VUE_AJOUT_RV];
- linhas 17–35: executadas quando o fragmento é gerado pela primeira vez (previousState == null) ou regenerado em ocasiões subsequentes (previousState != null);
- linha 20: a lista de clientes é recuperada da sessão e colocada num campo do fragmento;
- linhas 22–30: para uma primeira visita, a fonte de dados para o spinner do cliente é construída;
- linhas 32–33: para visitas subsequentes, a fonte de dados para o spinner de clientes é recuperada do estado anterior do fragmento;
- linhas 37–49: executadas quando a vista associada ao fragmento é construída pela primeira vez (previousState == null) ou reconstruída em ocasiões subsequentes (previousState != null);
- linhas 40–43: em todos os casos, o spinner do cliente é associado à sua fonte de dados;
- linhas 45–48: na primeira visita, o menu é apresentado sem a ação [Cancel] (linhas 107–111);
- linhas 51-70: executadas quando o fragmento é acedido através de uma operação [SUBMIT]. Estamos a vir da vista [CALENDAR];
- linha 54: recuperamos o número do horário em que agendaremos uma consulta;
- linhas 56–59: recuperamos as entidades [Médico] e [Intervalo de tempo] necessárias para adicionar esta consulta e colocamo-las nos campos dentro do fragmento;
- linhas 61–65: utilizando esta informação, podemos construir o título da vista;
- linha 67: o spinner do cliente é definido para o seu primeiro item;
- linha 69: o menu é definido para o seu estado inicial (sem a opção [Cancelar]);
- linhas 72-80: executadas quando se chega ao fragmento através de uma operação [NAVIGATION] ou [RESTORE];
- linha 77: o título da vista é regenerado;
- linha 79: o indicador de carregamento do cliente é reiniciado para o último cliente selecionado;
- linhas 82–84: executadas quando todas as atualizações anteriores tiverem sido concluídas. Não há mais nada a fazer aqui;
- linhas 86–104: executadas quando todas as tarefas assíncronas estiverem concluídas;
- linha 89: o menu é reposto no seu estado padrão;
- linhas 91–94: se as tarefas foram concluídas normalmente, regressa à vista [CALENDÁRIO] através de um [ENVIAR] (aqui, isto também poderia ter sido uma ação de NAVEGAÇÃO);
- linhas 96–103: se as tarefas terminaram com um cancelamento, verificamos ainda se o compromisso foi adicionado (isto significaria que a recuperação do novo calendário falhou);
- linhas 98-99: se o compromisso tiver sido adicionado;
- linhas 98-99: o compromisso devolvido pelo servidor é adicionado ao calendário atual, aquele que está ativo;
- linha 101: regressamos à vista [AGENDA] através de um [SUBMIT] (aqui, isto também poderia ter sido uma ação do tipo NAVEGAÇÃO);
3.7. Execução
Realize os seguintes testes:
- utilize a aplicação em condições normais e verifique se funciona;
- Gire o dispositivo para cada vista e verifique se cada uma é restaurada corretamente;
- Adicione uma espera de alguns segundos em [IMainActivity];
- Em seguida, cancele as tarefas e verifique se o resultado corresponde ao esperado;
- Gire o dispositivo durante os períodos de espera e verifique se as tarefas são canceladas corretamente e se não ocorrem falhas;
- Altere a ordem dos fragmentos em [IMainActivity] e verifique se a aplicação continua a funcionar;












































