Skip to content

3. Caso di studio - Gestione degli appuntamenti

3.1. Il progetto

Nel documento [Tutorial AngularJS / Spring 4], è stata sviluppata un'applicazione client/server per gestire gli appuntamenti medici. D'ora in poi ci riferiremo a questo documento come [rdvmedecins-angular]. L'applicazione aveva due tipi di client:

  • un client HTML/CSS/JS;
  • un client Android;

Il client Android è stato generato automaticamente dalla versione HTML del client utilizzando lo strumento [Cordova]. L'obiettivo di questo progetto è ricreare manualmente questo client Android utilizzando le conoscenze acquisite nei capitoli precedenti.

Si noti un'importante differenza tra le due soluzioni:

  • quello che creeremo funzionerà solo su tablet Android;
  • nella versione [rdvmedecins-angular], il client web mobile (HTML/CSS/JS) funziona su qualsiasi piattaforma (Android, iOS, Windows);

3.2. Le viste del client Android

Ci sono quattro visualizzazioni.

Vista Configurazione

Image

Schermata di selezione del medico e della data dell'appuntamento

Image

Schermata di selezione della fascia oraria dell'appuntamento

Image

Schermata di selezione del cliente per l'appuntamento

Image

3.3. Architettura del progetto

Utilizzeremo un'architettura client/server simile a quella dell'Esempio [Esempio-15] (vedere la Sezione 1.16) del presente documento:

Image

La comunicazione asincrona tra il client e il server sarà gestita utilizzando la libreria RxAndroid.

3.4. Il database

Non svolge un ruolo fondamentale in questo documento. Lo forniamo a scopo informativo. Lo 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: il numero ID del medico — la chiave primaria della tabella
  • VERSION: un numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • LAST_NAME: il cognome del medico
  • FIRST_NAME: il nome del medico
  • TITLE: il titolo (Sig.ra, Sig.ra, Sig.)

3.4.2. La tabella [CLIENTS]

I clienti dei vari medici sono memorizzati nella tabella [CLIENTS]:

  • ID: il numero ID del cliente — la 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.
  • COGNOME: il cognome del cliente
  • NOME: il nome del cliente
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

3.4.3. La tabella [SLOTS]

Elenca le fasce orarie in cui sono disponibili gli appuntamenti:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • 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.
  • DOCTOR_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
  • START_TIME: ora di inizio della fascia oraria
  • MSTART: minuto di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).

3.4.4. La tabella [RV]

Elenca gli appuntamenti fissati per ciascun medico:

  • ID: identificatore univoco dell'appuntamento – chiave primaria
  • DAY: giorno dell'appuntamento
  • SLOT_ID: fascia oraria dell’appuntamento – chiave esterna sul campo [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
  • CUSTOMER_ID: l'ID del cliente per il quale è stata effettuata la prenotazione – una chiave esterna sul campo [ID] nella tabella [CUSTOMERS]

Questa tabella presenta un vincolo di unicità sui valori delle colonne unite (DAY, SLOT_ID):

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

Se una riga nella tabella [RV] ha il valore (DAY1, SLOT_ID1) per le colonne (DAY, SLOT_ID), questo valore non può comparire in nessun altro punto. In caso contrario, ciò significherebbe che sono stati prenotati due appuntamenti contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database genera un'eccezione SQLException quando ciò si verifica.

La riga con ID pari a 3 (vedi [1] sopra) indica che è stato prenotato un appuntamento per lo slot n. 20 e il cliente n. 4 il 23/08/2006. La tabella [SLOTS] ci dice che lo slot n. 20 corrisponde alla fascia oraria 16:20 – 16:40 e appartiene al medico n. 1 (Sig.ra Marie PELISSIER). La tabella [CLIENTS] ci dice che il cliente n. 4 è la sig.ra Brigitte BISTROU.

3.4.5. Generazione del database

Per creare le tabelle e popolarle, è possibile utilizzare lo script [dbrdvmedecins.sql], che si trova nell'archivio degli esempi |QUI|.

  

Con [WampServer] (vedere la sezione 6.15), procedere come segue:

 
  • In [1], clicca sull'icona [WampServer] e seleziona l'opzione [PhpMyAdmin] [2],
  • in [3], nella finestra che si apre, selezionare il link [Databases],
 
  • in [4-6], importare un file SQL,
  • in [7], selezionare lo script SQL e in [8] eseguirlo,
  • in [9], le tabelle del database sono state create. Segui uno dei link,
 
  • in [10], il contenuto della tabella.

Non torneremo più su questo database, ma invitiamo il lettore a seguirne l'evoluzione nel corso dei test, specialmente quando l'applicazione non funziona.

3.5. Il server Web / JSON

Image

Qui ci concentriamo sul server [1]. Non lo approfondiremo ulteriormente. È stato descritto in dettaglio nel documento [Spring MVC and Thymeleaf by Example]. I lettori interessati possono consultarlo. È stato sviluppato come il server dell'Esempio 15. Il suo codice sorgente è incluso negli esempi. Qui useremo il suo binario:

  
  • [rdvmedecins-server-all-1.0.jar] è il file binario del server;

3.5.1. Implementazione

In una finestra di comando, accedere alla 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, immettere il seguente comando (il DBMS MySQL deve 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. Abbiamo incluso solo quelli rilevanti per la comprensione del processo sopra descritto:

  • righe 14–18: viene avviato un server Tomcat incorporato sulla porta 8080 della macchina. Questo server 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 utilizzando il framework [Spring Security]. L'accesso agli URL del servizio web avviene tramite autenticazione;
  • Righe 29–44: gli URL esposti dal servizio web;

Approfondiremo questi aspetti.

3.5.2. Protezione del servizio web

Gli URL resi disponibili dal servizio web sono protetti. Il server richiede la presenza del seguente header nella richiesta HTTP del client:

Authorization: Basic code

Il codice previsto è la codifica Base64 [http://fr.wikipedia.org/wiki/Base64] della stringa 'username:password'. All'inizio, il servizio web accetta solo un utente chiamato 'admin' con la password 'admin'. Per questo utente specifico, l'intestazione sopra diventa la riga seguente:

Authorization: Basic YWRtaW46YWRtaW4=

Per inviare questa intestazione HTTP, utilizziamo il client HTTP [Advanced Rest Client], che è un plugin per il browser Chrome (vedi sezione 6.13). Testeremo manualmente i vari URL esposti dal servizio web per capire:

  • i parametri previsti dall'URL;
  • la natura esatta della sua risposta;

3.5.3. Elenco dei medici

L'URL [/getAllMedecins] recupera l'elenco dei medici:

  • in [1], l'URL oggetto della richiesta;
  • in [2], il metodo HTTP utilizzato per questa richiesta;
  • in [3], l'intestazione di sicurezza HTTP dell'utente (admin, admin);
  • in [4], la richiesta HTTP viene inviata;

La risposta del server è la seguente:

  • in [5], la risposta JSON formattata dal server;
  • in [6], la stessa risposta in formato grezzo;

La forma in [5] rende più facile vedere la struttura della risposta. Tutte le risposte del servizio web sono istanze della seguente classe [Response]:


package rdvmedecins.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • riga 9: lo stato della risposta. Il valore 0 indica che non si è verificato alcun errore; in caso contrario, si è verificato un errore;
  • riga 11: un elenco di messaggi di errore se si è verificato un errore;
  • riga 13: la risposta effettivamente prevista dal client;

La risposta all'URL [/getAllMedecins] è una 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 {
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }
 
}

Riga 3: La classe [Doctor] estende la seguente classe [Person]:


package rdvmedecins.android.dao.entities;
 
public class Personne extends AbstractEntity {
    // attributes of a person
    private String titre;
    private String nom;
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }
 
    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }
 
    // getters and setters
    ...
}

Riga 3: La classe [Person] 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;
    }
 
    // initialization
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }
 
    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }
 
    // getters and setters
    ...
}

In definitiva, la struttura di un oggetto [Doctor] è la seguente:


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

e quella di [Response<List<Doctor>>] è la seguente:

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

D'ora in poi useremo queste definizioni abbreviate per descrivere la risposta del server. Inoltre, per il momento, non includeremo più screenshot. Basta rivedere ciò che abbiamo appena trattato. Torneremo agli screenshot quando sarà il momento di effettuare una richiesta POST. Presenteremo anche un esempio di esecuzione nel seguente formato:

URL
/getAllDoctors
Risposta
{"status":0,"messages":null,"doctors":
[{"id":1,"version":1,"title":"Sig.ra","lastName":"PELISSIER","firstName":"Marie"},
{"id":2,"version":1,"title":"Sig.","lastName":"BROMARD","firstName":"Jacques"},
{"id":3,"version":1,"title":"Sig.","lastName":"JANDOT","firstName":"Philippe"},
{"id":4,"version":1,"title":"Sig.ra","lastName":"JACQUEMOT","firstName":"Justine"}]}

3.5.4. Elenco dei clienti

URL
/getAllClients
Risposta

Risposta<List<Client>> :[int status; List<String> messages;
 List<Client> clients]
Client: [Long id; Long version; String titolo;
 String cognome; String nome;]

Esempio:

URL
/getAllClients
Risposta
{"status":0,"messages":null,"clients":
[{"id":1,"version":1,"title":"Mr","lastName":"MARTIN","firstName":"Jules"},
{"id":2,"version":1,"title":"Ms","lastName":"GERMAN","firstName":"Christine"},
{"id":3,"version":1,"title":"Sig.","lastName":"JACQUARD","firstName":"Jules"},
{"id":4,"version":1,"title":"Sig.ra","lastName":"BISTROU","firstName":"Brigitte"}]}

3.5.5. Elenco degli orari disponibili per le visite mediche

URL
/getAllSlots/{doctorId}
Risposta

Risposta<List<Appointment>>:[int status ; List<String> messages ;
 List<Appointment> appuntamenti]
Slot: [int oraInizio; int minutoInizio; int oraFine; int minutoFine;]
  • [idMedecin]: ID del medico per il quale si desiderano gli slot di appuntamento;
  • [startTime]: ora di inizio dell'appuntamento;
  • [start_time]: ora di inizio della visita;
  • [hfin]: ora di fine della visita;
  • [endmin] : minuti di fine della visita;

Per una fascia oraria compresa tra le 10:20 e le 10:40, abbiamo [starts, starts, ends, ends] = [10, 20, 10, 40].

Esempio:

URL
/getAllSlots/1
Risposta
{"status":0,"messages":null,"slots":
[{"id":1,"version":1,"startTime":8,"startDate":0,"endTime":8,"endDate":20,"doctorId":1},
{"id":2,"version":1,"startTime":8,"startMin":20,"endTime":8,"endMin":40,"doctorId":1},
{"id":3,"version":1,"startHour":8,"startMinute":40,"endHour":9,"endMinute":0,"doctorId":1},
{"id":4,"version":1,"startHour":9,"startMinute":0,"endHour":9,"endMinute":20,"doctorId":1},
{"id":5,"version":1,"hstart":9,"mstart":20,"hend":9,"mend":40,"doctorId":1},
{"id":6,"versione":1,"oraInizio":9,"minutoInizio":40,"oraFine":10,"minutoFine":0,"idMedico":1},
{"id":7,"version":1,"oraInizio":10,"dataInizio":0,"oraFine":10,"dataFine":20,"idMedico":1},
{"id":8,"version":1,"startTime":10,"startMin":20,"endTime":10,"endMin":40,"doctorId":1},
{"id":9,"version":1,"startTime":10,"startDate":40,"endTime":11,"endDate":0,"doctorId":1},
{"id":10,"version":1,"startTime":11,"startDate":0,"endTime":11,"endDate":20,"doctorId":1},
{"id":11,"versione":1,"oraInizio":11,"dataInizio":20,"oraFine":11,"dataFine":40,"idMedico":1},
{"id":12,"versione":1,"oraInizio":11,"dataInizio":40,"oraFine":12,"dataFine":0,"idMedico":1},
{"id":13,"versione":1,"oraInizio":14,"dataInizio":0,"oraFine":14,"dataFine":20,"idMedico":1},
{"id":14,"versione":1,"oraInizio":14,"dataInizio":20,"oraFine":14,"dataFine":40,"idMedico":1},
{"id":15,"versione":1,"oraInizio":14,"dataInizio":40,"oraFine":15,"dataFine":0,"idMedico":1},
{"id":16,"versione":1,"oraInizio":15,"dataInizio":0,"oraFine":15,"dataFine":20,"idMedico":1},
{"id":17,"versione":1,"oraInizio":15,"dataInizio":20,"oraFine":15,"dataFine":40,"idMedico":1},
{"id":18,"versione":1,"oraInizio":15,"dataInizio":40,"oraFine":16,"dataFine":0,"idMedico":1},
{"id":19,"versione":1,"oraInizio":16,"dataInizio":0,"oraFine":16,"dataFine":20,"idMedico":1},
{"id":20,"versione":1,"oraInizio":16,"dataInizio":20,"oraFine":16,"dataFine":40,"idMedico":1},
{"id":21,"versione":1,"oraInizio":16,"dataInizio":40,"oraFine":17,"dataFine":0,"idMedico":1},
{"id":22,"versione":1,"oraInizio":17,"dataInizio":0,"oraFine":17,"dataFine":20,"idMedico":1},
{"id":23,"versione":1,"oraInizio":17,"dataInizio":20,"oraFine":17,"dataFine":40,"idMedico":1},
{"id":24,"version":1,"startTime":17,"startDate":40,"endTime":18,"endDate":0,"doctorId":1}]}

3.5.6. Elenco degli appuntamenti di un medico

URL
/getRvMedecinJour/{idMedecin}/{day}
Risposta

Risposta<List<Rv>>: [int status; List<String> messages;
 List<Rv> rvs]
Rv: [Date day; Client client; Slot slot;
 long clientId; long slotId]
  • [idMedic] : identificativo del medico per il quale si richiedono gli appuntamenti;
  • URL [day]: giorno degli appuntamenti nel formato 'yyyy-mm-dd';
  • Risposta [giorno]: come sopra, ma sotto forma di data Java;
  • [client]: il cliente per l'appuntamento. La sua struttura è stata descritta in precedenza;
  • [idClient]: l'identificatore del cliente;
  • [slot]: la fascia oraria dell'appuntamento. La sua struttura è stata descritta in precedenza;
  • [slotId]: l'identificatore dello slot;

Esempio:

URL
/getRvMedecinJour/1/2014-07-08
Risposta
{"status":0,"messages":null,
"rvs":[{"id":45,"version":0,"date":"2014-07-08","client":
{"id":1,"version":1,"title":"Mr","lastName":"MARTIN","firstName":"Jules"},"slot":
{"id":1,"version":1,"startTime":8,"startMinute":0,"endTime":8,"endMinute":20,"doctorId":1},
"idCliente":1,"idAppuntamento":1}]}

3.5.7. L'agenda di un medico

URL
/getDoctorScheduleDay/{doctorId}/{day}
Risposta

Risposta<DoctorScheduleDay>:[int status ; List<String> messages ;
 DoctorScheduleDay schedule]
DailyDoctorSchedule: [Doctor doctor; Date day;
DoctorAppointmentSlot[] doctorAppointmentSlots]
DailyDoctorSlot : [Slot slot ; Appointment appointment]
  • [doctorId]: identificativo del medico di cui si desiderano gli appuntamenti;
  • URL [giorno] : giorno degli appuntamenti nel formato 'aaaa-mm-gg' ;
  • [calendar]: calendario del medico;
  • [doctor]: il medico in questione. La sua struttura è stata definita in precedenza;
  • Risposta [day]: il giorno del calendario sotto forma di data Java;
  • [doctorDaySlots]: un array di elementi di tipo [DoctorDaySlot];
  • [slot]: uno slot. La sua struttura è stata descritta in precedenza;
  • [appuntamento]: un appuntamento. La sua struttura è stata descritta in precedenza;

Esempio:

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

{"status":0,"messages":null,"agenda":{"doctor":
{"id":1,"version":1,"title":"Sig.ra","lastName":"PELISSIER","firstName":"Marie"},
"day":1404770400000,"doctorDaySlots":[{"slot":
{"id":1,"version":1,"startHour":8,"startMinute":0,"endHour":8,"endMinute":20,"doctorId":1},
"appointment":{"id":45,"version":0,"date":"2014-07-08","client":
{"id":1,"version":1,"title":"Mr","lastName":"MARTIN","firstName":"Jules"},
"slot":{"id":1,"version":1,"start_h":8,"start_m":0,"end_h":8,"end_m":20,"doctor_id":1},
"clientId":1,"slotId":1}},{"slot":
{"id":2,"versione":1,"ora_inizio":8,"min_inizio":20,"ora_fine":8,"min_fine":40,"id_medico":1},
"rv":null},{"slot":{"id":3,"version":1,"startTime":8,"startMin":40,"endTime":9,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":4,"version":1,"startHour":9,"startMinute":0,"endHour":9,"endMinute":20,"doctorId":1},
"rv":null},{"slot":{"id":5,"version":1,"startHour":9,"startMinute":20,"endHour":9,"endMinute":40,"doctorId":1},
"rv":null},{"slot":{"id":6,"version":1,"startHour":9,"startMinute":40,"endHour":10,"endMinute":0,"doctorId":1},
"rv":null},{"slot":{"id":7,"version":1,"startHour":10,"startMinute":0,"endHour":10,"endMinute":20,"doctorId":1},
"rv":null},{"slot":{"id":8,"version":1,"startTime":10,"startMin":20,"endTime":10,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":9,"version":1,"startTime":10,"startMin":40,"endTime":11,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":10,"version":1,"startTime":11,"startMin":0,"endTime":11,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":11,"version":1,"startTime":11,"startMin":20,"endTime":11,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":12,"version":1,"startTime":11,"startMin":40,"endTime":12,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":13,"version":1,"startTime":14,"startMin":0,"endTime":14,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":14,"version":1,"startTime":14,"startMin":20,"endTime":14,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":15,"version":1,"startTime":14,"startMin":40,"endTime":15,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":16,"version":1,"startTime":15,"startMin":0,"endTime":15,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":17,"version":1,"startTime":15,"startMin":20,"endTime":15,"endMin":40,"doctorId":1},
"rv":null},{"slot":
{"id":18,"version":1,"startTime":15,"startMin":40,"endTime":16,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":19,"version":1,"startTime":16,"startMin":0,"endTime":16,"endMin":20,"doctorId":1},
"rv":null},{"slot":{"id":20,"version":1,"startTime":16,"startMin":20,"endTime":16,"endMin":40,"doctorId":1},
"rv":null},{"slot":{"id":21,"version":1,"startTime":16,"startMin":40,"endTime":17,"endMin":0,"doctorId":1},
"rv":null},{"slot":{"id":22,"version":1,"startTime":17,"startMin":0,"endTime":17,"endMin":20,"doctorId":1},
"rv":null},{"slot":
{"id":23,"version":1,"startTime":17,"startMin":20,"endTime":17,"endMin":40,"doctorId":1},
"rv":null},{"slot":
{"id":24,"version":1,"startTime":17,"startMin":40,"endTime":18,"endMin":0,"doctorId":1},
"rv":null}]}}

Abbiamo evidenziato il caso in cui c'è un appuntamento nella fascia oraria e il caso in cui non ce n'è nessuno.

3.5.8. Trova un medico in base al suo ID

URL
/getMedecinById/{idMedecin}
Risposta

Risposta<Doctor> :[int status ; List<String> messages ; Doctor doctor]
  • [doctorId]: l'ID del medico;

Esempio 1:

URL
/getDoctorById/1
Risposta
{"status":0,"messages":null,"doctor":
{"id":1,"version":1,"title":"Sig.ra",
"lastName":"PELISSIER","firstName":"Marie"}}

Esempio 2:

URL
/getMedecinById/100
Risposta
{"status":2,
"messages":["Il medico [100] non esiste"],"doctor":null}

3.5.9. Ottieni un cliente tramite ID

URL
/getClientById/{idClient}
Risposta

Risposta<Client> :[int status ; List<String> messages ;
 Client client]
  • [idClient]: l'ID del client;

Esempio 1:

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

Esempio 2:

URL
/getClientById/100
Risposta
{"status":2,"messages":["Il client [100] non esiste"],"client":null}

3.5.10. Prenota una fascia oraria utilizzando il tuo ID

URL
/getCreneauById/{idCreneau}
Risposta

Risposta<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [slotId]: l'ID dello slot;

Esempio 1:

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

Si noti che la risposta non include il medico titolare dello slot, ma solo il suo ID.

Esempio 2:

URL
/getCreneauById/100
Risposta
{"status":2,"messages":["Lo slot [100] non esiste"],
"slot":null}

3.5.11. Ottieni un appuntamento tramite il suo ID

URL
/getRvById/{idRv}
Risposta

Response<Rv> :[int status ; List<String> messages ; Rv rv]
  • [idRv]: l'ID dell'appuntamento;

Esempio 1:

URL
/getRvById/45
Risposta
{"status":0,"messages":null,"rv":{"id":45,"version":0,
"date":"2014-07-08","clientId":1,"slotId":1}}

Si noti che la risposta non include il cliente né la fascia oraria dell'appuntamento, ma solo i relativi identificatori.

Esempio 2:

URL
/getCreneauById/455
Risposta
{"status":2,"messages":["L'appuntamento [455] non esiste"],"rv":null}

3.5.12. Aggiungi un appuntamento

L'URL [/addAppointment] consente di aggiungere un appuntamento. Le informazioni necessarie per questa aggiunta (il giorno, la fascia oraria e il cliente) vengono inviate tramite una richiesta HTTP POST. Mostriamo come effettuare questa richiesta utilizzando lo strumento [Advanced Rest Client].

Image

  • in [1], l'URL oggetto della richiesta;
  • in [2], viene interrogato tramite una richiesta POST;
  • in [3-4], specifichiamo al server che i valori inviati sono in formato JSON;
  • in [4], l'intestazione di autenticazione HTTP;
  • in [5], le informazioni inviate tramite la richiesta POST. Si tratta di una stringa JSON contenente:
    • [day]: il giorno dell'appuntamento nel formato 'yyyy-mm-dd',
    • [idClient]: l'ID del cliente per il quale viene fissato l'appuntamento,
    • [idCreneau]: l'identificatore della fascia oraria dell'appuntamento. Poiché una fascia oraria appartiene a un medico specifico, questo si riferisce anche al medico;
  • in [6], la richiesta viene inviata;

La stringa JSON inviata è quella del seguente oggetto [PostAjouterRv]:


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

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 incluse [idClient, idCreneau], ma sono disponibili nei campi [client] e [creneau]. L'informazione importante è l'ID dell'appuntamento aggiunto (209). Il servizio web avrebbe potuto limitarsi a restituire questa singola informazione.

3.5.13. Eliminare un appuntamento

Anche questa operazione viene eseguita tramite una richiesta POST:

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

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

Il valore inviato è la stringa JSON di un oggetto di tipo [PostSupprimerRv] come segue:


public class PostSupprimerRv {
 
  // pOST DATA
  private long idRv;
 
  // manufacturers
  public PostSupprimerRv() {
 
  }
 
  public PostSupprimerRv(long idRv) {
    this.idRv = idRv;
  }
 
  // getters and setters
  ...
}
  • Riga 4: [idRv] è l'ID dell'appuntamento da eliminare.

Esempio 1:

URL
/cancellaAppuntamento
POST
{"idRv":209}
Risposta
{"status":0,"messages":null,"rv":null}

L'appuntamento n. 209 è stato eliminato con successo perché [status=0].

Esempio 2:

URL
/cancellaAppuntamento
POST
{"appointmentId":650}
Risposta
{"status":2,"messages":["L'appuntamento [650] non esiste"],"rv":null}

3.6. Il client Android

Image

Ora che il server [1] è stato descritto in dettaglio ed è attivo e funzionante, esamineremo il client Android [2].

3.6.1. Architettura del progetto Android Studio

Il progetto utilizza l'architettura del progetto [client-android-skel] (vedere la sezione 1.17). Nell'architettura del client Android mostrata sopra, sono presenti tre livelli distinti:

  • il livello [DAO] responsabile della comunicazione con il servizio web;
  • le [viste] incaricate di comunicare con l'utente;
  • l'[Activity] che funge da collegamento tra i due blocchi precedenti. Le viste non sono a conoscenza del livello [DAO]. Comunicano solo con l'Activity.

Questa architettura si riflette nel progetto Android Studio per il client Android:

 
  • il pacchetto [activity] implementa l'attività;
  • il pacchetto [architecture] include gli elementi architetturali che abbiamo sviluppato in precedenza;
  • il pacchetto [dao] implementa il livello [DAO];
  • il pacchetto [fragments] implementa le [views];

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 {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 4;
 
  // view n°s
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • righe 25, 28: personalizzazione del livello [DAO];
  • riga 31: questa applicazione effettua richieste autenticate al server;
  • riga 40: è richiesta un'immagine di caricamento;
  • 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] per gli 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 {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • 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 {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
 
  // list of doctors
  private List<Medecin> médecins;
  // customer list
  private List<Client> clients;
  // a doctor's diary for a given day
  private AgendaMedecinJour agenda;
  // position of clicked item in diary
  private int position;
  // rv day in English notation "yyyy-MM-dd"
  private String dayRv;
  // rv day in French notation "dd-MM-yyyy"
  private String jourRv;
 
  // getters and setters
...
}
  • Righe 17–28: La sessione memorizza sei informazioni. Spiegheremo il loro ruolo quando necessario.

3.6.3. Il livello [DAO]

  • in [1], le entità incapsulate nelle risposte del server. Queste sono state presentate nella Sezione 3.5;
  • in [2], i componenti client che gestiscono la comunicazione con il server;

Non torneremo sui componenti di [1]. Sono già stati presentati. Si invita il lettore a fare riferimento alla Sezione 3.5 se necessario. Esamineremo l'implementazione del pacchetto [service]. Questo ci porterà anche a discutere l'implementazione della comunicazione sicura tra il client e il server.

3.6.3.1. Implementazione della comunicazione client/server

  

La classe [WebClient] è un componente AA che descrive:

  • gli URL esposti dal servizio web;
  • i loro parametri;
  • le loro 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);
 
  // list of doctors
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();
 
  // customer list
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();
 
  // list of physician slots
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
 
  // list of doctor's appointments
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
 
  // Customer
  @Get("/getClientById/{id}")
  public Response<Client> getClientById(@Path long id);
 
  // Doctor
  @Get("/getMedecinById/{id}")
  public Response<Medecin> getMedecinById(@Path long id);
 
  // Rv
  @Get("/getRvById/{id}")
  public Response<Rv> getRvById(@Path long id);
 
  // Niche
  @Get("/getCreneauById/{id}")
  public Response<Creneau> getCreneauById(@Path long id);
 
  // add a RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);
 
  // delete an appointment
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
 
  // get a doctor's schedule
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
 
}
  • righe 19–60: sono presenti tutti gli URL discussi nella sezione 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 {
  // Web service url
  public void setUrlServiceWebJson(String url);
 
  // user
  public void setUser(String user, String mdp);
 
  // customer timeout
  public void setTimeout(int timeout);
 
  // customer list
  public Observable<List<Client>> getAllClients();
 
  // list of doctors
  public Observable<List<Medecin>> getAllMedecins();
 
  // list of physician slots
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
 
  // list of doctor's appointments on a given day
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
 
  // find a customer identified by its id
  public Observable<Client> getClientById(long id);
 
  // find a doctor identified by his id
  public Observable<Medecin> getMedecinById(long id);
 
  // find an Rv identified by its id
  public Observable<Rv> getRvById(long id);
 
  // find a time slot identified by its id
  public Observable<Creneau> getCreneauById(long id);
 
  // add a RV to the list
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
 
  // delete a RV
  public Observable<Rv> supprimerRv(long idRv);
 
  // job
  public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • riga 10: per impostare l'URL del servizio web / JSON;
  • riga 13: per impostare l'utente per la comunicazione client/server. [user] è l'ID utente, [password] è la password;
  • riga 16: per impostare un timeout massimo per la risposta del server;
  • righe 18–49: ogni URL esposto dal servizio web corrisponde a un metodo. Utilizzano le stesse firme di metodo del componente AA [WebClient];
  • riga 52: per controllare la modalità di debug del livello [DAO];

3.6.3.3. La classe [Dao]

  

L'implementazione [DAO] della precedente interfaccia [IDao] è 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 {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    ...
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    ...
  }
 
  @Override
  public void setUser(String user, String mdp) {
    ...
  }
 
  @Override
  public void setTimeout(int timeout) {
    ...
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
 
  }
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // implementation of the IDao interface --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // log
    log("getAllClients");
    // result
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }
 
  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // log
    log("getAllMedecins");
    // result
    return getResponse(new IRequest<Response<List<Medecin>>>() {
      @Override
      public Response<List<Medecin>> getResponse() {
        return webClient.getAllMedecins();
      }
    });
  }
 
  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
    // log
    log("getAllCreneaux");
    // result
    return getResponse(new IRequest<Response<List<Creneau>>>() {
      @Override
      public Response<List<Creneau>> getResponse() {
        return webClient.getAllCreneaux(idMedecin);
      }
    });
  }
 
  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getRvMedecinJour");
    // result
    return getResponse(new IRequest<Response<List<Rv>>>() {
      @Override
      public Response<List<Rv>> getResponse() {
        return webClient.getRvMedecinJour(idMedecin, jour);
      }
    });
  }
 
  @Override
  public Observable<Response<Client>> getClientById(final long id) {
    // log
    log("getClientById");
    // result
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Medecin>> getMedecinById(final long id) {
    // log
    log("getMedecinById");
    // result
    return getResponse(new IRequest<Response<Medecin>>() {
      @Override
      public Response<Medecin> getResponse() {
        return webClient.getMedecinById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Rv>> getRvById(final long id) {
    // log
    log("getRvById");
    // result
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.getRvById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // log
    log("getCreneauById");
    // result
    return getResponse(new IRequest<Response<Creneau>>() {
      @Override
      public Response<Creneau> getResponse() {
        return webClient.getCreneauById(id);
      }
    });
  }
 
  @Override
  public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
    // log
    log("ajouterRv");
    // result
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
      }
    });
  }
 
  @Override
  public Observable<Response<Rv>> supprimerRv(final long idRv) {
    // log
    log("supprimerRv");
    // result
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.supprimerRv(new PostSupprimerRv(idRv));
      }
    });
  }
 
  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // result
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }
 
}
  • righe 18–72: queste sono le righe predefinite nella classe [Dao] del progetto [client-android-skel];
  • righe 74–216: implementazione dell'interfaccia [IDao]. I metodi che interrogano gli URL esposti dal servizio web delegano questa interrogazione al componente AA [WebClient] (righe 22–23);
  • righe 58–63: se gli scambi client/server sono autenticati tramite autenticazione di base, viene aggiunto un intercettatore al componente [RestTemplate]. Ciò farà sì che qualsiasi richiesta HTTP inviata dal componente [RestTemplate] venga 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 {
 
  // user
  private String user;
  private String mdp;
 
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    HttpHeaders headers = request.getHeaders();
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    headers.setAuthorization(auth);
    return execution.execute(request, body);
  }
 
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}
  • riga 15: la classe [MyAuthInterceptor] è un componente AA di tipo [singleton];
  • riga 16: la classe [MyAuthInterceptor] estende l'interfaccia Spring [ClientHttpRequestInterceptor]. Questa interfaccia ha un metodo, il metodo [intercept] alla riga 22. Estendiamo questa interfaccia per intercettare qualsiasi richiesta HTTP proveniente dal client. Il metodo [intercept] accetta tre parametri;
    • [HttpRequest request]: la richiesta HTTP intercettata,
    • [byte[] body]: il suo corpo, se presente (ad esempio, valori inviati via 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 presentata nella Sezione 3.5.

  • riga 23: recuperiamo le intestazioni HTTP della richiesta intercettata;
  • riga 24: creiamo l'intestazione di autenticazione HTTP. Il metodo di autenticazione utilizzato (codifica Base64 della stringa 'user:mdp') è fornito dalla classe Spring [HttpBasicAuthentication];
  • riga 25: l'intestazione di autenticazione appena creata viene aggiunta alle intestazioni correnti della richiesta intercettata;
  • riga 26: continuiamo l'esecuzione della richiesta intercettata. In sintesi, la richiesta intercettata è stata arricchita con l'intestazione di autenticazione;

Le implementazioni dei metodi nell'interfaccia [IDao] seguono tutte lo stesso schema. Prendiamo l'esempio del metodo [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // result
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • Riga 2: Il metodo richiede due parametri:
    • [idMedecin]: l'ID del medico di cui si desidera l'orario;
    • [day]: il giorno per il quale si desidera l'agenda;
  • riga 6: chiamiamo 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 previsto dal metodo [getAgendaMedecinJour] alla riga 2, di tipo [Response<AgendaMedecinJour>];
  • riga 9: la risposta viene restituita dal metodo [webClient.getAgendaMedecinJour]:

  // get a doctor's schedule
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

I parametri utilizzati nella riga 9 sono quelli passati al metodo [getAgendaMedecinJour] nella riga 2. Per questo motivo, tali parametri devono avere l'attributo final;

3.6.4. Il [MainActivity]

Server
  

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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // parent class ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
    return fragments;
  }
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
 
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  // interface IDao -----------------------------------------------------
...
 
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    return dao.getAllClients();
  }
 
  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    return dao.getAllMedecins();
  }
 
  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
    return dao.getAllCreneaux(idMedecin);
  }
 
  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
    return dao.getRvMedecinJour(idMedecin, jour);
  }
 
  @Override
  public Observable<Response<Client>> getClientById(long id) {
    return dao.getClientById(id);
  }
 
  @Override
  public Observable<Response<Medecin>> getMedecinById(long id) {
    return dao.getMedecinById(id);
  }
 
  @Override
  public Observable<Response<Rv>> getRvById(long id) {
    return dao.getRvById(id);
  }
 
  @Override
  public Observable<Response<Creneau>> getCreneauById(long id) {
    return dao.getCreneauById(id);
  }
 
  @Override
  public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
    return dao.ajouterRv(jour, idCreneau, idClient);
  }
 
  @Override
  public Observable<Response<Rv>> supprimerRv(long idRv) {
    return dao.supprimerRv(idRv);
  }
 
  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
    return dao.getAgendaMedecinJour(idMedecin, jour);
  }
}
  • righe 21–66: queste righe sono fornite di default nel modello [client-android-skel];
  • righe 66–119: implementazione dell'interfaccia [IDao]. Tutti i metodi delegano il lavoro al livello [DAO] alla 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 visualizzata all'avvio dell'applicazione;

3.6.5. La sessione

  

La classe [Session] viene utilizzata per memorizzare le informazioni che devono essere passate 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 {
  // list of doctors
  private List<Medecin> médecins;
  // customer list
  private List<Client> clients;
  // agenda
  private AgendaMedecinJour agenda;
  // position of clicked item in diary
  private int position;
  // rv day in English notation "yyyy-MM-dd"
  private String dayRv;
  // rv day in French notation "dd-MM-yyyy"
  private String jourRv;
 
 
  // getters and setters
...
}
  • riga 10: la classe [Session] è un componente AA istanziato come singola istanza;
  • righe 12–15: in questo caso di studio, supporremo che gli elenchi di medici e clienti non cambino. Li recupereremo all'avvio dell'applicazione e li memorizzeremo nella sessione in modo che i frammenti possano utilizzarli;
  • righe 20–23: la data desiderata per un appuntamento. Viene gestita in due formati: in notazione francese (riga 23) all'interno del client Android e in notazione inglese (riga 21) per la comunicazione con il server;
  • riga 19: la posizione dell'elemento cliccato (link aggiungi/elimina) sul calendario;

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:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

N.
Tipo
Nome
1
Modifica testo
edtUrlServiceRest
3
Modifica testo
edtUser
5
Modifica testo
edtPassword
2
TextView
txtErroreUrlServizioRest
3
TextView
txtErroreUtente

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 {
 
  // visual interface elements
  @ViewById(R.id.edt_urlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_errorUrlServiceRest)
  protected TextView txtErrorUrlServiceRest;
  @ViewById(R.id.txt_errorUtilisateur)
  protected TextView txtErrorUtilisateur;
  @ViewById(R.id.edt_utilisateur)
  protected EditText edtUtilisateur;
  @ViewById(R.id.edt_mdp)
  protected EditText edtMdp;
 
  // seizures
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // implementation methods parent class -------------------------------------------
 ...
 
}
  • 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 del modulo;

Il clic sull'opzione di menu [Convalida] viene gestito dal metodo [doValidate]:


// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // hide any previous error messages
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // enter the URL of the web service
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // user information
    mainActivity.setUser(utilisateur, mdp);
    // start of wait - 2 asynchronous tasks will be launched
    beginWaiting(2);
    // doctors
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // we consume the answer
        consumeMedecins(responseMedecins);
      }
    });
    // customers
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // we consume the answer
        consumeClients(responseClients);
      }
    });
  }
 
 
  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // mistake?
    if (responseMedecins.getStatus() != 0) {
      // message
      showAlert(responseMedecins.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // doctors are saved in the session
    session.setMédecins(responseMedecins.getBody());
  }
 
  private void consumeClients(Response<List<Client>> responseClients) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // mistake?
    if (responseClients.getStatus() != 0) {
      // message
      showAlert(responseClients.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // customers are stored in the session
    session.setClients(responseClients.getBody());
  }
  • righe 8–10: viene verificata la validità dei tre campi del modulo. Se il modulo non è valido, il processo si interrompe in questa fase;
  • righe 11–14: gli input richiesti dal livello [DAO] vengono passati all'attività;
  • riga 16: la classe padre viene informata che verranno avviate due attività asincrone e viene preparata 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 osservato, in questo caso [Response<List<Medecin>>]
  • riga 22: quando viene ricevuta la risposta, questa viene passata al metodo [consumeMedecins] alla riga 36;
  • righe 25–33: dopo aver avviato un primo task asincrono, ne avviamo un secondo per richiedere l'elenco dei clienti. Avremo quindi due task in esecuzione in parallelo;
  • righe 36–52: abbiamo ricevuto la risposta dall'attività relativa ai medici. La elaboriamo;
  • righe 42–49: per prima cosa, controlliamo 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: annulliamo tutte le attività;
  • riga 48: torniamo all'interfaccia utente;
  • riga 51: se non c'è stato alcun errore, l'elenco dei medici viene caricato nella sessione;

La validità dell'input (riga 8) viene verificata utilizzando il seguente metodo:


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

Il metodo [beginWaiting] (riga 16) è il seguente:


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • riga 4: comunichiamo al task padre che stiamo per avviare [numberOfRunningTasks] task;
  • riga 6: tutte le opzioni del menu vengono nascoste;
  • riga 7: quindi rende visibile l'opzione [Azioni/Annulla];

Il clic sull'opzione di menu [Annulla] viene gestito dal metodo [doCancel]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • riga 8: chiediamo 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 {
 
  // visibility of two error messages
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;
 
  // getters and setters
...
}
  • Quando la classe padre lo richiede, il frammento salverà la visibilità dei suoi due messaggi di errore;

Il ciclo di vita del frammento è implementato come segue:


// implementation methods parent class -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // save fragment status
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
    state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return     IMainActivity.VUE_CONFIG;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    if (previousState == null) {
      // 1st visit
      // hide error messages
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore error msg visibility
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // not the 1st visit - error messages are returned
    txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
  }
 
 
  @Override
  protected void notifyEndOfUpdates() {
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }
 
  // méthodes privées ------------------------------------------------
  private void initMenu(){
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • righe 2–9: quando richiesto dalla classe padre, il frammento salva lo stato dei suoi due messaggi di errore;
  • righe 11-14: l'ID del frammento è [IMainActivity.VUE_CONFIG];
  • righe 16–19: eseguite quando il frammento viene generato per la prima volta (previousState == null) o rigenerato in occasioni successive (previousState != null). Qui non c'è nulla da fare;
  • righe 21–31: eseguito quando la vista associata al frammento viene costruita per la prima volta (previousState == null) o ricostruita in occasioni successive (previousState != null);
    • righe 24–29: alla prima visita, i messaggi di errore vengono nascosti e il menu viene visualizzato senza l'azione [Cancel] (righe 62–66);
  • righe 33–35: eseguite quando si raggiunge il frammento tramite un'operazione [SUBMIT]. Qui ciò non accade mai;
  • righe 37–44: eseguite quando si raggiunge il frammento tramite un'operazione [NAVIGATION] o [RESTORE]. Lo stato dei messaggi di errore viene ripristinato dallo stato precedente;
  • righe 47–49: eseguite quando tutti gli aggiornamenti precedenti sono stati effettuati. Non c'è altro da fare;
  • righe 51–59: eseguite quando tutte le attività asincrone sono state completate;
    • righe 53–54: ripristina il menu allo stato predefinito;
    • righe 56–58: se le attività sono state completate con successo, si passa alla vista successiva; altrimenti, si rimane sulla stessa vista;

3.6.7. Gestione della vista Home

3.6.7.1. La vista

La vista iniziale è la seguente:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

No.
Tipo
Nome
1
Spinner
spinnerMedici
2
DatePicker
Modifica appuntamento

3.6.7.2. Il frammento

La schermata iniziale è gestita dal seguente frammento [HomeFragment]:

 

package client.android.fragments.behavior;
 
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
 
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
 
@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;
 
  // local data
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...
 
  // implementation methods parent class -------------------------------------
...
}
  • 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: l'origine dati per lo spinner dei medici;

Il clic sul link [Convalida] viene gestito dal seguente metodo [doValidate]:


// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // note the id of the selected doctor
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // the day is saved in the session
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // switch to date format yyyy-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // start wait - 1 asynchronous task will be launched
    beginWaiting(1);
    // we ask for the doctor's diary
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
 
      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // we consume the answer
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }
 
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • riga 5: recupera l'ID del medico selezionato;
  • righe 7-8: memorizziamo la data selezionata nella sessione in formato francese;
  • righe 10-11: impostiamo la data selezionata nella sessione, in formato inglese;
  • riga 13: notifichiamo alla classe padre che stiamo per avviare un'attività asincrona e ci prepariamo all'attesa;
  • righe 15–22: viene recuperato l'orario del medico;
    • riga 15: il metodo [executeInBackground] richiede due parametri:
      • riga 15: il processo da eseguire e osservare è 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 osservato, in questo caso [Response<AgendaMedecinJour>]
    • riga 20: quando viene ricevuta la risposta, questa viene passata al metodo [consumeAgenda] alla riga 25;
  • righe 25–37: abbiamo ricevuto l'agenda del medico. La elaboriamo;
  • righe 27–34: per prima cosa, controlliamo se il server ha segnalato un errore nel campo [status] della risposta;
  • riga 29: se c'è un errore, visualizziamo i messaggi che il server ha inserito nel campo [messages] della risposta;
  • riga 31: annulliamo tutte le attività;
  • riga 33: torniamo all'interfaccia utente;
  • riga 36: se non ci sono stati errori, il calendario viene portato in primo piano;

Il metodo [beginWaiting] (riga 13) è il seguente:


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • riga 4: comunichiamo al task padre che stiamo per avviare [numberOfRunningTasks] task;
  • riga 6: tutte le opzioni del menu vengono nascoste;
  • riga 7: quindi rende visibile l'opzione [Azioni/Annulla];

Il clic sull'opzione di menu [Annulla] viene gestito dal metodo [doCancel]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • riga 8: chiediamo alla classe padre di annullare le attività asincrone;

Cliccando sull'opzione di menu [Torna alle impostazioni], la gestione avviene come segue:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Riga 4: Passiamo alla vista di configurazione utilizzando l'azione [NAVIGATION]. Ciò significa che vogliamo ripristinare la vista di configurazione allo stato in cui l'avevamo lasciata;

3.6.7.3. Gestione del ciclo di vita dei frammenti

Il frammento ha il seguente [HomeFragmentState]:


package client.android.fragments.state;
 
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;
 
public class AccueilFragmentState extends CoreState {
 
  // fragment status [Home]
  // selected doctor's position
  private int selectedMedecinPosition;
  // selected date
  private int year;
  private int month;
  private int dayOfMonth;
  // doctors' spinner data source
  private String[] spinnerMedecinsDataSource;
 
  // manufacturers
  public AccueilFragmentState() {
 
  }
 
  // getters and setters
...
}
  • riga 11: restituisce l'elemento selezionato dall'elenco dei medici;
  • righe 13–15: restituisce la data selezionata dal calendario;
  • riga 17: recupera l'origine dati per l'elenco dei medici;

Il ciclo di vita del frammento è implementato come segue:


// implementation methods parent class -------------------------------------
  @Override
  public CoreState saveFragment() {
    // save the view
    AccueilFragmentState state = new AccueilFragmentState();
    state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
    state.setDayOfMonth(edtJourRv.getDayOfMonth());
    state.setMonth(edtJourRv.getMonth());
    state.setYear(edtJourRv.getYear());
    state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_ACCUEIL;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // we get the doctors back in session
    medecins = session.getMédecins();
    // 1st visit?
    if (previousState == null) {
      // we build the table displayed by the spinner
      spinnerMedecinsDataSource = new String[medecins.size()];
      int i = 0;
      for (Medecin medecin : medecins) {
        spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
        i++;
      }
    } else {
      // no 1st visit
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // the calendar
    calendrier = Calendar.getInstance();
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // we associate the doctors' spinner with its data source
    ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
    dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerMedecins.setAdapter(dataAdapterMedecins);
    // minimum calendar date to today
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // 1st visit?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // menu
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore the state currently in session
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // selection in doctors' spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // calendar
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // called after all tasks have been completed or cancelled
    // menu status
    initMenu();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }
 
  // méthodes privées ------------------------------------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • righe 2–9: quando richiesto dalla classe padre, 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: l'origine dati per lo spinner dei medici;
  • righe 14-17: l'ID del frammento è [IMainActivity.VUE_ACCUEIL];
  • righe 19–39: eseguite quando il frammento viene generato per la prima volta (previousState == null) o rigenerato in occasioni successive (previousState != null);
    • righe 25–31: per una prima visita, viene costruita la fonte dati per lo spinner dei medici;
    • righe 33–35: per le visite successive, l'origine 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 visite successive (previousState !=null);
    • righe 50–53: per la prima visita, il menu viene visualizzato senza l'azione [Annulla] (righe 88–92);
    • righe 43–48: per tutte le visite, che siano la prima o meno, lo spinner dei medici viene associato alla sua fonte (righe 44–46) e la data minima sul calendario viene impostata sulla data odierna (riga 48);
  • righe 56–60: eseguite quando si raggiunge il frammento tramite un'operazione [SUBMIT]. L'utente proviene dalla vista [CONFIG]. Il menu viene riportato allo stato iniziale;
  • righe 62–70: eseguite quando si raggiunge il frammento tramite un'operazione [NAVIGATION] o [RESTORE];
    • riga 67: lo spinner dei medici viene reimpostato sull'ultimo medico selezionato;
    • riga 69: il calendario viene impostato sull'ultima data selezionata;
  • righe 72–74: eseguite una volta completati tutti gli aggiornamenti precedenti. Non c'è altro da fare;
  • righe 76–85: eseguite quando tutte le attività asincrone sono complete;
    • riga 80: ripristina il menu allo stato predefinito;
    • righe 82–84: se le attività sono state completate normalmente, passare alla vista successiva; altrimenti, rimanere sulla stessa vista;

3.6.8. Gestione della vista Calendario

3.6.8.1. La vista

La schermata iniziale si presenta così:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

No.
Tipo
Nome
1
TextView
txtTitle2
2
Elenco
slotList

3.6.8.2. Il frammento

La vista Calendario è 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 {
 
  // visual interface elements
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;
 
  // agenda displayed by the fragment
  private AgendaMedecinJour agenda;
  // info ListView slots
  private int firstPosition;
  private int top;
  // appointment deleted or not
  private boolean rdvSupprimé;
  // slot number added or deleted
  private int numCréneau;
 
  // update schedule after adding/deleting
  private void updateAgenda() {
  ...
  }
 
...
 
  // implementation methods parent class ------------------------------------------------------
  ...
}
  • 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: elementi dell'interfaccia visiva;
  • righe 37-45: dati globali per i metodi;

3.6.8.2.1. Metodo [updateAgenda]

La (ri)generazione dell'elenco degli slot del calendario è richiesta in diversi punti del codice. È stata implementata nel seguente metodo privato [updateAgenda]:


  // update schedule after adding/deleting
  private void updateAgenda() {
    // (re)generation of calendar slots
    // the agenda is taken from the session and stored in a fragment field
    agenda = session.getAgenda();
    // regeneration of ListView slots
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // we reposition ourselves at the right spot on the ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • riga 5: il calendario viene recuperato dalla sessione e memorizzato nel campo [calendar] del frammento;
  • righe 7–9: definiamo l'adattatore per il componente [ListView]. Questo adattatore definisce sia l'origine dati per [ListView] sia il modello di visualizzazione per ciascuno dei suoi elementi. Presenteremo questo adattatore tra poco;
  • riga 11: torniamo alla posizione precedente nel calendario. Questo perché vediamo solo una parte degli slot temporali della giornata. Se aggiungiamo o rimuoviamo un appuntamento nell'ultimo slot, il codice sopra riportato aggiornerà la pagina per visualizzare il nuovo calendario. Questo aggiornamento fa sì che la vista torni al primo slot, il che non è auspicabile. La riga 5 risolve questo problema. Una descrizione di questa soluzione è disponibile all'URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

La classe [ListCreneauxAdapter] viene utilizzata per definire una riga nella [ListView]:

Image

Come mostrato sopra, la visualizzazione varia a seconda che lo slot temporale abbia o meno un appuntamento. Il codice per la classe [ListCreneauxAdapter] è il seguente:


...
 
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
 
    // time slot table
    private CreneauMedecinJour[] creneauxMedecinJour;
    // execution context
    private Context context;
    // the layout id for displaying a line in the slot list
    private int layoutResourceId;
    // click listener
    private AgendaFragment vue;
 
    // manufacturer
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // memorize information
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // sort the table of slots in schedule order
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ...
}
 
// sorting the slot table
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • Riga 3: La classe [ListCreneauxAdapter] deve estendere un adattatore predefinito per le [ListView], in questo caso la classe [ArrayAdapter], che, come suggerisce il nome, popola la [ListView] con un array di oggetti, in questo caso di tipo [CreneauMedecinJour]. Rivediamo il codice di questa entità:

public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
...  
}
  • La classe [CreneauMedecinJour] contiene una fascia oraria (riga 5) e un potenziale appuntamento (riga 6) o null se non c'è alcun appuntamento;

Torniamo al codice della classe [ListCreneauxAdapter]:

  • riga 15: il costruttore accetta quattro parametri:
    1. l'attività Android corrente,
    2. il file XML che definisce il contenuto di ciascun elemento [ListView],
    3. l'array delle fasce orarie del medico,
    4. la vista stessa;
  • Riga 24: L'array delle fasce orarie è ordinato in ordine crescente in base all'ora;

Il metodo [getView] è responsabile della generazione della vista corrispondente a una riga nella [ListView]. Questa vista è composta da tre elementi:

 
No.
ID
Tipo
Ruolo
1
txtCreneau
TextView
fascia oraria
2
txtClient
TextView
il cliente
3
btnConferma
TextView
link per aggiungere/eliminare un appuntamento

Il codice per il metodo [getView] è il seguente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // we position ourselves in the right niche
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // create the line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the time slot
        TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
        txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
                .getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
        // the customer
        TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
        String text;
        if (creneauMedecin.getRv() != null) {
            Client client = creneauMedecin.getRv().getClient();
            text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        } else {
            text = "";
        }
        txtClient.setText(text);
        // the link
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // add
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // delete
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // link listener
        btnValider.setOnClickListener(new OnClickListener() {
 
            @Override
            public void onClick(View v) {
                // we skip the news on the calendar view
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // we return the line
        return row;
    }
  • riga 2: position è il numero della riga da generare nella [ListView]. È anche il numero dello slot nell'array [creneauxMedecinJour]. Ignoriamo gli altri due parametri;
  • riga 4: recuperiamo la fascia oraria da visualizzare nella riga della [ListView];
  • riga 6: la riga viene costruita in base alla sua definizione XML
 

Il codice per [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 generato lo slot temporale [1];
  • righe 12–20: viene generato l'ID cliente [2];
  • riga 23: se la fascia oraria non ha alcun appuntamento;
  • righe 25-26: viene creato il link blu [Aggiungi];
  • righe 29-30: altrimenti, viene creato il link rosso [Elimina];
  • righe 33-40: indipendentemente dal tipo di link [Aggiungi / Elimina], il metodo [doValider] della vista gestirà il clic sul link. Il metodo riceverà due argomenti:
    1. il numero dello slot su cui è stato cliccato,
    2. l'etichetta del link su cui è stato cliccato;
  • riga 42: restituiamo la riga appena creata.

Si noti che è il metodo [doValider] del frammento [AgendaFragment] a gestire i link. È il seguente:


  // click on a link [Add / Remove]
  public void doValider(int numCréneau, String texte) {
    // operation in progress?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // note the scroll position to return to it
    // read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // we also note the number of the clicked slot
    this.numCréneau = numCréneau;
    // depending on the text of the link, we do not do the same thing
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • Il metodo [doValider] riceve due informazioni:
    • il numero dello slot su cui è stato cliccato;
    • il testo (Aggiungi / Elimina) del link su cui è stato cliccato;
  • righe 4–7: il clic sui link [Elimina / Aggiungi] è disabilitato se sono in corso attività asincrone. Si tratta di una scelta progettuale che semplifica la scrittura del codice. È aperta alla discussione;
  • righe 11–15: memorizziamo le informazioni (firstPosition, top) dallo slot ListView in campi all'interno del frammento in modo che il metodo privato [updateAgenda] possa rigenerarlo con la stessa posizione di scorrimento;
  • riga 17: memorizziamo il numero dello slot cliccato;
  • righe 19–23: a seconda del testo del link cliccato, aggiungiamo o rimuoviamo un elemento;

3.6.8.2.2. Metodo [doDelete]

Il metodo [doSupprimer] garantisce la rimozione dell'appuntamento dallo slot cliccato:


// deleting an appointment
  private void doSupprimer() {
    // waiting for two tasks to be completed
    beginWaiting(2);
    // delete the Rdv in the background
    rdvSupprimé = false;
    // rv identifier to be deleted
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // deletion by an asynchronous task
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
 
      @Override
      public void call(Response<Rv> responseRv) {
        // income consumption
        consumeRv(responseRv);
      }
    });
  }
 
  // consumption of an answer
  private void consumeRv(Response<Rv> responseRv) {
    // mistake?
    if (responseRv.getStatus() != 0) {
      // message
      showAlert(responseRv.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // we note that the appointment has been cancelled
    rdvSupprimé = true;
    // the most recent agenda is requested
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {
 
        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // we consume the answer
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }
 
  // diary consumption
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // update the view's agenda
    updateAgenda();
  }
  • riga 4: notifichiamo alla classe padre che stiamo per avviare due attività asincrone e iniziamo ad attendere il completamento di queste due attività;
  • riga 8: recuperiamo l'ID dell'appuntamento da eliminare. Il server ha bisogno di questa informazione;
  • righe 9–18: richiediamo l'eliminazione dell'appuntamento tramite un'attività asincrona;
    • riga 10: il metodo [executeInBackground] richiede due parametri:
      • riga 10: il processo da eseguire e osservare è fornito dal metodo [mainActivity.deleteRv(idRv)];
      • righe 10–17: il secondo parametro è un'istanza di tipo [Action1<T>], dove T è il tipo restituito dal processo osservato, in questo caso [Response<Rv>]
    • riga 15: quando viene ricevuta la risposta, questa viene passata al metodo [consumeRv] alla riga 21;
  • righe 21–44: abbiamo ricevuto la risposta dall'attività asincrona. La elaboriamo;
  • righe 23–30: per prima cosa, controlliamo se il server ha segnalato un errore nel campo [status] della risposta;
    • riga 25: se c'è un errore, visualizziamo i messaggi che il server ha inserito nel campo [messages] della risposta;
    • riga 27: annulliamo tutte le attività;
    • riga 29: torniamo all'interfaccia utente;
  • riga 32: se non si sono verificati errori, segnaliamo che l'appuntamento è stato cancellato;
  • righe 34–43: invece di limitarsi a cancellare l'appuntamento dal calendario attualmente visualizzato dal frammento, richiediamo il nuovo calendario del medico. Poiché l'applicazione è multiutente, anche altri utenti potrebbero aver modificato il calendario del medico. Pertanto, è meglio utilizzare la versione più recente;
  • righe 34–43, 47–61: ripetiamo ciò che è stato fatto nel frammento [AccueilFragment], questa volta utilizzando le informazioni recuperate dalla sessione;

Il metodo [beginWaiting] (riga 4) è il seguente:


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • riga 4: comunichiamo al task padre che stiamo per avviare [numberOfRunningTasks] task;
  • riga 6: tutte le opzioni del menu sono nascoste;
  • riga 7: quindi rendiamo visibile l'opzione [Azioni/Annulla];

3.6.8.2.3. Metodo [doCancel]

Il clic sull'opzione di menu [Annulla] viene gestito dal metodo [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • riga 7: chiediamo alla classe padre di annullare le attività asincrone;

3.6.8.2.4. Opzione di menu [Torna alla configurazione]

Cliccando sull'opzione di menu [Torna alla configurazione] si procede come segue:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigate to the configuration view
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Riga 4: Passiamo alla vista di configurazione utilizzando l'azione [NAVIGATION]. Ciò significa che vogliamo ripristinare la vista di configurazione allo stato in cui l'avevamo lasciata;

3.6.8.2.5. Opzione di menu [Torna alla home]

Il clic sull'opzione di menu [Torna alla Home] viene gestito in modo simile:


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

3.6.8.3. Gestione del ciclo di vita dei frammenti

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 {
 
  // title view
  private String titre;
  // ListView
  private int firstPosition;
  private int top;
 
  // manufacturers
  public AgendaFragmentState() {
 
  }
 
  public AgendaFragmentState(String titre) {
    this.titre = titre;
  }
 
  // getters and setters
...
}
  • riga 10: il titolo visualizzato nella parte superiore della vista;
  • righe 12-13: abilita lo scorrimento della ListView che mostra gli slot disponibili del medico;

Il ciclo di vita del frammento è implementato come segue:


// implementation methods parent class ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // save status
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // note the scroll position to return to it
    // read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // we memorize it all
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AGENDA;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // 1st visit?
    if (previousState != null) {
      // not the 1st visit
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // and information from ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }
 
  @Override
  protected void initView(CoreState previousState) {
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // get the agenda
    agenda = session.getAgenda();
    // generate the page title
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // menu status
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // regenerate the page title
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // regenerate the slot list
    updateAgenda();
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu status
    initMenu();
    // if cancelled but appointment deleted, update local calendar
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // we delete the appointment from the local calendar (we were unable to access the global calendar)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // update the visual interface
      updateAgenda();
    }
  }
 
 
  // méthodes privées ------------------------------------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • righe 2–19: quando richiesto dalla classe padre, 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 ripristinare lo scorrimento della ListView;
  • righe 21–24: l'ID del frammento è [IMainActivity.VUE_AGENDA];
  • righe 26–35: eseguite quando il frammento viene generato per la prima volta (previousState == null) o rigenerato nelle visite successive (previousState != null);
    • righe 30–34: se questa non è la prima visita al frammento, recuperiamo le informazioni (top, firstPosition) necessarie per ripristinare lo stato di scorrimento della ListView;
  • righe 38–40: eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState == null) o ricostruita nelle visite successive (previousState != null). Qui non c'è nulla da fare perché la ListView degli slot verrà generata dal metodo privato [updateAgenda] (righe 61-65);
  • righe 42–52: eseguite quando si raggiunge il frammento tramite un'operazione [SUBMIT]. Proveniamo dalla vista [HOME];
    • riga 45: recuperiamo l'agenda impostata da [AccueilFragment];
    • righe 47–49: viene generato il titolo della vista;
    • la ListView degli slot temporali verrà generata dal metodo privato [updateAgenda] (righe 61-65);
  • righe 54–59: eseguite quando si raggiunge il frammento tramite un'operazione [NAVIGATION] o [RESTORE];
    • righe 57-58: il titolo della vista viene rigenerato;
    • la ListView degli slot temporali verrà generata dal metodo privato [updateAgenda] (righe 61–65);
  • righe 72–74: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. La ListView degli slot temporali viene aggiornata perché questo aggiornamento è necessario indipendentemente da come si accede al frammento;
  • righe 67–77: eseguite quando tutte le attività asincrone sono complete;
    • riga 70: il menu viene riportato allo stato predefinito (righe 82–86);
    • riga 72: c'erano due attività asincrone. Verifichiamo se la prima (eliminazione dell'appuntamento) è andata a buon fine, nonostante una cancellazione;
    • riga 74: in tal caso, l'appuntamento viene eliminato dal calendario locale
    • riga 75: e aggiorniamo la visualizzazione del calendario;

3.6.9. Gestione della vista "Aggiungi appuntamento"

3.6.9.1. La vista

La vista per l'aggiunta di un appuntamento è la seguente:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

N.
Tipo
Nome
1
TextView
txtTitle2
2
Spinner
spinnerClienti

3.6.9.2. Il frammento

La vista per l'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 {
 
  // visual interface elements
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;
 
  // our customers
  private List<Client> clients;
 
  // local data
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
 
  // validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...
 
  // implementation methods parent class ----------------------------------
...
}
  • 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 client;
  • riga 43: l'origine dati per il selettore del cliente;

Il clic sul link [Validate] viene gestito dal seguente metodo [doValidate]:


  // our customers
  private List<Client> clients;
 
  // local data
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// validation page
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // the selected customer is retrieved
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // start waiting for 2 asynchronous tasks
    beginWaiting(2);
    // we add the RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {
 
        @Override
        public void call(Response<Rv> responseRv) {
          // we consume the answer
          consumeRv(responseRv);
        }
      });
  }
 
  // consumption of a Response<Rv> object
  void consumeRv(Response<Rv> responseRv) {
    // mistake?
    if (responseRv.getStatus() != 0) {
      // message
      showAlert(responseRv.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // note that the rdv has been added
    rdvAjouté = true;
    // memorize the appointment
    this.rv = responseRv.getBody();
    // we ask for the new agenda
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
 
      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // we consume the answer
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }
 
  // consumption of a Response<AgendaMedecinJour> object
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // mistake?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // message
      showAlert(responseAgendaMedecinJour.getMessages());
      // cancellation
      doAnnuler();
      // back to UI
      return;
    }
    // put the agenda in the session
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • riga 13: quando il metodo [doValider] ha inizio, i campi 2, 5, 6 e 9 sono stati inizializzati durante il ciclo di vita del frammento. Vedremo come;
  • riga 15: recuperiamo l'entità [Client] corrispondente all'elemento selezionato nel menu a rotella del client;
  • riga 17: notifichiamo alla classe padre che stiamo per avviare due attività asincrone e ci prepariamo all'attesa;
  • riga 19: inizialmente, l'appuntamento non è ancora stato aggiunto al calendario del medico;
  • righe 20–30: chiediamo al server di aggiungere un appuntamento;
    • riga 20: il metodo [executeInBackground] richiede due parametri:
      • riga 20: il processo da eseguire e osservare è fornito dal metodo [mainActivity.addRv(session.getDayRv(), slot.getId(), client.getId())];
      • righe 22–29: il secondo parametro è un'istanza di tipo [Action1<T>], dove T è il tipo restituito dal processo osservato, in questo caso [Response<Rv>]
    • riga 27: quando viene ricevuta la risposta, questa viene passata al metodo [consumeRV] alla riga 33;
  • righe 33–56: abbiamo ricevuto la risposta dal server. La elaboriamo;
    • righe 35–42: per prima cosa, controlliamo se il server ha segnalato un errore nel campo [status] della risposta;
    • riga 37: se c'è un errore, visualizziamo i messaggi che il server ha inserito nel campo [messages] della risposta;
    • riga 39: annulliamo tutte le attività;
    • riga 41 : torniamo all'interfaccia utente;
    • riga 44: se non c'è stato alcun errore, segnaliamo che l'appuntamento è stato aggiunto;
    • riga 46: l'appuntamento aggiunto viene memorizzato in un campo del frammento;
    • righe 47–55: come fatto durante l’eliminazione di un appuntamento, dopo aver aggiunto l’appuntamento, richiediamo al server l’orario più recente del medico;
  • righe 47–56, 59–71: questo codice è già stato incontrato diverse volte in precedenza;

Il metodo [beginWaiting] (riga 17) è il seguente:


   // beginning of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to launch tasks
    beginRunningTasks(numberOfRunningTasks);
    // status of buttons and menus
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
 
}
  • riga 4: comunichiamo al task padre che stiamo per avviare [numberOfRunningTasks] task;
  • riga 6: tutte le opzioni del menu vengono nascoste;
  • riga 7: quindi rende visibile l'opzione [Azioni/Annulla];

Il clic sull'opzione di menu [Annulla] viene gestito dal metodo [doCancel]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • riga 7: chiediamo alla classe padre di annullare le attività asincrone;

La navigazione indietro è gestita dai seguenti tre metodi:


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

3.6.9.3. Gestione del ciclo di vita dei frammenti

Il frammento presenta il seguente stato [AjoutRvFragmentState]:


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

Il ciclo di vita del frammento è implementato come segue:


// implementation methods parent class ----------------------------------
  @Override
  public CoreState saveFragment() {
    // save view
    AjoutRvFragmentState state = new AjoutRvFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
    state.setSpinnerClientsDataSource(spinnerClientsDataSource);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AJOUT_RV;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // retrieve clients in session
    clients = session.getClients();
    // 1st visit?
    if (previousState == null) {
      // we build the table displayed by the spinner
      spinnerClientsDataSource = new String[clients.size()];
      int i = 0;
      for (Client client : clients) {
        spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        i++;
      }
    } else {
      // no 1st visit
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // association spinner to its data source
    ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
      spinnerClientsDataSource);
    dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerClients.setAdapter(dataAdapterClients);
    // 1st visit?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // retrieve the number of the slot to be reserved in the session
    int position = session.getPosition();
    // the doctor's agenda is retrieved from the session
    AgendaMedecinJour agenda = session.getAgenda();
    // we get the doctor and the time slot we're going to schedule an appointment for
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // build page title 2
    String jour = session.getJourRv();
    txtTitre2.setText(String.format(Locale.FRANCE,
      "Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
      medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
      creneau.getMfin()));
    // customer selection
    spinnerClients.setSelection(0);
    // menu
    initMenu();
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore previous state
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // title
    txtTitre2.setText(state.getTitre());
    // spinner
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }
 
  @Override
  protected void notifyEndOfUpdates() {
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu status
    initMenu();
    // next view?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // there has been a cancellation - appointment already added?
    if (rdvAjouté) {
      // we modify the local agenda (we didn't get the global agenda)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // the agenda is displayed
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }
 
  // private methods -------------------
  private void initMenu() {
    // menu status
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
 
  • righe 2–10: quando richiesto dalla classe padre, 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 del cliente;
    • riga 8: l'origine dati dello spinner del cliente;
  • righe 12–15: l'ID del frammento è [IMainActivity.VUE_AJOUT_RV];
  • righe 17–35: eseguite quando il frammento viene generato per la prima volta (previousState == null) o rigenerato in occasioni successive (previousState != null);
    • riga 20: l'elenco dei clienti viene recuperato dalla sessione e inserito in un campo del frammento;
    • righe 22–30: per una prima visita, viene costruita l'origine dati per lo spinner dei clienti;
    • righe 32–33: per le visite successive, l'origine dati per lo 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 in occasioni successive (previousState != null);
    • righe 40–43: in tutti i casi, lo spinner del cliente viene associato alla sua origine dati;
    • righe 45–48: per la prima visita, il menu viene visualizzato senza l'azione [Annulla] (righe 107–111);
  • righe 51-70: eseguite quando si raggiunge il frammento tramite un'operazione [SUBMIT]. Proveniamo dalla vista [CALENDAR];
    • riga 54: recuperiamo il numero dello slot in cui fisseremo un appuntamento;
    • righe 56–59: recuperiamo le entità [Doctor] e [Time Slot] necessarie per aggiungere questo appuntamento e le inseriamo nei campi all'interno del frammento;
    • righe 61–65: utilizzando queste informazioni, possiamo costruire il titolo della vista;
    • riga 67: il menu a rotazione del client viene impostato sul primo elemento;
    • riga 69: il menu viene impostato sul suo stato iniziale (senza l'opzione [Annulla]);
  • righe 72-80: eseguite quando il frammento viene raggiunto tramite un'operazione [NAVIGATION] o [RESTORE];
    • riga 77: il titolo della vista viene rigenerato;
    • riga 79: lo spinner del client viene reimpostato sull'ultimo client selezionato;
  • righe 82–84: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Non c'è altro da fare qui;
  • righe 86–104: eseguite quando tutte le attività asincrone sono complete;
    • riga 89: il menu viene riportato allo stato predefinito;
    • righe 91–94: se le attività sono state completate normalmente, si ritorna alla vista [CALENDARIO] tramite un [INVIA] (in questo caso, avrebbe potuto essere anche un'azione di NAVIGAZIONE);
    • righe 96–103: se le attività si sono concluse con una cancellazione, si verifica comunque se l'appuntamento è stato aggiunto (ciò significherebbe che il recupero del nuovo calendario non è andato a buon fine);
    • righe 98-99: se l'appuntamento è stato aggiunto;
      • righe 98-99: l'appuntamento restituito dal server viene aggiunto al calendario corrente, quello attivo;
      • riga 101: torniamo alla vista [AGENDA] tramite un [SUBMIT] (in questo caso, avrebbe potuto trattarsi anche di un'azione di tipo NAVIGAZIONE);

3.7. Esecuzione

Eseguire i seguenti test:

  • utilizzare l'applicazione in condizioni normali e verificare che funzioni;
  • Ruotare il dispositivo per ogni vista e verificare che ciascuna venga ripristinata correttamente;
  • Aggiungere un'attesa di alcuni secondi in [IMainActivity];
  • Successivamente, annullare le attività e verificare che il risultato corrisponda a quello previsto;
  • ruotare il dispositivo durante i periodi di attesa e verificare che le attività vengano correttamente annullate e che non si verifichino arresti anomali;
  • Modifica l'ordine dei frammenti in [IMainActivity] e verifica che l'applicazione continui a funzionare;