3. Caso di studio - Gestione degli appuntamenti
3.1. Il progetto
Nel documento [Tutoriel AngularJS / Spring 4] è stata sviluppata un'applicazione client/server per la gestione degli appuntamenti medici. Di seguito faremo riferimento a questo documento con il codice [rdvmedecins-angular]. L'applicazione prevedeva due tipi di client:
- un client HTML / CSS / JS;
- un client Android;
Il client Android veniva ottenuto automaticamente dalla versione HTML del client tramite lo strumento [Cordova]. L’obiettivo di questo progetto sarà ricreare manualmente questo client Android utilizzando le conoscenze acquisite nei capitoli precedenti.
Va sottolineata un'importante differenza tra le due soluzioni:
- quella che creeremo sarà utilizzabile solo sui tablet Android;
- nella versione [rdvmedecins-angular], il client web mobile (HTML / CSS / JS) è utilizzabile su qualsiasi piattaforma (Android, IoS, Windows);
3.2. Le schermate del client Android
Sono disponibili quattro schermate.
Schermata di configurazione

Schermata di selezione del medico e della data dell’appuntamento

Schermata di selezione della fascia oraria dell’appuntamento

Schermata relativa alla scelta del cliente per l’appuntamento

3.3. L'architettura del progetto
Si avrà un'architettura client/server analoga a quella dell'esempio [Exemple-15] (cfr. paragrafo 1.16) del presente documento:

Gli scambi asincroni tra client e server saranno gestiti tramite la libreria RxAndroid.
3.4. Il database
Non riveste un ruolo fondamentale in questo documento. La riportiamo a titolo informativo. La chiameremo [dbrdvmedecins] . Si tratta di un database MySQL5 con quattro tabelle:
![]() |
3.4.1. La tabella [MEDECINS]
Contiene informazioni sui medici gestiti dall’applicazione [RdvMedecins].
![]() | ![]() |
- ID: numero identificativo del medico - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- NOM: il cognome del medico
- PRENOM: il suo nome
- TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)
3.4.2. La tabella [CLIENTS]
I pazienti dei diversi medici sono registrati nella tabella [CLIENTS]:
![]() | ![]() |
- ID: numero identificativo del cliente - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- NOM: il nome del cliente
- PRENOM: il suo nome
- TITRE: titolo (Signorina, Signora, Signor)
3.4.3. La tabella [CRENEAUX]
Elenca le fasce orarie in cui sono possibili i RV:
![]() |
![]() | ![]() | ![]() |
- ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
- VERSION: numero identificativo della versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
- HDEBUT: ora di inizio della fascia oraria
- MDEBUT: minuti di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [CRENEAUX] (cfr. [1] sopra) indica, ad esempio, che la fascia n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (la dott.ssa Marie PELISSIER).
3.4.4. La tabella [RV]
Elenca i RV assegnati a ciascun medico:
![]() | ![]() |
- ID: numero che identifica in modo univoco il RV – chiave primaria
- JOUR: giorno del RV
- ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
- ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]
Questa tabella presenta un e vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):
Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può comparire in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.
La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.
3.4.5. Generazione del database
Per creare le tabelle e compilarle è possibile utilizzare lo script [dbrdvmedecins.sql], che si trova nell’archivio degli esempi |ICI|.
![]() |
Con [WampServer] (cfr. paragrafo 6.15), si potrà procedere come segue:
![]() | ![]() |
- in [1], cliccare sull'icona di [WampServer] e selezionare l'opzione [PhpMyAdmin] [2],
- in [3], nella finestra che si è aperta, selezionare il link [Bases de données],
![]() |
- in [4-6], si importa un file SQL,
![]() | ![]() | ![]() |
- in [7], si seleziona lo script SQL e in [8] lo si esegue,
- in [9], le tabelle del database sono state create. Si segue uno dei link,
![]() |
- in [10], il contenuto della tabella.
In seguito, non torneremo più su questo database, ma il lettore è invitato a seguirne l’evoluzione nel corso dei test, soprattutto quando l’applicazione non funziona.
3.5. Il server web / jSON

Qui ci concentriamo sul server [1]. Non lo svilupperemo ulteriormente. È stato descritto in dettaglio nel documento [Spring MVC et Thymeleaf par l'exemple]. Il lettore interessato potrà consultarlo. È stato sviluppato come quello del server dell’esempio 15. Il suo codice sorgente è fornito negli esempi. Qui utilizzeremo il suo file binario:
![]() |
- [rdvmedecins-server-all-1.0.jar] è il file binario del server;
3.5.1. Implementazione
In una finestra di comando, ci si posiziona nella cartella contenente il file binario del server:
...\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
quindi, per avviare il server, digitare il seguente comando (SGBD e MySQL devono essere già in esecuzione):
...\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
Il server visualizza numerosi log. Di seguito abbiamo riportato solo quelli utili alla comprensione:
- righe 14-18: viene avviato un server Tomcat integrato sulla porta 8080 della macchina. È questo server che esegue l’applicazione web di gestione degli appuntamenti. Questa applicazione è in realtà un servizio web / jSON: viene interrogata tramite URL e risponde inviando una stringa jSON;
- riga 24: il servizio web è protetto con il framework [Spring Security]. Si accede alle URL del servizio web effettuando l’autenticazione;
- righe 29-44: i URL esposti dal servizio web;
Le analizzeremo nel dettaglio.
3.5.2. Sicurezza del servizio web
I URL esposti dal servizio web sono protetti. Il server si aspetta che la richiesta HTTP del client contenga la seguente intestazione:
Il codice atteso è la codifica in base64 [http://fr.wikipedia.org/wiki/Base64] della stringa 'utente:password'. Il servizio web, nella sua configurazione iniziale, accetta solo un utente 'admin' con la password 'admin'. L'intestazione sopra indicata diventa, per questo utente specifico, la seguente riga:
Per poter inviare questa intestazione HTTP, utilizziamo il client HTTP [Advanced Rest Client], che è un plugin del browser Chrome (cfr. paragrafo 6.13). Testeremo manualmente i diversi URL esposti dal servizio web per comprendere:
- i parametri previsti dall'URL;
- la natura esatta della sua risposta;
3.5.3. Elenco dei medici
L'URL [/getAllMedecins] consente di ottenere l'elenco dei medici:
![]() |
- in [1], l'interrogazione URL;
- in [2], il metodo HTTP utilizzato per questa interrogazione;
- in [3], l'intestazione di sicurezza dell'utente (admin, admin) HTTP;
- in [4], si invia la richiesta HTTP;
La risposta del server è la seguente:
![]() |
- in [5], la risposta jSON del server, formattata;
![]() |
- in [6], la stessa risposta allo stato grezzo;
Il formato [5] consente di vedere meglio la struttura della risposta. Tutte le risposte del servizio web sono un'istanza della seguente classe [Response]:
package rdvmedecins.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
- riga 9: lo stato della risposta. Il valore 0 indica che non si è verificato alcun errore, altrimenti si è verificato un errore;
- riga 11: un elenco di messaggi di errore, se si è verificato un errore;
- riga 13: la risposta effettivamente attesa dal client;
La risposta a URL [/getAllMedecins] è la stringa jSON di un oggetto di tipo [Response<List<Medecin>>]. La classe [Medecin] è la seguente:
package rdvmedecins.android.dao.entities;
public class Medecin extends Personne {
// costruttore predefinito
public Medecin() {
}
// costruttore con parametri
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
Riga 3: la classe [Medecin] estende la seguente classe [Personne]:
package rdvmedecins.android.dao.entities;
public class Personne extends AbstractEntity {
// attributi di una persona
private String titre;
private String nom;
private String prenom;
// costruttore predefinito
public Personne() {
}
// costruttore con parametri
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);
}
// getter e setter
...
}
Riga 3, la classe [Personne] estende la seguente 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;
}
// inizializzazione
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;
}
// getter e setter
...
}
In definitiva, la struttura di un oggetto [Medecin] è la seguente:
[Long id; Long version; String titre; String nom; String prenom;]
e quella di [Response<List<Medecin>>] è la seguente:
D'ora in poi useremo queste definizioni abbreviate per descrivere la risposta del server. Inoltre, per un po' non mostreremo più screenshot. Basta ripetere ciò che abbiamo appena visto. Torneremo agli screenshot quando sarà necessario effettuare una richiesta POST. Presenteremo inoltre un esempio di esecuzione nella forma seguente:
3.5.4. Elenco dei clienti
| |
|
Esempio:
3.5.5. Elenco degli orari di un medico
|
- [idMedecin]: identificativo del medico di cui si desiderano conoscere gli orari di visita;
- [hdebut]: ora di inizio della visita;
- [mdebut]: minuti di inizio della visita;
- [hfin]: ora di fine della visita;
- [mfin]: minuti di fine della visita;
Per una fascia oraria compresa tra le 10:20 e le 10:40 si avrà [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].
Esempio:
3.5.6. Elenco degli appuntamenti di un medico
|
- [idMedecin]: identificativo del medico di cui si desiderano gli appuntamenti;
- URL [jour]: giorno degli appuntamenti nel formato 'aaaa-mm-gg';
- Risposta [jour]: come sopra, ma nel formato di una data Java;
- [client]: il cliente dell'appuntamento. La sua struttura è stata descritta in precedenza;
- [idClient]: l'identificativo del cliente;
- [creneau]: la fascia oraria dell’appuntamento. La sua struttura è stata descritta in precedenza;
- [idCreneau]: l'identificativo della fascia oraria;
Esempio:
3.5.7. L'agenda di un medico
|
- [idMedecin]: identificativo del medico di cui si desiderano gli appuntamenti;
- URL [jour]: giorno degli appuntamenti nel formato 'aaaa-mm-gg';
- [agenda]: agenda del medico;
- [medecin]: il medico in questione. La sua struttura è stata definita in precedenza;
- Risposta [jour]: il giorno dell'agenda nel formato di una data Java;
- [creneauxMedecinJour]: un array di elementi di tipo [CreneauMedecinJour];
- [creneau]: una fascia oraria. La sua struttura è stata descritta in precedenza;
- [rv]: un appuntamento. La sua struttura è stata descritta in precedenza;
Esempio:
|
Sono stati evidenziati i casi in cui è presente un appuntamento nella fascia oraria e quelli in cui non ce n'è.
3.5.8. Ricerca di un medico tramite il suo identificativo
|
- [idMedecin]: l'identificativo del medico;
Esempio 1:
Esempio 2:
3.5.9. Recuperare un cliente tramite il suo ID
|
- [idClient]: l'identificativo del cliente;
Esempio 1:
Esempio 2:
3.5.10. Ottenere una fascia oraria tramite il relativo identificativo
|
- [idCreneau]: l'ID della fascia oraria;
Esempio 1:
Si noti che nella risposta non compare il nome del medico titolare della fascia oraria, ma solo il suo identificativo.
Esempio 2:
3.5.11. Fissare un appuntamento tramite il proprio ID
|
- [idRv]: l'ID dell'appuntamento;
Esempio 1:
Si noti che nella risposta non compaiono né il cliente né la fascia oraria dell'appuntamento, ma solo i relativi identificativi.
Esempio 2:
3.5.12. Aggiungi un appuntamento
Il codice URL [/ajouterRv] consente di aggiungere un appuntamento. Le informazioni necessarie per l'aggiunta (giorno, fascia oraria e cliente) vengono trasmesse tramite una richiesta HTTP POST. Mostriamo come eseguire questa richiesta con lo strumento [Advanced Rest Client].

- in [1], viene interrogata la URL;
- in [2], viene interrogata da un POST;
- in [3-4], si specifica al server che i valori inviati sono sotto forma di stringa jSON;
- in [4], l’intestazione HTTP dell’autenticazione;
- in [5], le informazioni trasmesse da POST. Si tratta di una stringa jSON contenente:
- [jour]: il giorno dell'appuntamento nel formato 'aaaa-mm-gg',
- [idClient]: l’identificativo del cliente per il quale è stato fissato l’appuntamento,
- [idCreneau]: l'identificativo della fascia oraria dell'appuntamento. Poiché una fascia oraria appartiene a un medico specifico, con questo si indica anche il medico;
- in [6], si invia la richiesta;
La stringa jSON che viene inviata è quella dell’oggetto di tipo [PostAjouterRv] seguente:
public class PostAjouterRv {
// dati del post
private String jour;
private long idClient;
private long idCreneau;
// costruttori
public PostAjouterRv() {
}
public PostAjouterRv(String jour, long idCreneau, long idClient) {
this.jour = jour;
this.idClient = idClient;
this.idCreneau = idCreneau;
}
// getter e setter
...
}
La risposta del server è di tipo [Response<Rv>] [int status; List<String> messages; Rv rv], dove [rv] è l'appuntamento aggiunto.
La risposta del server alla richiesta sopra riportata è la seguente:
![]() |
Si noti che alcune informazioni non sono riportate in [idClient, idCreneau], ma si trovano nei campi [client] e [creneau]. L'informazione importante è l'identificativo dell'appuntamento aggiunto (209). Il servizio web avrebbe potuto limitarsi a restituire solo questa informazione.
3.5.13. Eliminare un appuntamento
Anche questa operazione viene eseguita tramite un POST:
|
Il valore inserito è la stringa jSON di un oggetto di tipo [PostSupprimerRv] come segue:
public class PostSupprimerRv {
// dati del post
private long idRv;
// costruttori
public PostSupprimerRv() {
}
public PostSupprimerRv(long idRv) {
this.idRv = idRv;
}
// getter e setter
...
}
- riga 4, [idRv] è l'identificativo dell'appuntamento da eliminare.
Esempio 1:
L'appuntamento n. 209 è stato effettivamente cancellato perché [status=0].
Esempio 2:
3.6. L'app Android

Ora che il server [1] è stato descritto in dettaglio ed è operativo, esamineremo il client Android [2].
3.6.1. Architettura del progetto Android Studio
Il progetto riprende l'architettura del progetto [client-android-skel] (cfr. paragrafo 1.17). Nell'architettura sopra riportata del client Android si distinguono tre blocchi:
- il livello [DAO], responsabile della comunicazione con il servizio web;
- i [vues] incaricati della comunicazione con l’utente;
- il [activité] che funge da collegamento tra i due blocchi precedenti. Le viste non sono a conoscenza del livello [DAO]. Comunicano esclusivamente con l’attività.
Questa architettura si riflette in quella del progetto Android Studio del client Android:
![]() |
- il pacchetto [activity] implementa l’attività;
- il pacchetto [architecture] riprende gli elementi di architettura che abbiamo sviluppato in precedenza;
- il pacchetto [dao] implementa il livello [DAO];
- il pacchetto [fragments] implementa il pacchetto [vues];
3.6.2. Personalizzazione del progetto
![]() |
La cartella [architecture / custom] contiene gli elementi personalizzabili dell'architettura.
L'interfaccia [IMainActivity] è la seguente:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// accesso alla sessione
ISession getSession();
// cambio di vista
void navigateToView(int position, ISession.Action action);
// gestione delle attese
void beginWaiting();
void cancelWaiting();
// costanti dell'applicazione -------------------------------------
// modalità debug
boolean IS_DEBUG_ENABLED = true;
// tempo massimo di attesa per la risposta del server
int TIMEOUT = 1000;
// tempo di attesa prima dell'esecuzione della richiesta del client
int DELAY = 000;
// autenticazione di base
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
// adiacenza dei frammenti
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra delle schede
boolean ARE_TABS_NEEDED = false;
// immagine di attesa
boolean IS_WAITING_ICON_NEEDED = true;
// numero di frammenti dell'applicazione
int FRAGMENTS_COUNT = 4;
// numero di visualizzazioni
int VUE_CONFIG = 0;
int VUE_ACCUEIL = 1;
int VUE_AGENDA = 2;
int VUE_AJOUT_RV = 3;
}
- righe 25, 28: personalizzazione del livello [DAO];
- riga 31: questa applicazione effettua accessi autenticati al server;
- riga 40: è necessaria un'immagine di attesa;
- riga 43: l'applicazione ha quattro frammenti;
- righe 46-49: i numeri dei quattro frammenti;
- riga 37: non ci sono schede;
La classe base [CoreState] degli stati dei frammenti sarà la seguente:
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 {
// frammento visitato o meno
protected boolean hasBeenVisited = false;
// stato dell'eventuale menu del frammento
protected MenuItemState[] menuOptionsState;
// getter e setter
...
}
- righe 15-18: i quattro frammenti hanno uno stato:
![]() |
Infine, la sessione contiene i dati condivisi tra i frammenti:
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 {
// gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
// elenco dei medici
private List<Medecin> médecins;
// elenco dei clienti
private List<Client> clients;
// agenda di un medico per un determinato giorno
private AgendaMedecinJour agenda;
// posizione dell’elemento cliccato nell’agenda
private int position;
// giorno dell'appuntamento nel formato inglese "yyyy-MM-dd"
private String dayRv;
// giorno dell'appuntamento nel formato francese "dd-MM-yyyy"
private String jourRv;
// getter e setter
...
}
- righe 17-28: la sessione memorizza sei informazioni. Spiegheremo il ruolo di queste informazioni quando sarà necessario.
3.6.3. Lo strato [DAO]
![]() |
![]() | ![]() |
- in [1], le entità incapsulate nelle risposte del server. Sono state illustrate nel paragrafo 3.5;
- in [2], gli elementi del client che gestiscono gli scambi con il server;
Non torneremo sugli elementi [1]. Sono già stati presentati. Il lettore è invitato a tornare al paragrafo 3.5 se necessario. Esamineremo l’implementazione del pacchetto [service]. Questo ci porterà a parlare anche dell’implementazione degli scambi sicuri tra client e server.
3.6.3.1. Implementazione degli scambi client/server
![]() |
La classe [WebClient] è un componente di AA che descrive:
- i URL esposti dal servizio web;
- i relativi parametri;
- le relative risposte;
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);
// elenco dei medici
@Get("/getAllMedecins")
public Response<List<Medecin>> getAllMedecins();
// elenco dei clienti
@Get("/getAllClients")
public Response<List<Client>> getAllClients();
// elenco delle fasce orarie di un medico
@Get("/getAllCreneaux/{idMedecin}")
public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
// elenco degli appuntamenti di un medico
@Get("/getRvMedecinJour/{idMedecin}/{jour}")
public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
// Cliente
@Get("/getClientById/{id}")
public Response<Client> getClientById(@Path long id);
// Medico
@Get("/getMedecinById/{id}")
public Response<Medecin> getMedecinById(@Path long id);
// Appuntamento
@Get("/getRvById/{id}")
public Response<Rv> getRvById(@Path long id);
// Fascia oraria
@Get("/getCreneauById/{id}")
public Response<Creneau> getCreneauById(@Path long id);
// aggiungere un RV
@Post("/ajouterRv")
public Response<Rv> ajouterRv(@Body PostAjouterRv post);
// eliminare un appuntamento
@Post("/supprimerRv")
public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
// Ottenere l'agenda di un medico
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
}
- righe 19-60: sono presenti tutte le URL esaminate nel paragrafo 3.5;
- riga 16: il componente [RestTemplate] di [Spring Android] su cui si basa la comunicazione client/server;
3.6.3.2. L'interfaccia [IDao]
![]() |
L'interfaccia [IDao] del livello [DAO] è la seguente:
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import rx.Observable;
import java.util.List;
public interface IDao {
// URL del servizio web
public void setUrlServiceWebJson(String url);
// utente
public void setUser(String user, String mdp);
// timeout del client
public void setTimeout(int timeout);
// elenco dei clienti
public Observable<List<Client>> getAllClients();
// elenco dei medici
public Observable<List<Medecin>> getAllMedecins();
// elenco delle fasce orarie di un medico
public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
// elenco degli appuntamenti di un medico in un determinato giorno
public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
// trovare un cliente identificato dal suo ID
public Observable<Client> getClientById(long id);
// trovare un medico identificato dal suo ID
public Observable<Medecin> getMedecinById(long id);
// trovare un appuntamento identificato dal proprio ID
public Observable<Rv> getRvById(long id);
// trovare una fascia oraria identificata dal proprio ID
public Observable<Creneau> getCreneauById(long id);
// aggiungere un RV
public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
// eliminare un RV
public Observable<Rv> supprimerRv(long idRv);
// attività
public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
// modalità debug
void setDebugMode(boolean isDebugEnabled);
}
- riga 10: per impostare l’URL del servizio web / jSON;
- riga 13: per impostare l'utente della comunicazione client/server. [user] è l'ID utente, [mdp] la sua password;
- riga 16: per impostare un tempo massimo di attesa per la risposta del server;
- righe 18-49: a ogni URL esposto dal servizio web corrisponde un metodo. Essi riprendono la firma dei metodi con lo stesso nome del componente AA [WebClient];
- riga 52: per controllare la modalità debug del livello [DAO];
3.6.3.3. La classe [Dao]
![]() |
L'implementazione [DAO] dell'interfaccia [IDao] precedente è la seguente:
package client.android.dao.service;
import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// cliente del servizio web
@RestService
protected WebClient webClient;
// sicurezza
@Bean
protected MyAuthInterceptor authInterceptor;
// il RestTemplate
private RestTemplate restTemplate;
// factory di 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));
}
// intercettatore di autenticazione?
if (isBasicAuthentificationNeeded) {
// si aggiunge l'intercettatore di autenticazione
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// metodi privati -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// implementazione dell'interfaccia IDao --------------------------------------------------------------------
@Override
public Observable<Response<List<Client>>> getAllClients() {
// log
log("getAllClients");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
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");
// risultato
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
}
- righe 18-72: sono quelle presenti di base nella classe [Dao] del progetto [client-android-skel];
- righe 74-216: implementazione dell'interfaccia [IDao]. I metodi che interrogano i URL esposti dal servizio web delegano tale interrogazione al componente AA [WebClient] (righe 22-23);
- righe 58-63: se le comunicazioni client/server sono autenticate tramite un'autorizzazione di tipo basic, si aggiunge un intercettatore al componente [RestTemplate]. Ciò avrà come effetto che ogni richiesta HTTP emessa dal componente [RestTemplate] verrà intercettata dalla classe [MyAuthInterceptor] (righe 25-26);
La classe [MyAuthInterceptor] è la seguente:
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 {
// utente
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;
}
}
- riga 15: la classe [MyAuthInterceptor] è un componente AA di tipo [singleton];
- riga 16: la classe [MyAuthInterceptor] estende l'interfaccia Spring [ClientHttpRequestInterceptor]. Tale interfaccia presenta un metodo, il metodo [intercept] alla riga 22. Si estende questa interfaccia per intercettare qualsiasi richiesta HTTP proveniente dal client. Il metodo [intercept] riceve tre parametri;
- [HtpRequest request]: la richiesta HTTP intercettata,
- [byte[] body]: il suo corpo, se presente (ad esempio, valori inviati tramite POST),
- [ClientHttpRequestExecution execution]: il componente Spring che esegue la richiesta;
Intercettiamo tutte le richieste HTTP provenienti dal client Android per aggiungere l'intestazione di autenticazione HTTP descritta nel paragrafo 3.5.
- riga 23: recuperiamo le intestazioni HTTP della richiesta intercettata;
- riga 24: creiamo l'intestazione di autenticazione HTTP. La modalità di autenticazione utilizzata (codifica base64 della stringa 'user:mdp') è fornita dalla classe Spring [HttpBasicAuthentication];
- riga 25: l'intestazione di autenticazione appena creata viene aggiunta alle intestazioni attuali della richiesta intercettata;
- riga 26: si prosegue con l'esecuzione della richiesta intercettata. Riassumendo, la richiesta intercettata è stata arricchita con l'intestazione di autenticazione;
Le implementazioni dei metodi dell’interfaccia [IDao] seguono tutte lo stesso modello. Prendiamo ad esempio il metodo [getAgendaMedecinJour]:
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// risultato
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
- riga 2: il metodo richiede due parametri:
- [idMedecin]: l'identificativo del medico di cui si desidera l'agenda;
- [jour]: il giorno per il quale si desidera visualizzare l’agenda;
- riga 6: si chiama il metodo [getResponse] della classe padre [AbstractDao]. Questo metodo richiede un parametro di tipo [IRequest<T>], dove T è il tipo restituito dal metodo [getAgendaMedecinJour] alla riga 2, in questo caso [Response<AgendaMedecinJour>]. L’interfaccia [IRequest] ha un solo metodo: [getResponse] (riga 8);
- righe 8-10: implementazione del metodo [IRequest.getResponse]. Questo metodo deve restituire il risultato atteso dal metodo [getAgendaMedecinJour] alla riga 2, di tipo [Response<AgendaMedecinJour>];
- riga 9: la risposta viene restituita dal metodo [webClient.getAgendaMedecinJour]:
// ottenere l'agenda di un medico
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
I parametri utilizzati alla riga 9 sono quelli passati al metodo [getAgendaMedecinJour] alla riga 2. Per questo motivo, tali parametri devono avere l'attributo final;
3.6.4. L'attività [MainActivity]
Serveur ![]() |
![]() |
La classe [MainActivity] è la seguente:
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 {
// livello [DAO]
@Bean(Dao.class)
protected IDao dao;
// classe genitore ---------------------------------------
@Override
protected void onCreateActivity() {
// registro
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;
}
// interfaccia 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);
}
}
- righe 21-66: queste righe sono incluse di default nel modello [client-android-skel];
- righe 66-119: implementazione dell'interfaccia [IDao]. Tutti i metodi delegano il lavoro al livello [DAO] della riga 26;
- righe 42-46: il metodo [getFragments] restituisce l'array dei quattro frammenti dell'applicazione;
- righe 58-61: la vista di configurazione è la prima vista da visualizzare all’avvio dell’applicazione;
3.6.5. La sessione
![]() |
La classe [Session] serve a memorizzare le informazioni che devono essere trasmesse tra i frammenti. È la seguente:
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 {
// elenco dei medici
private List<Medecin> médecins;
// elenco dei clienti
private List<Client> clients;
// agenda
private AgendaMedecinJour agenda;
// posizione dell’elemento cliccato nell’agenda
private int position;
// giorno dell'appuntamento nel formato inglese "yyyy-MM-dd"
private String dayRv;
// giorno dell'appuntamento nel formato francese "dd-MM-yyyy"
private String jourRv;
// getter e setter
...
}
- riga 10: la classe [Session] è un componente AA istanziato in un unico esemplare;
- righe 12-15: in questo caso di studio si supporrà che gli elenchi dei medici e dei clienti non cambino. Verranno richiesti all’avvio dell’applicazione e memorizzati nella sessione affinché i frammenti possano utilizzarli;
- righe 20-23: il giorno desiderato per un appuntamento. Viene gestito in due formati: in notazione francese (riga 23) all’interno dell’app Android, in notazione inglese (riga 21) per gli scambi con il server;
- riga 19: la posizione dell’elemento cliccato (link “aggiungi”/“elimina”) sull’agenda;
3.6.6. Gestione della vista di configurazione
3.6.6.1. La vista
La vista di configurazione è la vista visualizzata all’avvio dell’applicazione:

Gli elementi dell'interfaccia visiva sono i seguenti:
3.6.6.2. Il frammento
La vista di configurazione è gestita dal seguente frammento [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 {
// gli elementi dell'interfaccia visiva
@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;
// i campi di immissione
private String urlServiceRest;
private String utilisateur;
private String mdp;
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
..
// implementazione dei metodi della classe padre -------------------------------------------
...
}
- riga 25: il frammento è associato al seguente 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>
- righe 28-38: gli elementi dell'interfaccia visiva;
- righe 41-43: i tre campi di immissione del modulo;
Il clic sull'opzione di menu [Valider] è gestito dal metodo [doValider]:
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
// si nascondono eventuali messaggi di errore precedenti
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
// si verifica la validità dei dati inseriti
if (!isPageValid()) {
return;
}
// si compila il campo URL del servizio web
mainActivity.setUrlServiceWebJson(urlServiceRest);
// si inseriscono i dati dell'utente
mainActivity.setUser(utilisateur, mdp);
// inizio dell'attesa - si avvieranno 2 attività asincrone
beginWaiting(2);
// medici
executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
@Override
public void call(Response<List<Medecin>> responseMedecins) {
// si elabora la risposta
consumeMedecins(responseMedecins);
}
});
// clienti
executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
@Override
public void call(Response<List<Client>> responseClients) {
// si elabora la risposta
consumeClients(responseClients);
}
});
}
private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
// log
if (isDebugEnabled) {
Log.d(className, "consume médecins");
}
// errore?
if (responseMedecins.getStatus() != 0) {
// messaggio
showAlert(responseMedecins.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// i medici vengono memorizzati nella sessione
session.setMédecins(responseMedecins.getBody());
}
private void consumeClients(Response<List<Client>> responseClients) {
// registro
if (isDebugEnabled) {
Log.d(className, "consume clients");
}
// errore?
if (responseClients.getStatus() != 0) {
// messaggio
showAlert(responseClients.getMessages());
// annullamento
doAnnuler();
// torna a UI
return;
}
// si memorizzano i clienti nella sessione
session.setClients(responseClients.getBody());
}
- righe 8-10: viene verificata la validità dei tre dati inseriti nel modulo. Se il modulo non è valido, non si procede oltre;
- righe 11-14: i dati necessari per il livello [DAO] vengono passati all’attività;
- riga 16: si comunica alla classe padre che verranno avviate due attività asincrone e si prepara l'attesa;
- righe 17-24: viene richiesta la lista dei medici;
- riga 18: il metodo [executeInBackground] richiede due parametri:
- riga 18: il processo da eseguire e osservare è fornito dal metodo [mainActivity.getAllMedecins()];
- righe 18-24: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<List<Medecin>>]
- riga 22: quando si riceve la risposta, questa viene passata al metodo [consumeMedecins] della riga 36;
- righe 25-33: dopo aver avviato un primo task asincrono, se ne avvia un secondo per richiedere l'elenco dei clienti. Si avranno quindi due task in esecuzione in parallelo;
- righe 36-52: abbiamo ricevuto la risposta dall’attività relativa ai medici. La elaboriamo;
- righe 42-49: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
- riga 44: se c’è un errore, visualizziamo i messaggi che il server ha inserito nel campo [messages] della risposta;
- riga 46: si annullano tutte le attività;
- riga 48: si torna all'interfaccia utente;
- riga 51: se non si è verificato alcun errore, l'elenco dei medici viene salvato nella sessione;
La validità dei dati inseriti (riga 8) viene verificata con il seguente metodo:
private boolean isPageValid() {
// si verifica la validità dei dati inseriti
boolean erreur;
URI service;
// validità del URL del servizio REST
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
try {
service = new URI(urlServiceRest);
erreur = service.getHost() == null || service.getPort() == -1;
} catch (Exception ex) {
// si registra l'errore
erreur = true;
}
if (erreur) {
// visualizzazione dell'errore
txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
}
// utente
utilisateur = edtUtilisateur.getText().toString().trim();
if (utilisateur.length() == 0) {
// viene visualizzato l'errore
txtErrorUtilisateur.setVisibility(View.VISIBLE);
// si rileva l'errore
erreur = true;
}
// password
mdp = edtMdp.getText().toString().trim();
// ritorno
return !erreur;
}
Il metodo [beginWaiting] (riga 16) è il seguente:
// inizio dell'attesa
protected void beginWaiting(int numberOfRunningTasks) {
// si prepara l'avvio delle attività
beginRunningTasks(numberOfRunningTasks);
// stato dei pulsanti e dei menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- riga 4: si comunica all’attività principale che si sta per avviare l’attività [numberOfRunningTasks];
- riga 6: si nascondono tutte le opzioni del menu;
- riga 7: per rendere poi visibile l’opzione [Actions/Annuler];
Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// si annullano le attività asincrone
cancelRunningTasks();
}
- riga 8: si richiede alla classe padre di annullare le attività asincrone;
3.6.6.3. Gestione del ciclo di vita del frammento
Il frammento presenta il seguente stato [ConfigFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// Visibilità dei due messaggi di errore
private boolean txtErrorUrlServiceRestVisible;
private boolean txtErrorUtilisateurVisible;
// getter e setter
...
}
- quando la classe padre glielo richiederà, il frammento salverà la visibilità dei suoi due messaggi di errore;
Il ciclo di vita del frammento è implementato nel modo seguente:
// implementazione dei metodi della classe padre -------------------------------------------
@Override
public CoreState saveFragment() {
// salvataggio dello stato del frammento
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) {
// prima visita
// si nascondono i messaggi di errore
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// ripristino della visibilità dei messaggi di errore
ConfigFragmentState state = (ConfigFragmentState) previousState;
// non è la prima visita - si ripristinano i messaggi di errore
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();
// vista successiva?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
}
}
// metodi privati ------------------------------------------------
private void initMenu(){
// stato del menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- righe 2-9: quando la classe padre glielo richiede, il frammento salva lo stato dei suoi due messaggi di errore;
- righe 11-14: il numero del frammento è [IMainActivity.VUE_CONFIG];
- righe 16-19: vengono eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null). In questo caso, non c’è nulla da fare;
- righe 21-31: vengono eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
- righe 24-29: alla prima visita, si nascondono i messaggi di errore e si visualizza il menu senza l'azione [Annuler] (righe 62-66);
- righe 33-35: eseguite quando si accede al frammento tramite un'operazione [SUBMIT]. Qui ciò non accade mai;
- righe 37-44: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE]. Si ripristina lo stato dei messaggi di errore a partire dallo stato precedente;
- righe 47-49: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Non c'è altro da fare;
- righe 51-59: vengono eseguite quando tutte le attività asincrone sono terminate;
- righe 53-54: si riporta il menu allo stato predefinito;
- righe 56-58: se le attività si sono concluse normalmente, si passa alla vista successiva, altrimenti si rimane sulla stessa vista;
3.6.7. Gestione della schermata iniziale
3.6.7.1. La vista
La schermata iniziale è la seguente:

Gli elementi dell'interfaccia visiva sono i seguenti:
3.6.7.2. Il frammento
La pagina iniziale è gestita dal seguente frammento [AccueilFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {
// elementi dell'interfaccia visiva
@ViewById(R.id.spinnerMedecins)
protected Spinner spinnerMedecins;
@ViewById(R.id.edt_JourRv)
protected DatePicker edtJourRv;
// dati locali
private List<Medecin> medecins;
private Calendar calendrier;
private String[] spinnerMedecinsDataSource;
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementazione dei metodi della classe padre -------------------------------------
...
}
- riga 26: il frammento è associato al seguente 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>
- righe 31-34: gli elementi dell'interfaccia visiva;
- riga 37: l'elenco dei medici;
- riga 38: un calendario;
- riga 39: la fonte dati dello spinner dei medici;
Il clic sul link [Valider] è gestito dal seguente metodo [doValider]:
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
// si registra l'ID del medico selezionato
Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
// si memorizza il giorno nella sessione
String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
session.setJourRv(jourRv);
// si passa al formato data aaaa-MM-gg
String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
session.setDayRv(dayRv);
// inizio dell'attesa - si avvierà 1 attività asincrona
beginWaiting(1);
// si richiede l'agenda del medico
executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// si elabora la risposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// errore?
if (responseAgendaMedecinJour.getStatus() != 0) {
// messaggio
showAlert(responseAgendaMedecinJour.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// si inserisce l'agenda nella sessione
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- riga 5: si recupera l'identificativo del medico selezionato;
- righe 7-8: si inserisce, in formato francese, la data scelta;
- righe 10-11: si inserisce, in formato inglese, la data scelta;
- riga 13: si comunica alla classe padre che si sta per avviare un'operazione asincrona e si prepara l'attesa;
- righe 15-22: viene richiesta l'agenda del medico;
- riga 15: il metodo [executeInBackground] richiede due parametri:
- riga 15: il processo da eseguire e da osservare viene fornito dal metodo [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
- righe 15-22: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<AgendaMedecinJour>]
- riga 20: quando si riceve la risposta, questa viene passata al metodo [consumeAgenda] della riga 25;
- riga 15: il metodo [executeInBackground] richiede due parametri:
- righe 25-37: è stata ricevuta l'agenda del medico. La si analizza;
- righe 27-34: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
- riga 29: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
- riga 31: si annullano tutte le attività;
- riga 33: si torna all'interfaccia utente;
- riga 36: se non si sono verificati errori, l'agenda viene attivata;
Il metodo [beginWaiting] (riga 13) è il seguente:
// inizio dell'attesa
protected void beginWaiting(int numberOfRunningTasks) {
// si prepara l'avvio delle attività
beginRunningTasks(numberOfRunningTasks);
// stato dei pulsanti e dei menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- riga 4: si comunica all'attività padre che si sta per avviare [numberOfRunningTasks] attività;
- riga 6: si nascondono tutte le opzioni del menu;
- riga 7: per rendere poi visibile l’opzione [Actions/Annuler];
Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// si annullano le attività asincrone
cancelRunningTasks();
}
- riga 8: si richiede alla classe padre di annullare le attività asincrone;
Il clic sull'opzione di menu [Retour à la configuration] viene gestito nel modo seguente:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// si passa alla vista di configurazione
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- riga 4: si passa alla vista di configurazione con l'azione [NAVIGATION]. Ciò significa che si desidera ripristinare la vista di configurazione nello stato in cui è stata lasciata;
3.6.7.3. Gestione del ciclo di vita del frammento
Il frammento presenta il seguente stato [AccueilFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
public class AccueilFragmentState extends CoreState {
// stato del frammento [Accueil]
// posizione del medico selezionato
private int selectedMedecinPosition;
// data selezionata
private int year;
private int month;
private int dayOfMonth;
// fonte dati del menu a rotazione dei medici
private String[] spinnerMedecinsDataSource;
// costruttori
public AccueilFragmentState() {
}
// getter e setter
...
}
- riga 11: consente di visualizzare l’elemento selezionato nell’elenco dei medici;
- righe 13-15: consentono di restituire la data selezionata nel calendario;
- riga 17: consente di restituire la fonte dei dati dell’elenco dei medici;
Il ciclo di vita del frammento è implementato come segue:
// implementazione dei metodi della classe padre -------------------------------------
@Override
public CoreState saveFragment() {
// si salva la vista
AccueilFragmentState state = new AccueilFragmentState();
state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
state.setDayOfMonth(edtJourRv.getDayOfMonth());
state.setMonth(edtJourRv.getMonth());
state.setYear(edtJourRv.getYear());
state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_ACCUEIL;
}
@Override
protected void initFragment(CoreState previousState) {
// si recuperano i medici dalla sessione
medecins = session.getMédecins();
// Prima visita?
if (previousState == null) {
// si crea la tabella visualizzata dallo 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 {
// non è la prima visita
AccueilFragmentState state = (AccueilFragmentState) previousState;
spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
}
// il calendario
calendrier = Calendar.getInstance();
}
@Override
protected void initView(CoreState previousState) {
// si associa lo spinner dei medici alla relativa fonte di dati
ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerMedecins.setAdapter(dataAdapterMedecins);
// data minima del calendario fino ad oggi
edtJourRv.setMinDate(calendrier.getTimeInMillis());
// Prima visita?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// si ripristina lo stato della sessione corrente
AccueilFragmentState state = (AccueilFragmentState) previousState;
// selezione dei medici tramite spinner
spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
// calendario
edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// chiamata al termine o all'annullamento di tutte le attività
// Stato del menu
initMenu();
// vista successiva?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
}
}
// metodi privati ------------------------------------------------
private void initMenu() {
// stato del menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- righe 2-9: quando la classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
- riga 6: la posizione selezionata nell'elenco dei medici;
- righe 7-9: il giorno del mese, il mese e l'anno della data selezionata nel calendario;
- riga 10: la fonte dei dati dello spinner dei medici;
- righe 14-17: il numero del frammento è [IMainActivity.VUE_ACCUEIL];
- righe 19-39: eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
- righe 25-31: in caso di prima visita, viene costruita la fonte dati dello spinner dei medici;
- righe 33-35: per le visite successive, la fonte dati dello spinner viene recuperata dallo stato precedente del frammento;
- righe 41-54: eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
- righe 50-53: per la prima visita, viene visualizzato il menu senza l’azione [Annuler] (righe 88-92);
- righe 43-48: per tutte le visite, che siano la prima o meno, si associa lo spinner dei medici alla sua fonte (righe 44-46) e si imposta la data minima del calendario alla data odierna (riga 48);
- righe 56-60: eseguite quando si accede al frammento tramite un’operazione [SUBMIT]. In questo caso si proviene dalla vista [CONFIG]. Si riporta il menu allo stato iniziale;
- righe 62-70: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
- riga 67: si riposiziona lo spinner dei medici sull'ultimo medico selezionato;
- riga 69: si imposta il calendario sull'ultima data scelta;
- righe 72-74: vengono eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Non c'è altro da fare;
- righe 76-85: vengono eseguite quando tutte le attività asincrone sono terminate;
- riga 80: si riporta il menu allo stato predefinito;
- righe 82-84: se le attività si sono concluse normalmente, si passa alla vista successiva, altrimenti si rimane sulla stessa vista;
3.6.8. Gestione della vista Agenda
3.6.8.1. La vista
La vista iniziale è la seguente:

Gli elementi dell'interfaccia visiva sono i seguenti:
3.6.8.2. Il frammento
La vista Agenda è gestita dal seguente frammento [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 {
// elementi dell'interfaccia visiva
@ViewById(R.id.txt_titre2_agenda)
protected TextView txtTitre2;
@ViewById(R.id.listViewAgenda)
protected ListView lstCreneaux;
// agenda visualizzata dal frammento
private AgendaMedecinJour agenda;
// informazioni ListView sulle fasce orarie
private int firstPosition;
private int top;
// appuntamento eliminato o meno
private boolean rdvSupprimé;
// numero della fascia oraria aggiunta o eliminata
private int numCréneau;
// aggiornamento dell'agenda dopo un'aggiunta/eliminazione
private void updateAgenda() {
...
}
...
// implementazione dei metodi della classe padre ------------------------------------------------------
...
}
- riga 27: il frammento è associato al seguente 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>
- righe 32-35: gli elementi dell'interfaccia visiva;
- righe 37-45: dati globali dei metodi;
3.6.8.2.1. Metodo [updateAgenda]
La (ri)generazione dell'elenco delle fasce orarie dell'agenda è necessaria in diversi punti del codice. È stata fattorizzata nel seguente metodo privato [updateAgenda]:
// aggiornamento dell'agenda dopo un'aggiunta/eliminazione
private void updateAgenda() {
// (ri)generazione delle fasce orarie dell'agenda
// l'agenda viene acquisita nella sessione e memorizzata in un campo del frammento
agenda = session.getAgenda();
// rigenerazione del ListView delle fasce orarie
ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour(), this);
lstCreneaux.setAdapter(adapter);
// ci si riposiziona nel punto corretto del ListView
lstCreneaux.setSelectionFromTop(firstPosition, top);
}
- riga 5: l'agenda viene prelevata dalla sessione e memorizzata nel campo [agenda] del frammento;
- righe 7-9: si definisce l’adattatore del componente [ListView]. Questo adattatore definisce sia la fonte dati di [ListView] sia il modello di visualizzazione di ciascun elemento di quest’ultima. Presenteremo questo adattatore prossimamente;
- riga 11: si torna alla posizione precedente dell’agenda. Infatti, è visibile solo una parte delle fasce orarie della giornata. Se si aggiunge o si elimina un appuntamento nell’ultima fascia oraria, il codice sopra riportato aggiornerà la pagina per visualizzare la nuova agenda. Questo aggiornamento fa sì che ci si ritrovi nuovamente posizionati sulla prima fascia oraria, il che non è auspicabile. La riga 5 risolve questo problema. La descrizione di questa soluzione si trova in URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];
La classe [ListCreneauxAdapter] serve a definire una riga del [ListView]:

Come si vede sopra, a seconda che la fascia oraria abbia o meno un appuntamento, la visualizzazione non è la stessa. Il codice della classe [ListCreneauxAdapter] è il seguente:
...
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
// la tabella delle fasce orarie
private CreneauMedecinJour[] creneauxMedecinJour;
// il contesto di esecuzione
private Context context;
// l'ID del layout di visualizzazione di una riga dell'elenco delle fasce orarie
private int layoutResourceId;
// listener dei clic
private AgendaFragment vue;
// costruttore
public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaFragment vue) {
super(context, layoutResourceId, creneauxMedecinJour);
// si memorizzano le informazioni
this.creneauxMedecinJour = creneauxMedecinJour;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.vue = vue;
// si ordina la tabella delle fasce orarie in base all'orario
Arrays.sort(creneauxMedecinJour, new MyComparator());
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
// ordinamento della tabella delle fasce orarie
class MyComparator implements Comparator<CreneauMedecinJour> {
...
}
}
- riga 3: la classe [ListCreneauxAdapter] deve estendere un adattatore predefinito per le classi [ListView], in questo caso la classe [ArrayAdapter] che, come indica il nome, fornisce alla classe [ListView] un array di oggetti, in questo caso di tipo [CreneauMedecinJour]. Ricordiamo il codice di questa entità:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campi
private Creneau creneau;
private Rv rv;
...
}
- la classe [CreneauMedecinJour] contiene una fascia oraria (riga 5) e un eventuale appuntamento (riga 6) oppure null se non c’è alcun appuntamento;
Torniamo al codice della classe [ListCreneauxAdapter]:
- riga 15: il costruttore riceve quattro parametri:
- l'attività Android in corso,
- il file XML che definisce il contenuto di ciascun elemento del [ListView],
- l'array degli slot orari del medico,
- la vista stessa;
- riga 24: la tabella delle fasce orarie è ordinata in ordine crescente in base agli orari;
Il metodo [getView] ha il compito di generare la vista corrispondente a una riga del [ListView]. Questa comprende tre elementi:
Il codice del metodo [getView] è il seguente:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// si seleziona la fascia oraria corretta
CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
// si crea la riga
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// la fascia oraria
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()));
// il cliente
TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
String text;
if (creneauMedecin.getRv() != null) {
Client client = creneauMedecin.getRv().getClient();
text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
} else {
text = "";
}
txtClient.setText(text);
// il collegamento
final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
if (creneauMedecin.getRv() == null) {
// aggiungere
btnValider.setText(R.string.btn_ajouter);
btnValider.setTextColor(context.getResources().getColor(R.color.blue));
} else {
// elimina
btnValider.setText(R.string.btn_supprimer);
btnValider.setTextColor(context.getResources().getColor(R.color.red));
}
// listener del collegamento
btnValider.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// si trasmettono le informazioni alla vista dell'agenda
vue.doValider(position, btnValider.getText().toString());
}
});
// si restituisce la riga
return row;
}
- riga 2: «posizione» è il numero di riga che verrà generato nel [ListView]. È anche il numero della fascia oraria nella tabella [creneauxMedecinJour]. Gli altri due parametri vengono ignorati;
- riga 4: si recupera la fascia oraria da visualizzare nella riga del file [ListView];
- riga 6: la riga viene costruita in base alla sua definizione XML
![]() |
Il codice di [creneau_medecin.xml] è il seguente:
<?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>
- righe 8-10: viene generata la fascia oraria [1];
- righe 12-20: viene generata l'identità del cliente [2];
- riga 23: se la fascia oraria non ha alcun appuntamento;
- righe 25-26: viene creato il link [Ajouter] di colore blu;
- righe 29-30: altrimenti si crea il link [Supprimer] di colore rosso;
- righe 33-40: indipendentemente dalla natura del collegamento [Ajouter / Supprimer], sarà il metodo [doValider] della vista a gestire il clic sul collegamento. Il metodo riceverà due argomenti:
- il numero della fascia oraria su cui è stato cliccato,
- il testo del link su cui è stato cliccato;
- riga 42: si restituisce la riga appena costruita.
Si noti che è il metodo [doValider] del frammento [AgendaFragment] a gestire i link. Esso è il seguente:
// clic su un link [Ajouter / Supprimer]
public void doValider(int numCréneau, String texte) {
// operazione in corso?
if (numberOfRunningTasks != 0) {
Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
return;
}
// si prende nota della posizione di scorrimento per tornarci
// leggi [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// posizione del primo elemento visibile completamente o parzialmente
firstPosition = lstCreneaux.getFirstVisiblePosition();
// offset Y di questo elemento rispetto alla parte superiore di ListView
// misura l'altezza della parte eventualmente nascosta
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// si registra anche il numero della casella cliccata
this.numCréneau = numCréneau;
// a seconda del testo del link, non si esegue la stessa operazione
if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
doAjouter();
} else {
doSupprimer();
}
}
- il metodo [doValider] riceve due informazioni:
- il numero della fascia oraria su cui è stato cliccato;
- il testo (Aggiungi / Elimina) del link su cui è stato cliccato;
- righe 4-7: il clic sui link [Supprimer / Ajouter] è disabilitato se sono in corso attività asincrone. Si tratta di una scelta che facilita la scrittura del codice. È discutibile;
- righe 11-15: si registrano le informazioni (firstPosition, top) relative a ListView delle fasce orarie nei campi del frammento, in modo che il metodo privato [updateAgenda] possa rigenerarlo con la stessa posizione di scorrimento;
- riga 17: si registra il numero della fascia oraria cliccata;
- righe 19-23: a seconda del testo del link cliccato, si effettua un'aggiunta o una cancellazione;
3.6.8.2.2. Metodo [doSupprimer]
Il metodo [doSupprimer] garantisce l’eliminazione dell’appuntamento relativo alla fascia oraria cliccata:
// cancellazione di un appuntamento
private void doSupprimer() {
// si attende il completamento di due attività
beginWaiting(2);
// l'appuntamento viene cancellato in background
rdvSupprimé = false;
// ID dell'appuntamento da eliminare
long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
// cancellazione tramite un'attività asincrona
executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// utilizzo del risultato
consumeRv(responseRv);
}
});
}
// utilizzo di una risposta
private void consumeRv(Response<Rv> responseRv) {
// errore?
if (responseRv.getStatus() != 0) {
// messaggio
showAlert(responseRv.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// si nota che l'appuntamento è stato cancellato
rdvSupprimé = true;
// si richiede l'agenda più recente
executeInBackground(
mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// si elabora la risposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// utilizzo di un calendario
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// errore?
if (responseAgendaMedecinJour.getStatus() != 0) {
// messaggio
showAlert(responseAgendaMedecinJour.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// si inserisce l'agenda nella sessione
session.setAgenda(responseAgendaMedecinJour.getBody());
// si aggiorna l'agenda della vista
updateAgenda();
}
- riga 4: si segnala alla classe padre che si stanno per avviare due attività asincrone e si inizia ad attendere il completamento di entrambe;
- riga 8: si recupera l'identificativo dell'appuntamento da eliminare. Il server, infatti, necessita di questa informazione;
- righe 9-18: si richiede l’eliminazione dell’appuntamento tramite un’attività asincrona;
- riga 10: il metodo [executeInBackground] richiede due parametri:
- riga 10: il processo da eseguire e da osservare viene fornito dal metodo [mainActivity.supprimerRv(idRv)];
- righe 10-17: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<Rv>]
- riga 15: quando si riceve la risposta, questa viene passata al metodo [consumeRv] della riga 21;
- riga 10: il metodo [executeInBackground] richiede due parametri:
- righe 21-44: è stata ricevuta la risposta dall'attività asincrona. La si elabora;
- righe 23-30: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
- riga 25: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
- riga 27: si annullano tutte le attività;
- riga 29: si torna all'interfaccia utente;
- riga 32: se non si è verificato alcun errore, si registra che l'appuntamento è stato cancellato;
- righe 34-43: anziché limitarsi a eliminare l’appuntamento dall’agenda attualmente visualizzata dal frammento, si richiede la nuova agenda del medico. Infatti, l’applicazione è multiutente e anche altri utenti potrebbero aver modificato l’agenda del medico. Quindi è meglio disporre della versione più recente;
- righe 34-43, 47-61: si ripete quanto fatto nel frammento [AccueilFragment], questa volta utilizzando le informazioni ricavate dalla sessione;
Il metodo [beginWaiting] (riga 4) è il seguente:
// inizio dell'attesa
protected void beginWaiting(int numberOfRunningTasks) {
// si prepara l'avvio delle attività
beginRunningTasks(numberOfRunningTasks);
// stato dei pulsanti e dei menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- riga 4: si comunica al processo padre che si sta per avviare il processo [numberOfRunningTasks];
- riga 6: si nascondono tutte le opzioni del menu;
- riga 7: per rendere poi visibile l’opzione [Actions/Annuler];
3.6.8.2.3. Metodo [doAnnuler]
Il clic sull'opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// si annullano le attività asincrone
cancelRunningTasks();
}
- riga 7: si richiede alla classe padre di annullare le attività asincrone;
3.6.8.2.4. Opzione di menu [Retour à la configuration]
Il clic sull’opzione di menu [Retour à la configuration] viene gestito nel modo seguente:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// si passa alla vista di configurazione
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- riga 4: si passa alla vista di configurazione con l'azione [NAVIGATION]. Ciò significa che si desidera ripristinare la vista di configurazione nello stato in cui era stata lasciata;
3.6.8.2.5. Opzione di menu [Retour à l'accueil]
Il clic sull’opzione di menu [Retour à l'accueil] viene gestito in modo simile:
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// si passa alla vista iniziale
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
3.6.8.3. Gestione del ciclo di vita del frammento
Il frammento presenta il seguente stato [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 {
// titolo della vista
private String titre;
// ListView
private int firstPosition;
private int top;
// costruttori
public AgendaFragmentState() {
}
public AgendaFragmentState(String titre) {
this.titre = titre;
}
// getter e setter
...
}
- riga 10: il titolo visualizzato nella parte superiore della vista;
- righe 12-13: consentono di restituire il scrolling del ListView relativo agli orari del medico;
Il ciclo di vita del frammento è implementato come segue:
// implementazione dei metodi della classe padre ------------------------------------------------------
@Override
public CoreState saveFragment() {
// salvataggio dello stato
AgendaFragmentState state = new AgendaFragmentState();
state.setTitre(txtTitre2.getText().toString());
// si registra la posizione di scorrimento per tornarci
// leggi [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// posizione del primo elemento visibile completamente o parzialmente
firstPosition = lstCreneaux.getFirstVisiblePosition();
// offset Y di questo elemento rispetto alla parte superiore di ListView
// misura l'altezza della parte eventualmente nascosta
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// si memorizza tutto questo
state.setTop(top);
state.setFirstPosition(firstPosition);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AGENDA;
}
@Override
protected void initFragment(CoreState previousState) {
// Prima visita?
if (previousState != null) {
// Non è la prima visita
AgendaFragmentState state = (AgendaFragmentState) previousState;
// e le informazioni da ListView
firstPosition = state.getFirstPosition();
top = state.getTop();
}
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// si recupera l'agenda
agenda = session.getAgenda();
// si genera il titolo della pagina
Medecin medecin = agenda.getMedecin();
txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
medecin.getNom(), session.getJourRv()));
// stato del menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// si rigenera il titolo della pagina
AgendaFragmentState state = (AgendaFragmentState) previousState;
txtTitre2.setText(state.getTitre());
}
@Override
protected void notifyEndOfUpdates() {
// si rigenera l'elenco delle fasce orarie
updateAgenda();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// stato del menu
initMenu();
// in caso di annullamento, se l'appuntamento è stato cancellato, è necessario aggiornare l'agenda locale
if (runningTasksHaveBeenCanceled && rdvSupprimé) {
// si elimina l'appuntamento dall'agenda locale (non è stato possibile accedere all'agenda globale)
agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
// si aggiorna l'interfaccia visiva
updateAgenda();
}
}
// metodi privati ------------------------------------------------
private void initMenu() {
// stato del menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- righe 2-19: quando la sua classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
- riga 6: il titolo visualizzato nella parte superiore della vista;
- righe 7-17: le informazioni (top, firstPosition) che consentiranno di ricreare il scrolling del ListView;
- righe 21-24: il numero del frammento è [IMainActivity.VUE_AGENDA];
- righe 26-35: vengono eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
- righe 30-34: se non si tratta della prima visita al frammento, si recuperano le informazioni (top, firstPosition) che consentiranno di ricostituire il scrolling a partire dal ListView;
- righe 38-40: vengono eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState == null) o ricostruita nelle volte successive (previousState != null). Qui non c'è nulla da fare perché il ListView degli slot verrà generato dal metodo privato [updateAgenda] (righe 61-65);
- righe 42-52: eseguite quando si arriva al frammento tramite un'operazione [SUBMIT]. Si proviene quindi dalla vista [ACCUEIL];
- riga 45: si recupera l'agenda attivata da [AccueilFragment];
- righe 47-49: si genera il titolo della vista;
- il ListView degli slot verrà generato dal metodo privato [updateAgenda] (righe 61-65);
- righe 54-59: eseguite quando si arriva al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
- righe 57-58: si rigenera il titolo della vista;
- il ListView delle fasce orarie verrà generato dal metodo privato [updateAgenda] (righe 61-65);
- righe 72-74: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Si aggiorna il ListView delle fasce orarie poiché questo aggiornamento è necessario indipendentemente dal modo in cui si arriva al frammento;
- righe 67-77: eseguite quando tutte le attività asincrone sono terminate;
- riga 70: si riporta il menu allo stato predefinito (righe 82-86);
- riga 72: c'erano due attività asincrone. Si verifica se la prima (l'eliminazione dell'appuntamento) è andata a buon fine, nonostante un annullamento;
- riga 74: in caso affermativo, si elimina l'appuntamento dall'agenda locale
- riga 75: e si aggiorna la visualizzazione dello stesso;
3.6.9. Gestione della schermata di aggiunta di un appuntamento
3.6.9.1. La schermata
La schermata di aggiunta di un appuntamento è la seguente:

Gli elementi dell'interfaccia visiva sono i seguenti:
3.6.9.2. Il frammento
La schermata di aggiunta di un appuntamento è gestita dal seguente frammento [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 {
// gli elementi dell'interfaccia visiva
@ViewById(R.id.spinnerClients)
protected Spinner spinnerClients;
@ViewById(R.id.txt_titre2_ajoutRv)
protected TextView txtTitre2;
// i clienti
private List<Client> clients;
// dati locali
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementazione dei metodi della classe padre ----------------------------------
...
}
- riga 26: il frammento è associato al seguente 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>
- righe 30-33: gli elementi dell'interfaccia visiva;
- riga 36: l'elenco dei clienti;
- riga 43: la fonte dati dello spinner dei clienti;
Il clic sul link [Valider] è gestito dal seguente metodo [doValider]:
// i clienti
private List<Client> clients;
// dati locali
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
...
// convalida della pagina
@OptionsItem(R.id.actionValider)
protected void doValider() {
// si recupera il cliente selezionato
Client client = clients.get(spinnerClients.getSelectedItemPosition());
// inizio dell'attesa di 2 attività asincrone
beginWaiting(2);
// si aggiunge il RV
rdvAjouté = false;
executeInBackground(
mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// si elabora la risposta
consumeRv(responseRv);
}
});
}
// elaborazione di un oggetto Response<Rv>
void consumeRv(Response<Rv> responseRv) {
// errore?
if (responseRv.getStatus() != 0) {
// messaggio
showAlert(responseRv.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// si nota che l'appuntamento è stato aggiunto
rdvAjouté = true;
// si salva l'appuntamento
this.rv = responseRv.getBody();
// si richiede la nuova agenda
executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// si elabora la risposta
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// elaborazione di un oggetto Response<AgendaMedecinJour>
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// errore?
if (responseAgendaMedecinJour.getStatus() != 0) {
// messaggio
showAlert(responseAgendaMedecinJour.getMessages());
// annullamento
doAnnuler();
// ritorno a UI
return;
}
// si inserisce l'agenda nella sessione
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- riga 13: all’avvio del metodo [doValider], i campi 2, 5, 6 e 9 sono stati inizializzati durante il ciclo di vita del frammento. Vedremo come;
- riga 15: si recupera l’entità [Client] corrispondente all’elemento selezionato nel selettore dei clienti;
- riga 17: si comunica alla classe padre che si stanno per avviare due attività asincrone e si prepara l'attesa;
- riga 19: inizialmente l’appuntamento non è ancora stato aggiunto all’agenda del medico;
- righe 20-30: si richiede al server l'aggiunta di un appuntamento;
- riga 20: il metodo [executeInBackground] richiede due parametri:
- riga 20: il processo da eseguire e osservare è fornito dal metodo [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
- righe 22-29: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<Rv>]
- riga 27: quando si riceve la risposta, questa viene passata al metodo [consumeRV] della riga 33;
- riga 20: il metodo [executeInBackground] richiede due parametri:
- righe 33-56: è stata ricevuta la risposta dal server. La si elabora;
- righe 35-42: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
- riga 37: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
- riga 39: si annullano tutte le attività;
- riga 41 : si torna all'interfaccia utente;
- riga 44: se non si è verificato alcun errore, si registra che l'appuntamento è stato aggiunto;
- riga 46: si memorizza l'appuntamento aggiunto in un campo del frammento;
- righe 47-55: come già fatto durante l'eliminazione di un appuntamento, dopo l'aggiunta dell'appuntamento si richiede al server l'agenda più recente del medico;
- righe 47-56, 59-71: si tratta di un codice già incontrato più volte;
Il metodo [beginWaiting] (riga 17) è il seguente:
// inizio dell'attesa
protected void beginWaiting(int numberOfRunningTasks) {
// si prepara l'avvio delle attività
beginRunningTasks(numberOfRunningTasks);
// stato dei pulsanti e dei menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- riga 4: si indica all’attività principale che si lanceranno le attività [numberOfRunningTasks];
- riga 6: si nascondono tutte le opzioni del menu;
- riga 7: per poi rendere visibile l’opzione [Actions/Annuler];
Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// si annullano le attività asincrone
cancelRunningTasks();
}
- riga 7: si richiede alla classe padre di annullare le attività asincrone;
Le navigazioni indietro sono gestite dai tre metodi seguenti:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// si passa alla vista di configurazione
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// si passa alla vista di configurazione
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAgenda)
protected void navigationToAgenda() {
// si passa alla vista dell'agenda
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}
3.6.9.3. Gestione del ciclo di vita del frammento
Il frammento presenta il seguente stato [AjoutRvFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
// stato del frammento AjoutRvFragment
public class AjoutRvFragmentState extends CoreState {
// posizione del cliente selezionato
private int selectedClientPosition;
// titolo della vista
private String titre;
// fonte dati dello spinner dei clienti
private String[] spinnerClientsDataSource;
// getter e setter
...
}
Il ciclo di vita del frammento è implementato nel modo seguente:
// implementazione dei metodi della classe padre ----------------------------------
@Override
public CoreState saveFragment() {
// salvataggio della vista
AjoutRvFragmentState state = new AjoutRvFragmentState();
state.setTitre(txtTitre2.getText().toString());
state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
state.setSpinnerClientsDataSource(spinnerClientsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AJOUT_RV;
}
@Override
protected void initFragment(CoreState previousState) {
// recupero dei clienti in sessione
clients = session.getClients();
// Prima visita?
if (previousState == null) {
// si crea l'array visualizzato dallo 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 {
// non è la prima visita
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
spinnerClientsDataSource = state.getSpinnerClientsDataSource();
}
}
@Override
protected void initView(CoreState previousState) {
// Associazione dello spinner alla sua fonte di dati
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);
// Prima visita?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// si recupera il numero della fascia oraria da prenotare nella sessione
int position = session.getPosition();
// si recupera l'agenda del medico nella sessione
AgendaMedecinJour agenda = session.getAgenda();
// si recupera il medico e la fascia oraria in cui inserire un appuntamento
medecin = agenda.getMedecin();
creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
// si crea il titolo 2 della pagina
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()));
// selezione cliente
spinnerClients.setSelection(0);
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// ripristino dello stato precedente
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
// titolo
txtTitre2.setText(state.getTitre());
// indicatore di caricamento
spinnerClients.setSelection(state.getSelectedClientPosition());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// stato menu
initMenu();
// vista successiva?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
// c'è stata un'annullamento - appuntamento già aggiunto?
if (rdvAjouté) {
// si modifica l'agenda locale (non si è ricevuta l'agenda globale)
AgendaMedecinJour agenda = session.getAgenda();
agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
// si visualizza l'agenda
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
}
// metodi privati -------------------
private void initMenu() {
// stato del menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- righe 2-10: quando la sua classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
- riga 6: il titolo nella parte superiore della vista;
- riga 7: la posizione dell'elemento selezionato nello spinner dei clienti;
- riga 8: la fonte dati dello spinner dei clienti;
- righe 12-15: il numero del frammento è [IMainActivity.VUE_AJOUT_RV];
- righe 17-35: eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
- riga 20: si recupera l'elenco dei clienti nella sessione per inserirlo in un campo del frammento;
- righe 22-30: in caso di prima visita, viene costruita la fonte dati dello spinner dei clienti;
- righe 32-33: per le visite successive, la fonte dati dello spinner dei clienti viene recuperata dallo stato precedente del frammento;
- righe 37-49: eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
- righe 40-43: in ogni caso, lo spinner dei clienti viene associato alla sua fonte dati;
- righe 45-48: alla prima visita, viene visualizzato il menu senza l’azione [Annuler] (righe 107-111);
- righe 51-70: eseguite quando si arriva al frammento tramite un'operazione [SUBMIT]. Si proviene quindi dalla vista [AGENDA];
- riga 54: si recupera il numero della fascia oraria in cui si inserirà un appuntamento;
- righe 56-59: si recuperano l'entità [Medecin] e l'entità [Creneau] necessarie per l'aggiunta di questo appuntamento e le si inseriscono nei campi del frammento;
- righe 61-65: con queste informazioni, è possibile costruire il titolo della vista;
- riga 67: lo spinner dei clienti viene posizionato sul primo elemento;
- riga 69: il menu viene riportato allo stato iniziale (senza l’opzione [Annuler]);
- righe 72-80: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
- riga 77: si rigenera il titolo della vista;
- riga 79: si riposiziona lo spinner dei clienti sull'ultimo cliente selezionato;
- righe 82-84: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. A questo punto non c'è altro da fare;
- righe 86-104: vengono eseguite quando tutte le operazioni asincrone sono terminate;
- riga 89: si riporta il menu allo stato predefinito;
- righe 91-94: se le attività si sono concluse normalmente, si ritorna alla vista [AGENDA] tramite un [SUBMIT] (in questo caso, avrebbe potuto essere anche un'azione di tipo NAVIGATION);
- righe 96-103: se le operazioni si sono concluse con un'annullamento, si verifica comunque se l'appuntamento è stato aggiunto (ciò significherebbe che è stato il recupero della nuova agenda a non andare a buon fine);
- righe 98-99: se l'appuntamento è stato aggiunto;
- righe 98-99: l’appuntamento restituito dal server viene aggiunto all’agenda corrente, quella attiva nella sessione;
- riga 101: si ritorna alla vista [AGENDA] tramite un'azione [SUBMIT] (in questo caso, avrebbe potuto trattarsi anche di un'azione di tipo NAVIGATION);
3.7. Exécution
Eseguire i seguenti test:
- utilizzare l'applicazione in condizioni normali e verificare che funzioni;
- ruotare il dispositivo per ciascuna delle viste e verificare che ciascuna venga ripristinata correttamente;
- impostare un tempo di attesa di alcuni secondi in [IMainActivity];
- procedere quindi all'annullamento delle attività e verificare che il risultato ottenuto sia quello previsto;
- ruotare il dispositivo durante i tempi di attesa e verificare che le attività vengano effettivamente annullate e che non si verifichino arresti anomali;
- modificare l’adiacenza dei frammenti in [IMainActivity] e verificare che l’applicazione continui a funzionare;












































