Skip to content

3. Caso di studio - Gestione degli appuntamenti

3.1. Il progetto

Nel documento [Tutoriel AngularJS / Spring 4] è stata sviluppata un'applicazione client/server per la gestione degli appuntamenti medici. Di seguito faremo riferimento a questo documento con il codice [rdvmedecins-angular]. L'applicazione prevedeva due tipi di client:

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

Il client Android veniva ottenuto automaticamente dalla versione HTML del client tramite lo strumento [Cordova]. L’obiettivo di questo progetto sarà ricreare manualmente questo client Android utilizzando le conoscenze acquisite nei capitoli precedenti.

Va sottolineata un'importante differenza tra le due soluzioni:

  • quella che creeremo sarà utilizzabile solo sui tablet Android;
  • nella versione [rdvmedecins-angular], il client web mobile (HTML / CSS / JS) è utilizzabile su qualsiasi piattaforma (Android, IoS, Windows);

3.2. Le schermate del client Android

Sono disponibili quattro schermate.

Schermata di configurazione

Image

Schermata di selezione del medico e della data dell’appuntamento

Image

Schermata di selezione della fascia oraria dell’appuntamento

Image

Schermata relativa alla scelta del cliente per l’appuntamento

Image

3.3. L'architettura del progetto

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

Image

Gli scambi asincroni tra client e server saranno gestiti tramite la libreria RxAndroid.

3.4. Il database

Non riveste un ruolo fondamentale in questo documento. La riportiamo a titolo informativo. La chiameremo [dbrdvmedecins] . Si tratta di un database MySQL5 con quattro tabelle:

  

3.4.1. La tabella [MEDECINS]

Contiene informazioni sui medici gestiti dall’applicazione [RdvMedecins].

  • ID: numero identificativo del medico - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • NOM: il cognome del medico
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

3.4.2. La tabella [CLIENTS]

I pazienti dei diversi medici sono registrati nella tabella [CLIENTS]:

  • ID: numero identificativo del cliente - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • NOM: il nome del cliente
  • PRENOM: il suo nome
  • TITRE: titolo (Signorina, Signora, Signor)

3.4.3. La tabella [CRENEAUX]

Elenca le fasce orarie in cui sono possibili i RV:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • VERSION: numero identificativo della versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
  • HDEBUT: ora di inizio della fascia oraria
  • MDEBUT: minuti di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

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

3.4.4. La tabella [RV]

Elenca i RV assegnati a ciascun medico:

  • ID: numero che identifica in modo univoco il RV – chiave primaria
  • JOUR: giorno del RV
  • ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
  • ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella presenta un e vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):

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

Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può comparire in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.

La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.

3.4.5. Generazione del database

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

  

Con [WampServer] (cfr. paragrafo 6.15), si potrà procedere come segue:

 
  • in [1], cliccare sull'icona di [WampServer] e selezionare l'opzione [PhpMyAdmin] [2],
  • in [3], nella finestra che si è aperta, selezionare il link [Bases de données],
 
  • in [4-6], si importa un file SQL,
  • in [7], si seleziona lo script SQL e in [8] lo si esegue,
  • in [9], le tabelle del database sono state create. Si segue uno dei link,
 
  • in [10], il contenuto della tabella.

In seguito, non torneremo più su questo database, ma il lettore è invitato a seguirne l’evoluzione nel corso dei test, soprattutto quando l’applicazione non funziona.

3.5. Il server web / jSON

Image

Qui ci concentriamo sul server [1]. Non lo svilupperemo ulteriormente. È stato descritto in dettaglio nel documento [Spring MVC et Thymeleaf par l'exemple]. Il lettore interessato potrà consultarlo. È stato sviluppato come quello del server dell’esempio 15. Il suo codice sorgente è fornito negli esempi. Qui utilizzeremo il suo file binario:

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

3.5.1. Implementazione

In una finestra di comando, ci si posiziona nella cartella contenente il file binario del server:


...\rdvmedecins>dir
 Le volume dans le lecteur D s’appelle Données
 Le numéro de série du volume est 7A34-AE5F

 Répertoire de D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins

09/06/2016  10:50    <DIR>          .
09/06/2016  10:50    <DIR>          ..
06/07/2014  16:36             7 631 dbrdvmedecins.sql
08/06/2016  16:31    <DIR>          rdvmedecins-client
08/06/2016  16:22    <DIR>          rdvmedecins-server
08/06/2016  16:23        29 618 709 rdvmedecins-server-all-1.0.jar

quindi, per avviare il server, digitare il seguente comando (SGBD e MySQL devono essere già in esecuzione):


...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                  (v1.0)

10:55:48.617 [main] INFO  rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO  rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting service Tomcat
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFOS: Starting Servlet Engine: Apache Tomcat/8.0.33
juin 09, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFOS: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO  rdvmedecins.boot.Boot - Started Boot in 8.231 seconds

Il server visualizza numerosi log. Di seguito abbiamo riportato solo quelli utili alla comprensione:

  • righe 14-18: viene avviato un server Tomcat integrato sulla porta 8080 della macchina. È questo server che esegue l’applicazione web di gestione degli appuntamenti. Questa applicazione è in realtà un servizio web / jSON: viene interrogata tramite URL e risponde inviando una stringa jSON;
  • riga 24: il servizio web è protetto con il framework [Spring Security]. Si accede alle URL del servizio web effettuando l’autenticazione;
  • righe 29-44: i URL esposti dal servizio web;

Le analizzeremo nel dettaglio.

3.5.2. Sicurezza del servizio web

I URL esposti dal servizio web sono protetti. Il server si aspetta che la richiesta HTTP del client contenga la seguente intestazione:

Authorization: Basic code

Il codice atteso è la codifica in base64 [http://fr.wikipedia.org/wiki/Base64] della stringa 'utente:password'. Il servizio web, nella sua configurazione iniziale, accetta solo un utente 'admin' con la password 'admin'. L'intestazione sopra indicata diventa, per questo utente specifico, la seguente riga:

Authorization: Basic YWRtaW46YWRtaW4=

Per poter inviare questa intestazione HTTP, utilizziamo il client HTTP [Advanced Rest Client], che è un plugin del browser Chrome (cfr. paragrafo 6.13). Testeremo manualmente i diversi URL esposti dal servizio web per comprendere:

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

3.5.3. Elenco dei medici

L'URL [/getAllMedecins] consente di ottenere l'elenco dei medici:

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

La risposta del server è la seguente:

  • in [5], la risposta jSON del server, formattata;
  • in [6], la stessa risposta allo stato grezzo;

Il formato [5] consente di vedere meglio la struttura della risposta. Tutte le risposte del servizio web sono un'istanza della seguente classe [Response]:


package rdvmedecins.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getter e setter
...
}
  • riga 9: lo stato della risposta. Il valore 0 indica che non si è verificato alcun errore, altrimenti si è verificato un errore;
  • riga 11: un elenco di messaggi di errore, se si è verificato un errore;
  • riga 13: la risposta effettivamente attesa dal client;

La risposta a URL [/getAllMedecins] è la stringa jSON di un oggetto di tipo [Response<List<Medecin>>]. La classe [Medecin] è la seguente:


package rdvmedecins.android.dao.entities;

public class Medecin extends Personne {

    // costruttore predefinito
    public Medecin() {
    }

    // costruttore con parametri
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}

Riga 3: la classe [Medecin] estende la seguente classe [Personne]:


package rdvmedecins.android.dao.entities;

public class Personne extends AbstractEntity {
    // attributi di una persona
    private String titre;
    private String nom;
    private String prenom;

    // costruttore predefinito
    public Personne() {
    }

    // costruttore con parametri
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // getter e setter
    ...
}

Riga 3, la classe [Personne] estende la seguente classe [AbstractEntity]:


package rdvmedecins.android.dao.entities;

import java.io.Serializable;

public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    protected Long id;
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // inizializzazione
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // getter e setter
    ...
}

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


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

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

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

D'ora in poi useremo queste definizioni abbreviate per descrivere la risposta del server. Inoltre, per un po' non mostreremo più screenshot. Basta ripetere ciò che abbiamo appena visto. Torneremo agli screenshot quando sarà necessario effettuare una richiesta POST. Presenteremo inoltre un esempio di esecuzione nella forma seguente:

URL

/getAllMedecins
Réponse
{"status":0,"messages":null,"medecins":
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},
{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},
{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]}

3.5.4. Elenco dei clienti

URL

/getAllClients
Réponse

Response<List<Client>> :[int status; List<String> messages;
 List<Client> clients]
Client : [Long id;  Long version; String titre;
 String nom; String prenom;]

Esempio:

URL

/getAllClients
Réponse
{"status":0,"messages":null,"clients":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]}

3.5.5. Elenco degli orari di un medico

URL
/getAllCreneaux/{idMedecin}
Réponse

Response<List<Creneau>>:[int status ; List<String> messages ;
 List<Creneau> creneaux]
Creneau : [int hdebut ; int mdebut ; int hfin ; int mfin ;]
  • [idMedecin]: identificativo del medico di cui si desiderano conoscere gli orari di visita;
  • [hdebut]: ora di inizio della visita;
  • [mdebut]: minuti di inizio della visita;
  • [hfin]: ora di fine della visita;
  • [mfin]: minuti di fine della visita;

Per una fascia oraria compresa tra le 10:20 e le 10:40 si avrà [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].

Esempio:

URL
/getAllCreneaux/1
Réponse
{"status":0,"messages":null,"creneaux":
[{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1}]}

3.5.6. Elenco degli appuntamenti di un medico

URL
/getRvMedecinJour/{idMedecin}/{jour}
Réponse

Response<List<Rv>>:[int status ; List<String> messages ;
 List<Rv> rvs]
Rv : [Date jour ; Client client ; Creneau creneau ;
 long idClient ; long idCreneau]
  • [idMedecin]: identificativo del medico di cui si desiderano gli appuntamenti;
  • URL [jour]: giorno degli appuntamenti nel formato 'aaaa-mm-gg';
  • Risposta [jour]: come sopra, ma nel formato di una data Java;
  • [client]: il cliente dell'appuntamento. La sua struttura è stata descritta in precedenza;
  • [idClient]: l'identificativo del cliente;
  • [creneau]: la fascia oraria dell’appuntamento. La sua struttura è stata descritta in precedenza;
  • [idCreneau]: l'identificativo della fascia oraria;

Esempio:

URL
/getRvMedecinJour/1/2014-07-08
Réponse
{"status":0,"messages":null,
"rvs":[{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}]}

3.5.7. L'agenda di un medico

URL
/getAgendaMedecinJour/{idMedecin}/{jour}
Réponse

Response<AgendaMedecinJour>:[int status ; List<String> messages ;
 AgendaMedecinJour agenda]
AgendaMedecinJour : [Medecin medecin ;Date jour ; 
CreneauMedecinJour[] creneauxMedecinJour]
CreneauMedecinJour : [Creneau creneau ; Rv rv]
  • [idMedecin]: identificativo del medico di cui si desiderano gli appuntamenti;
  • URL [jour]: giorno degli appuntamenti nel formato 'aaaa-mm-gg';
  • [agenda]: agenda del medico;
  • [medecin]: il medico in questione. La sua struttura è stata definita in precedenza;
  • Risposta [jour]: il giorno dell'agenda nel formato di una data Java;
  • [creneauxMedecinJour]: un array di elementi di tipo [CreneauMedecinJour];
  • [creneau]: una fascia oraria. La sua struttura è stata descritta in precedenza;
  • [rv]: un appuntamento. La sua struttura è stata descritta in precedenza;

Esempio:

URL
/getAgendaMedecinJour/1/2014-07-08
Réponse

{"status":0,"messages":null,"agenda":{"medecin":
{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
"jour":1404770400000,"creneauxMedecinJour":[{"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"rv":{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}},{"creneau":
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1},
"rv":null}]}}

Sono stati evidenziati i casi in cui è presente un appuntamento nella fascia oraria e quelli in cui non ce n'è.

3.5.8. Ricerca di un medico tramite il suo identificativo

URL
/getMedecinById/{idMedecin}
Réponse

Response<Medecin> :[int status ; List<String> messages ; Medecin medecin]
  • [idMedecin]: l'identificativo del medico;

Esempio 1:

URL
/getMedecinById/1
Réponse
{"status":0,"messages":null,"medecin":
{"id":1,"version":1,"titre":"Mme",
"nom":"PELISSIER","prenom":"Marie"}}

Esempio 2:

URL
/getMedecinById/100
Réponse
{"status":2,
"messages":["Médecin [100] inexistant"],"medecin":null}

3.5.9. Recuperare un cliente tramite il suo ID

URL
/getClientById/{idClient}
Réponse

Response<Client> :[int status ; List<String> messages ;
 Client client]
  • [idClient]: l'identificativo del cliente;

Esempio 1:

URL
/getClientById/1
Réponse
{"status":0,"messages":null,"client":{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}}

Esempio 2:

URL
/getClientById/100
Réponse
{"status":2,"messages":["Client [100] inexistant"],"client":null}

3.5.10. Ottenere una fascia oraria tramite il relativo identificativo

URL
/getCreneauById/{idCreneau}
Réponse

Response<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [idCreneau]: l'ID della fascia oraria;

Esempio 1:

URL
/getCreneauById/10
Réponse
{"status":0,"messages":null,"creneau":
{"id":10,"version":1,"hdebut":11,"mdebut":0,
"hfin":11,"mfin":20,"idMedecin":1}}

Si noti che nella risposta non compare il nome del medico titolare della fascia oraria, ma solo il suo identificativo.

Esempio 2:

URL
/getCreneauById/100
Réponse
{"status":2,"messages":["Créneau [100] inexistant"],
"creneau":null}

3.5.11. Fissare un appuntamento tramite il proprio ID

URL
/getRvById/{idRv}
Réponse

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

Esempio 1:

URL
/getRvById/45
Réponse
{"status":0,"messages":null,"rv":{"id":45,"version":0,
"jour":"2014-07-08","idClient":1,"idCreneau":1}}

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

Esempio 2:

URL
/getCreneauById/455
Réponse
{"status":2,"messages":["Rv [455] inexistant"],"rv":null}

3.5.12. Aggiungi un appuntamento

Il codice URL [/ajouterRv] consente di aggiungere un appuntamento. Le informazioni necessarie per l'aggiunta (giorno, fascia oraria e cliente) vengono trasmesse tramite una richiesta HTTP POST. Mostriamo come eseguire questa richiesta con lo strumento [Advanced Rest Client].

Image

  • in [1], viene interrogata la URL;
  • in [2], viene interrogata da un POST;
  • in [3-4], si specifica al server che i valori inviati sono sotto forma di stringa jSON;
  • in [4], l’intestazione HTTP dell’autenticazione;
  • in [5], le informazioni trasmesse da POST. Si tratta di una stringa jSON contenente:
    • [jour]: il giorno dell'appuntamento nel formato 'aaaa-mm-gg',
    • [idClient]: l’identificativo del cliente per il quale è stato fissato l’appuntamento,
    • [idCreneau]: l'identificativo della fascia oraria dell'appuntamento. Poiché una fascia oraria appartiene a un medico specifico, con questo si indica anche il medico;
  • in [6], si invia la richiesta;

La stringa jSON che viene inviata è quella dell’oggetto di tipo [PostAjouterRv] seguente:


public class PostAjouterRv {

  // dati del post
  private String jour;
  private long idClient;
  private long idCreneau;

  // costruttori
  public PostAjouterRv() {

  }

  public PostAjouterRv(String jour, long idCreneau, long idClient) {
    this.jour = jour;
    this.idClient = idClient;
    this.idCreneau = idCreneau;
  }

  // getter e setter
  ...
}

La risposta del server è di tipo [Response<Rv>] [int status; List<String> messages; Rv rv], dove [rv] è l'appuntamento aggiunto.

La risposta del server alla richiesta sopra riportata è la seguente:

 

Si noti che alcune informazioni non sono riportate in [idClient, idCreneau], ma si trovano nei campi [client] e [creneau]. L'informazione importante è l'identificativo dell'appuntamento aggiunto (209). Il servizio web avrebbe potuto limitarsi a restituire solo questa informazione.

3.5.13. Eliminare un appuntamento

Anche questa operazione viene eseguita tramite un POST:

URL
/supprimerRv
POST
{'idRv':idRv}
Réponse

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

Il valore inserito è la stringa jSON di un oggetto di tipo [PostSupprimerRv] come segue:


public class PostSupprimerRv {

  // dati del post
  private long idRv;

  // costruttori
  public PostSupprimerRv() {

  }

  public PostSupprimerRv(long idRv) {
    this.idRv = idRv;
  }

  // getter e setter
  ...
}
  • riga 4, [idRv] è l'identificativo dell'appuntamento da eliminare.

Esempio 1:

URL
/supprimerRv
POST
{"idRv":209}
Réponse
{"status":0,"messages":null,"rv":null}

L'appuntamento n. 209 è stato effettivamente cancellato perché [status=0].

Esempio 2:

URL
/supprimerRv
POST
{"idRv":650}
Réponse
{"status":2,"messages":["Rv [650] inexistant"],"rv":null}

3.6. L'app Android

Image

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

3.6.1. Architettura del progetto Android Studio

Il progetto riprende l'architettura del progetto [client-android-skel] (cfr. paragrafo 1.17). Nell'architettura sopra riportata del client Android si distinguono tre blocchi:

  • il livello [DAO], responsabile della comunicazione con il servizio web;
  • i [vues] incaricati della comunicazione con l’utente;
  • il [activité] che funge da collegamento tra i due blocchi precedenti. Le viste non sono a conoscenza del livello [DAO]. Comunicano esclusivamente con l’attività.

Questa architettura si riflette in quella del progetto Android Studio del client Android:

 
  • il pacchetto [activity] implementa l’attività;
  • il pacchetto [architecture] riprende gli elementi di architettura che abbiamo sviluppato in precedenza;
  • il pacchetto [dao] implementa il livello [DAO];
  • il pacchetto [fragments] implementa il pacchetto [vues];

3.6.2. Personalizzazione del progetto

  

La cartella [architecture / custom] contiene gli elementi personalizzabili dell'architettura.

L'interfaccia [IMainActivity] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione delle attese
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 000;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = false;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = true;

  // numero di frammenti dell'applicazione
  int FRAGMENTS_COUNT = 4;

  // numero di visualizzazioni
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • righe 25, 28: personalizzazione del livello [DAO];
  • riga 31: questa applicazione effettua accessi autenticati al server;
  • riga 40: è necessaria un'immagine di attesa;
  • riga 43: l'applicazione ha quattro frammenti;
  • righe 46-49: i numeri dei quattro frammenti;
  • riga 37: non ci sono schede;

La classe base [CoreState] degli stati dei frammenti sarà la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = AccueilFragmentState.class),
  @JsonSubTypes.Type(value = AgendaFragmentState.class),
  @JsonSubTypes.Type(value = AjoutRvFragmentState.class),
  @JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • righe 15-18: i quattro frammenti hanno uno stato:
  

Infine, la sessione contiene i dati condivisi tra i frammenti:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;

import java.util.List;

public class Session extends AbstractSession {
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore

  // elenco dei medici
  private List<Medecin> médecins;
  // elenco dei clienti
  private List<Client> clients;
  // agenda di un medico per un determinato giorno
  private AgendaMedecinJour agenda;
  // posizione dell’elemento cliccato nell’agenda
  private int position;
  // giorno dell'appuntamento nel formato inglese "yyyy-MM-dd"
  private String dayRv;
  // giorno dell'appuntamento nel formato francese "dd-MM-yyyy"
  private String jourRv;

  // getter e setter
...
}
  • righe 17-28: la sessione memorizza sei informazioni. Spiegheremo il ruolo di queste informazioni quando sarà necessario.

3.6.3. Lo strato [DAO]

  • in [1], le entità incapsulate nelle risposte del server. Sono state illustrate nel paragrafo 3.5;
  • in [2], gli elementi del client che gestiscono gli scambi con il server;

Non torneremo sugli elementi [1]. Sono già stati presentati. Il lettore è invitato a tornare al paragrafo 3.5 se necessario. Esamineremo l’implementazione del pacchetto [service]. Questo ci porterà a parlare anche dell’implementazione degli scambi sicuri tra client e server.

3.6.3.1. Implementazione degli scambi client/server

  

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

  • i URL esposti dal servizio web;
  • i relativi parametri;
  • le relative risposte;

package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  public void setRestTemplate(RestTemplate restTemplate);

  // elenco dei medici
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();

  // elenco dei clienti
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();

  // elenco delle fasce orarie di un medico
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);

  // elenco degli appuntamenti di un medico
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);

  // Cliente
  @Get("/getClientById/{id}")
  public Response<Client> getClientById(@Path long id);

  // Medico
  @Get("/getMedecinById/{id}")
  public Response<Medecin> getMedecinById(@Path long id);

  // Appuntamento
  @Get("/getRvById/{id}")
  public Response<Rv> getRvById(@Path long id);

  // Fascia oraria
  @Get("/getCreneauById/{id}")
  public Response<Creneau> getCreneauById(@Path long id);

  // aggiungere un RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);

  // eliminare un appuntamento
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);

  // Ottenere l'agenda di un medico
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

}
  • righe 19-60: sono presenti tutte le URL esaminate nel paragrafo 3.5;
  • riga 16: il componente [RestTemplate] di [Spring Android] su cui si basa la comunicazione client/server;

3.6.3.2. L'interfaccia [IDao]

  

L'interfaccia [IDao] del livello [DAO] è la seguente:


package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import rx.Observable;

import java.util.List;

public interface IDao {
  // URL del servizio web
  public void setUrlServiceWebJson(String url);

  // utente
  public void setUser(String user, String mdp);

  // timeout del client
  public void setTimeout(int timeout);

  // elenco dei clienti
  public Observable<List<Client>> getAllClients();

  // elenco dei medici
  public Observable<List<Medecin>> getAllMedecins();

  // elenco delle fasce orarie di un medico
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);

  // elenco degli appuntamenti di un medico in un determinato giorno
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);

  // trovare un cliente identificato dal suo ID
  public Observable<Client> getClientById(long id);

  // trovare un medico identificato dal suo ID
  public Observable<Medecin> getMedecinById(long id);

  // trovare un appuntamento identificato dal proprio ID
  public Observable<Rv> getRvById(long id);

  // trovare una fascia oraria identificata dal proprio ID
  public Observable<Creneau> getCreneauById(long id);

  // aggiungere un RV
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);

  // eliminare un RV
  public Observable<Rv> supprimerRv(long idRv);

  // attività
  public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);

  // modalità debug
  void setDebugMode(boolean isDebugEnabled);
}
  • riga 10: per impostare l’URL del servizio web / jSON;
  • riga 13: per impostare l'utente della comunicazione client/server. [user] è l'ID utente, [mdp] la sua password;
  • riga 16: per impostare un tempo massimo di attesa per la risposta del server;
  • righe 18-49: a ogni URL esposto dal servizio web corrisponde un metodo. Essi riprendono la firma dei metodi con lo stesso nome del componente AA [WebClient];
  • riga 52: per controllare la modalità debug del livello [DAO];

3.6.3.3. La classe [Dao]

  

L'implementazione [DAO] dell'interfaccia [IDao] precedente è la seguente:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente del servizio web
  @RestService
  protected WebClient webClient;
  // sicurezza
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // il RestTemplate
  private RestTemplate restTemplate;
  // factory di RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    ...
  }

  @Override
  public void setUser(String user, String mdp) {
    ...
  }

  @Override
  public void setTimeout(int timeout) {
    ...
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercettatore di autenticazione?
    if (isBasicAuthentificationNeeded) {
      // si aggiunge l'intercettatore di autenticazione
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }

  }

  // metodi privati -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // implementazione dell'interfaccia IDao --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // log
    log("getAllClients");
    // risultato
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // log
    log("getAllMedecins");
    // risultato
    return getResponse(new IRequest<Response<List<Medecin>>>() {
      @Override
      public Response<List<Medecin>> getResponse() {
        return webClient.getAllMedecins();
      }
    });
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
    // log
    log("getAllCreneaux");
    // risultato
    return getResponse(new IRequest<Response<List<Creneau>>>() {
      @Override
      public Response<List<Creneau>> getResponse() {
        return webClient.getAllCreneaux(idMedecin);
      }
    });
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getRvMedecinJour");
    // risultato
    return getResponse(new IRequest<Response<List<Rv>>>() {
      @Override
      public Response<List<Rv>> getResponse() {
        return webClient.getRvMedecinJour(idMedecin, jour);
      }
    });
  }

  @Override
  public Observable<Response<Client>> getClientById(final long id) {
    // log
    log("getClientById");
    // risultato
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(final long id) {
    // log
    log("getMedecinById");
    // risultato
    return getResponse(new IRequest<Response<Medecin>>() {
      @Override
      public Response<Medecin> getResponse() {
        return webClient.getMedecinById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> getRvById(final long id) {
    // log
    log("getRvById");
    // risultato
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.getRvById(id);
      }
    });
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // log
    log("getCreneauById");
    // risultato
    return getResponse(new IRequest<Response<Creneau>>() {
      @Override
      public Response<Creneau> getResponse() {
        return webClient.getCreneauById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
    // log
    log("ajouterRv");
    // risultato
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
      }
    });
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(final long idRv) {
    // log
    log("supprimerRv");
    // risultato
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.supprimerRv(new PostSupprimerRv(idRv));
      }
    });
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // risultato
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }

}
  • righe 18-72: sono quelle presenti di base nella classe [Dao] del progetto [client-android-skel];
  • righe 74-216: implementazione dell'interfaccia [IDao]. I metodi che interrogano i URL esposti dal servizio web delegano tale interrogazione al componente AA [WebClient] (righe 22-23);
  • righe 58-63: se le comunicazioni client/server sono autenticate tramite un'autorizzazione di tipo basic, si aggiunge un intercettatore al componente [RestTemplate]. Ciò avrà come effetto che ogni richiesta HTTP emessa dal componente [RestTemplate] verrà intercettata dalla classe [MyAuthInterceptor] (righe 25-26);

La classe [MyAuthInterceptor] è la seguente:


package rdvmedecins.android.dao.security;

import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // utente
  private String user;
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    HttpHeaders headers = request.getHeaders();
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    headers.setAuthorization(auth);
    return execution.execute(request, body);
  }

  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}
  • riga 15: la classe [MyAuthInterceptor] è un componente AA di tipo [singleton];
  • riga 16: la classe [MyAuthInterceptor] estende l'interfaccia Spring [ClientHttpRequestInterceptor]. Tale interfaccia presenta un metodo, il metodo [intercept] alla riga 22. Si estende questa interfaccia per intercettare qualsiasi richiesta HTTP proveniente dal client. Il metodo [intercept] riceve tre parametri;
    • [HtpRequest request]: la richiesta HTTP intercettata,
    • [byte[] body]: il suo corpo, se presente (ad esempio, valori inviati tramite POST),
    • [ClientHttpRequestExecution execution]: il componente Spring che esegue la richiesta;

Intercettiamo tutte le richieste HTTP provenienti dal client Android per aggiungere l'intestazione di autenticazione HTTP descritta nel paragrafo 3.5.

  • riga 23: recuperiamo le intestazioni HTTP della richiesta intercettata;
  • riga 24: creiamo l'intestazione di autenticazione HTTP. La modalità di autenticazione utilizzata (codifica base64 della stringa 'user:mdp') è fornita dalla classe Spring [HttpBasicAuthentication];
  • riga 25: l'intestazione di autenticazione appena creata viene aggiunta alle intestazioni attuali della richiesta intercettata;
  • riga 26: si prosegue con l'esecuzione della richiesta intercettata. Riassumendo, la richiesta intercettata è stata arricchita con l'intestazione di autenticazione;

Le implementazioni dei metodi dell’interfaccia [IDao] seguono tutte lo stesso modello. Prendiamo ad esempio il metodo [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // risultato
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • riga 2: il metodo richiede due parametri:
    • [idMedecin]: l'identificativo del medico di cui si desidera l'agenda;
    • [jour]: il giorno per il quale si desidera visualizzare l’agenda;
  • riga 6: si chiama il metodo [getResponse] della classe padre [AbstractDao]. Questo metodo richiede un parametro di tipo [IRequest<T>], dove T è il tipo restituito dal metodo [getAgendaMedecinJour] alla riga 2, in questo caso [Response<AgendaMedecinJour>]. L’interfaccia [IRequest] ha un solo metodo: [getResponse] (riga 8);
  • righe 8-10: implementazione del metodo [IRequest.getResponse]. Questo metodo deve restituire il risultato atteso dal metodo [getAgendaMedecinJour] alla riga 2, di tipo [Response<AgendaMedecinJour>];
  • riga 9: la risposta viene restituita dal metodo [webClient.getAgendaMedecinJour]:

  // ottenere l'agenda di un medico
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

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

3.6.4. L'attività [MainActivity]

Serveur
  

La classe [MainActivity] è la seguente:


package client.android.activity;

import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.AccueilFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AjoutRvFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;

import java.util.List;

@EActivity
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // classe genitore ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
    return fragments;
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // interfaccia IDao -----------------------------------------------------
...

  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
    return dao.getAllCreneaux(idMedecin);
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
    return dao.getRvMedecinJour(idMedecin, jour);
  }

  @Override
  public Observable<Response<Client>> getClientById(long id) {
    return dao.getClientById(id);
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(long id) {
    return dao.getMedecinById(id);
  }

  @Override
  public Observable<Response<Rv>> getRvById(long id) {
    return dao.getRvById(id);
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(long id) {
    return dao.getCreneauById(id);
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
    return dao.ajouterRv(jour, idCreneau, idClient);
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(long idRv) {
    return dao.supprimerRv(idRv);
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
    return dao.getAgendaMedecinJour(idMedecin, jour);
  }
}
  • righe 21-66: queste righe sono incluse di default nel modello [client-android-skel];
  • righe 66-119: implementazione dell'interfaccia [IDao]. Tutti i metodi delegano il lavoro al livello [DAO] della riga 26;
  • righe 42-46: il metodo [getFragments] restituisce l'array dei quattro frammenti dell'applicazione;
  • righe 58-61: la vista di configurazione è la prima vista da visualizzare all’avvio dell’applicazione;

3.6.5. La sessione

  

La classe [Session] serve a memorizzare le informazioni che devono essere trasmesse tra i frammenti. È la seguente:


package rdvmedecins.android.architecture;

import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Medecin;
import org.androidannotations.annotations.EBean;

import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // elenco dei medici
  private List<Medecin> médecins;
  // elenco dei clienti
  private List<Client> clients;
  // agenda
  private AgendaMedecinJour agenda;
  // posizione dell’elemento cliccato nell’agenda
  private int position;
  // giorno dell'appuntamento nel formato inglese "yyyy-MM-dd"
  private String dayRv;
  // giorno dell'appuntamento nel formato francese "dd-MM-yyyy"
  private String jourRv;


  // getter e setter
...
}
  • riga 10: la classe [Session] è un componente AA istanziato in un unico esemplare;
  • righe 12-15: in questo caso di studio si supporrà che gli elenchi dei medici e dei clienti non cambino. Verranno richiesti all’avvio dell’applicazione e memorizzati nella sessione affinché i frammenti possano utilizzarli;
  • righe 20-23: il giorno desiderato per un appuntamento. Viene gestito in due formati: in notazione francese (riga 23) all’interno dell’app Android, in notazione inglese (riga 21) per gli scambi con il server;
  • riga 19: la posizione dell’elemento cliccato (link “aggiungi”/“elimina”) sull’agenda;

3.6.6. Gestione della vista di configurazione

3.6.6.1. La vista

La vista di configurazione è la vista visualizzata all’avvio dell’applicazione:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

Type
Nom
1
EditText
edtUrlServiceRest
3
EditText
edtUtilisateur
5
EditText
edtMdp
2
TextView
txtErrorUrlServiceRest
3
TextView
txtErrorUtilisateur

3.6.6.2. Il frammento

La vista di configurazione è gestita dal seguente frammento [ConfigFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.net.URI;
import java.util.List;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {

  // gli elementi dell'interfaccia visiva
  @ViewById(R.id.edt_urlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_errorUrlServiceRest)
  protected TextView txtErrorUrlServiceRest;
  @ViewById(R.id.txt_errorUtilisateur)
  protected TextView txtErrorUtilisateur;
  @ViewById(R.id.edt_utilisateur)
  protected EditText edtUtilisateur;
  @ViewById(R.id.edt_mdp)
  protected EditText edtMdp;

  // i campi di immissione
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;

  // convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // implementazione dei metodi della classe padre -------------------------------------------
 ...

}
  • riga 25: il frammento è associato al seguente menu [menu_config]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>

</menu>
  • righe 28-38: gli elementi dell'interfaccia visiva;
  • righe 41-43: i tre campi di immissione del modulo;

Il clic sull'opzione di menu [Valider] è gestito dal metodo [doValider]:


// convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // si nascondono eventuali messaggi di errore precedenti
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // si verifica la validità dei dati inseriti
    if (!isPageValid()) {
      return;
    }
    // si compila il campo URL del servizio web
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // si inseriscono i dati dell'utente
    mainActivity.setUser(utilisateur, mdp);
    // inizio dell'attesa - si avvieranno 2 attività asincrone
    beginWaiting(2);
    // medici
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // si elabora la risposta
        consumeMedecins(responseMedecins);
      }
    });
    // clienti
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // si elabora la risposta
        consumeClients(responseClients);
      }
    });
  }


  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // errore?
    if (responseMedecins.getStatus() != 0) {
      // messaggio
      showAlert(responseMedecins.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // i medici vengono memorizzati nella sessione
    session.setMédecins(responseMedecins.getBody());
  }

  private void consumeClients(Response<List<Client>> responseClients) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // errore?
    if (responseClients.getStatus() != 0) {
      // messaggio
      showAlert(responseClients.getMessages());
      // annullamento
      doAnnuler();
      // torna a UI
      return;
    }
    // si memorizzano i clienti nella sessione
    session.setClients(responseClients.getBody());
  }
  • righe 8-10: viene verificata la validità dei tre dati inseriti nel modulo. Se il modulo non è valido, non si procede oltre;
  • righe 11-14: i dati necessari per il livello [DAO] vengono passati all’attività;
  • riga 16: si comunica alla classe padre che verranno avviate due attività asincrone e si prepara l'attesa;
  • righe 17-24: viene richiesta la lista dei medici;
  • riga 18: il metodo [executeInBackground] richiede due parametri:
    • riga 18: il processo da eseguire e osservare è fornito dal metodo [mainActivity.getAllMedecins()];
    • righe 18-24: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<List<Medecin>>]
  • riga 22: quando si riceve la risposta, questa viene passata al metodo [consumeMedecins] della riga 36;
  • righe 25-33: dopo aver avviato un primo task asincrono, se ne avvia un secondo per richiedere l'elenco dei clienti. Si avranno quindi due task in esecuzione in parallelo;
  • righe 36-52: abbiamo ricevuto la risposta dall’attività relativa ai medici. La elaboriamo;
  • righe 42-49: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
  • riga 44: se c’è un errore, visualizziamo i messaggi che il server ha inserito nel campo [messages] della risposta;
  • riga 46: si annullano tutte le attività;
  • riga 48: si torna all'interfaccia utente;
  • riga 51: se non si è verificato alcun errore, l'elenco dei medici viene salvato nella sessione;

La validità dei dati inseriti (riga 8) viene verificata con il seguente metodo:


  private boolean isPageValid() {
    // si verifica la validità dei dati inseriti
    boolean erreur;
    URI service;
    // validità del URL del servizio REST
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    try {
      service = new URI(urlServiceRest);
      erreur = service.getHost() == null || service.getPort() == -1;
    } catch (Exception ex) {
      // si registra l'errore
      erreur = true;
    }
    if (erreur) {
      // visualizzazione dell'errore
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // utente
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // viene visualizzato l'errore
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // si rileva l'errore
      erreur = true;
    }
    // password
    mdp = edtMdp.getText().toString().trim();
    // ritorno
    return !erreur;
}

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


  // inizio dell'attesa
  protected void beginWaiting(int numberOfRunningTasks) {
    // si prepara l'avvio delle attività
    beginRunningTasks(numberOfRunningTasks);
    // stato dei pulsanti e dei menu
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • riga 4: si comunica all’attività principale che si sta per avviare l’attività [numberOfRunningTasks];
  • riga 6: si nascondono tutte le opzioni del menu;
  • riga 7: per rendere poi visibile l’opzione [Actions/Annuler];

Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:


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

3.6.6.3. Gestione del ciclo di vita del frammento

Il frammento presenta il seguente stato [ConfigFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // Visibilità dei due messaggi di errore
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;

  // getter e setter
...
}
  • quando la classe padre glielo richiederà, il frammento salverà la visibilità dei suoi due messaggi di errore;

Il ciclo di vita del frammento è implementato nel modo seguente:


// implementazione dei metodi della classe padre -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // salvataggio dello stato del frammento
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
    state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected int getNumView() {
    return     IMainActivity.VUE_CONFIG;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    if (previousState == null) {
      // prima visita
      // si nascondono i messaggi di errore
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // ripristino della visibilità dei messaggi di errore
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // non è la prima visita - si ripristinano i messaggi di errore
    txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
  }


  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // vista successiva?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }

  // metodi privati ------------------------------------------------
  private void initMenu(){
    // stato del menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • righe 2-9: quando la classe padre glielo richiede, il frammento salva lo stato dei suoi due messaggi di errore;
  • righe 11-14: il numero del frammento è [IMainActivity.VUE_CONFIG];
  • righe 16-19: vengono eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null). In questo caso, non c’è nulla da fare;
  • righe 21-31: vengono eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
    • righe 24-29: alla prima visita, si nascondono i messaggi di errore e si visualizza il menu senza l'azione [Annuler] (righe 62-66);
  • righe 33-35: eseguite quando si accede al frammento tramite un'operazione [SUBMIT]. Qui ciò non accade mai;
  • righe 37-44: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE]. Si ripristina lo stato dei messaggi di errore a partire dallo stato precedente;
  • righe 47-49: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Non c'è altro da fare;
  • righe 51-59: vengono eseguite quando tutte le attività asincrone sono terminate;
    • righe 53-54: si riporta il menu allo stato predefinito;
    • righe 56-58: se le attività si sono concluse normalmente, si passa alla vista successiva, altrimenti si rimane sulla stessa vista;

3.6.7. Gestione della schermata iniziale

3.6.7.1. La vista

La schermata iniziale è la seguente:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

Type
Nom
1
Spinner
spinnerMedecins
2
DatePicker
edtJourRv

3.6.7.2. Il frammento

La pagina iniziale è gestita dal seguente frammento [AccueilFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.util.Calendar;
import java.util.List;
import java.util.Locale;

@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;

  // dati locali
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;

  // convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...

  // implementazione dei metodi della classe padre -------------------------------------
...
}
  • riga 26: il frammento è associato al seguente menu [menu_accueil]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
    </menu>
  </item>
</menu>
  • righe 31-34: gli elementi dell'interfaccia visiva;
  • riga 37: l'elenco dei medici;
  • riga 38: un calendario;
  • riga 39: la fonte dati dello spinner dei medici;

Il clic sul link [Valider] è gestito dal seguente metodo [doValider]:


// convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // si registra l'ID del medico selezionato
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // si memorizza il giorno nella sessione
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // si passa al formato data aaaa-MM-gg
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // inizio dell'attesa - si avvierà 1 attività asincrona
    beginWaiting(1);
    // si richiede l'agenda del medico
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // si elabora la risposta
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // errore?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // messaggio
      showAlert(responseAgendaMedecinJour.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // si inserisce l'agenda nella sessione
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • riga 5: si recupera l'identificativo del medico selezionato;
  • righe 7-8: si inserisce, in formato francese, la data scelta;
  • righe 10-11: si inserisce, in formato inglese, la data scelta;
  • riga 13: si comunica alla classe padre che si sta per avviare un'operazione asincrona e si prepara l'attesa;
  • righe 15-22: viene richiesta l'agenda del medico;
    • riga 15: il metodo [executeInBackground] richiede due parametri:
      • riga 15: il processo da eseguire e da osservare viene fornito dal metodo [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
      • righe 15-22: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<AgendaMedecinJour>]
    • riga 20: quando si riceve la risposta, questa viene passata al metodo [consumeAgenda] della riga 25;
  • righe 25-37: è stata ricevuta l'agenda del medico. La si analizza;
  • righe 27-34: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
  • riga 29: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
  • riga 31: si annullano tutte le attività;
  • riga 33: si torna all'interfaccia utente;
  • riga 36: se non si sono verificati errori, l'agenda viene attivata;

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


  // inizio dell'attesa
  protected void beginWaiting(int numberOfRunningTasks) {
    // si prepara l'avvio delle attività
    beginRunningTasks(numberOfRunningTasks);
    // stato dei pulsanti e dei menu
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • riga 4: si comunica all'attività padre che si sta per avviare [numberOfRunningTasks] attività;
  • riga 6: si nascondono tutte le opzioni del menu;
  • riga 7: per rendere poi visibile l’opzione [Actions/Annuler];

Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:


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

Il clic sull'opzione di menu [Retour à la configuration] viene gestito nel modo seguente:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // si passa alla vista di configurazione
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • riga 4: si passa alla vista di configurazione con l'azione [NAVIGATION]. Ciò significa che si desidera ripristinare la vista di configurazione nello stato in cui è stata lasciata;

3.6.7.3. Gestione del ciclo di vita del frammento

Il frammento presenta il seguente stato [AccueilFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AccueilFragmentState extends CoreState {

  // stato del frammento [Accueil]
  // posizione del medico selezionato
  private int selectedMedecinPosition;
  // data selezionata
  private int year;
  private int month;
  private int dayOfMonth;
  // fonte dati del menu a rotazione dei medici
  private String[] spinnerMedecinsDataSource;

  // costruttori
  public AccueilFragmentState() {

  }

  // getter e setter
...
}
  • riga 11: consente di visualizzare l’elemento selezionato nell’elenco dei medici;
  • righe 13-15: consentono di restituire la data selezionata nel calendario;
  • riga 17: consente di restituire la fonte dei dati dell’elenco dei medici;

Il ciclo di vita del frammento è implementato come segue:


// implementazione dei metodi della classe padre -------------------------------------
  @Override
  public CoreState saveFragment() {
    // si salva la vista
    AccueilFragmentState state = new AccueilFragmentState();
    state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
    state.setDayOfMonth(edtJourRv.getDayOfMonth());
    state.setMonth(edtJourRv.getMonth());
    state.setYear(edtJourRv.getYear());
    state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_ACCUEIL;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // si recuperano i medici dalla sessione
    medecins = session.getMédecins();
    // Prima visita?
    if (previousState == null) {
      // si crea la tabella visualizzata dallo spinner
      spinnerMedecinsDataSource = new String[medecins.size()];
      int i = 0;
      for (Medecin medecin : medecins) {
        spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
        i++;
      }
    } else {
      // non è la prima visita
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // il calendario
    calendrier = Calendar.getInstance();
  }

  @Override
  protected void initView(CoreState previousState) {
    // si associa lo spinner dei medici alla relativa fonte di dati
    ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
    dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerMedecins.setAdapter(dataAdapterMedecins);
    // data minima del calendario fino ad oggi
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // Prima visita?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // si ripristina lo stato della sessione corrente
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // selezione dei medici tramite spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // calendario
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // chiamata al termine o all'annullamento di tutte le attività
    // Stato del menu
    initMenu();
    // vista successiva?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }

  // metodi privati ------------------------------------------------
  private void initMenu() {
    // stato del menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • righe 2-9: quando la classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
    • riga 6: la posizione selezionata nell'elenco dei medici;
    • righe 7-9: il giorno del mese, il mese e l'anno della data selezionata nel calendario;
    • riga 10: la fonte dei dati dello spinner dei medici;
  • righe 14-17: il numero del frammento è [IMainActivity.VUE_ACCUEIL];
  • righe 19-39: eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
    • righe 25-31: in caso di prima visita, viene costruita la fonte dati dello spinner dei medici;
    • righe 33-35: per le visite successive, la fonte dati dello spinner viene recuperata dallo stato precedente del frammento;
  • righe 41-54: eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
    • righe 50-53: per la prima visita, viene visualizzato il menu senza l’azione [Annuler] (righe 88-92);
    • righe 43-48: per tutte le visite, che siano la prima o meno, si associa lo spinner dei medici alla sua fonte (righe 44-46) e si imposta la data minima del calendario alla data odierna (riga 48);
  • righe 56-60: eseguite quando si accede al frammento tramite un’operazione [SUBMIT]. In questo caso si proviene dalla vista [CONFIG]. Si riporta il menu allo stato iniziale;
  • righe 62-70: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
    • riga 67: si riposiziona lo spinner dei medici sull'ultimo medico selezionato;
    • riga 69: si imposta il calendario sull'ultima data scelta;
  • righe 72-74: vengono eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Non c'è altro da fare;
  • righe 76-85: vengono eseguite quando tutte le attività asincrone sono terminate;
    • riga 80: si riporta il menu allo stato predefinito;
    • righe 82-84: se le attività si sono concluse normalmente, si passa alla vista successiva, altrimenti si rimane sulla stessa vista;

3.6.8. Gestione della vista Agenda

3.6.8.1. La vista

La vista iniziale è la seguente:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

Type
Nom
1
TextView
txtTitre2
2
ListView
lstCreneaux

3.6.8.2. Il frammento

La vista Agenda è gestita dal seguente frammento [AgendaFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;

  // agenda visualizzata dal frammento
  private AgendaMedecinJour agenda;
  // informazioni ListView sulle fasce orarie
  private int firstPosition;
  private int top;
  // appuntamento eliminato o meno
  private boolean rdvSupprimé;
  // numero della fascia oraria aggiunta o eliminata
  private int numCréneau;

  // aggiornamento dell'agenda dopo un'aggiunta/eliminazione
  private void updateAgenda() {
  ...
  }

...

  // implementazione dei metodi della classe padre ------------------------------------------------------
  ...
}
  • riga 27: il frammento è associato al seguente menu [menu_agenda]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionAgenda"
        android:title="@string/actionAgenda"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
    </menu>
  </item>
</menu>
  • righe 32-35: gli elementi dell'interfaccia visiva;
  • righe 37-45: dati globali dei metodi;

3.6.8.2.1. Metodo [updateAgenda]

La (ri)generazione dell'elenco delle fasce orarie dell'agenda è necessaria in diversi punti del codice. È stata fattorizzata nel seguente metodo privato [updateAgenda]:


  // aggiornamento dell'agenda dopo un'aggiunta/eliminazione
  private void updateAgenda() {
    // (ri)generazione delle fasce orarie dell'agenda
    // l'agenda viene acquisita nella sessione e memorizzata in un campo del frammento
    agenda = session.getAgenda();
    // rigenerazione del ListView delle fasce orarie
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // ci si riposiziona nel punto corretto del ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • riga 5: l'agenda viene prelevata dalla sessione e memorizzata nel campo [agenda] del frammento;
  • righe 7-9: si definisce l’adattatore del componente [ListView]. Questo adattatore definisce sia la fonte dati di [ListView] sia il modello di visualizzazione di ciascun elemento di quest’ultima. Presenteremo questo adattatore prossimamente;
  • riga 11: si torna alla posizione precedente dell’agenda. Infatti, è visibile solo una parte delle fasce orarie della giornata. Se si aggiunge o si elimina un appuntamento nell’ultima fascia oraria, il codice sopra riportato aggiornerà la pagina per visualizzare la nuova agenda. Questo aggiornamento fa sì che ci si ritrovi nuovamente posizionati sulla prima fascia oraria, il che non è auspicabile. La riga 5 risolve questo problema. La descrizione di questa soluzione si trova in URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

La classe [ListCreneauxAdapter] serve a definire una riga del [ListView]:

Image

Come si vede sopra, a seconda che la fascia oraria abbia o meno un appuntamento, la visualizzazione non è la stessa. Il codice della classe [ListCreneauxAdapter] è il seguente:


...

public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {

    // la tabella delle fasce orarie
    private CreneauMedecinJour[] creneauxMedecinJour;
    // il contesto di esecuzione
    private Context context;
    // l'ID del layout di visualizzazione di una riga dell'elenco delle fasce orarie
    private int layoutResourceId;
    // listener dei clic
    private AgendaFragment vue;

    // costruttore
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // si memorizzano le informazioni
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // si ordina la tabella delle fasce orarie in base all'orario
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ...
}

// ordinamento della tabella delle fasce orarie
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • riga 3: la classe [ListCreneauxAdapter] deve estendere un adattatore predefinito per le classi [ListView], in questo caso la classe [ArrayAdapter] che, come indica il nome, fornisce alla classe [ListView] un array di oggetti, in questo caso di tipo [CreneauMedecinJour]. Ricordiamo il codice di questa entità:

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campi
    private Creneau creneau;
    private Rv rv;
...  
}
  • la classe [CreneauMedecinJour] contiene una fascia oraria (riga 5) e un eventuale appuntamento (riga 6) oppure null se non c’è alcun appuntamento;

Torniamo al codice della classe [ListCreneauxAdapter]:

  • riga 15: il costruttore riceve quattro parametri:
    1. l'attività Android in corso,
    2. il file XML che definisce il contenuto di ciascun elemento del [ListView],
    3. l'array degli slot orari del medico,
    4. la vista stessa;
  • riga 24: la tabella delle fasce orarie è ordinata in ordine crescente in base agli orari;

Il metodo [getView] ha il compito di generare la vista corrispondente a una riga del [ListView]. Questa comprende tre elementi:

 
Id
Type
Rôle
1
txtCreneau
TextView
créneau horaire
2
txtClient
TextView
le client
3
btnValider
TextView
lien pour ajouter / supprimer un rendez-vous

Il codice del metodo [getView] è il seguente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // si seleziona la fascia oraria corretta
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // si crea la riga
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // la fascia oraria
        TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
        txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
                .getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
        // il cliente
        TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
        String text;
        if (creneauMedecin.getRv() != null) {
            Client client = creneauMedecin.getRv().getClient();
            text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        } else {
            text = "";
        }
        txtClient.setText(text);
        // il collegamento
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // aggiungere
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // elimina
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // listener del collegamento
        btnValider.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // si trasmettono le informazioni alla vista dell'agenda
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // si restituisce la riga
        return row;
    }
  • riga 2: «posizione» è il numero di riga che verrà generato nel [ListView]. È anche il numero della fascia oraria nella tabella [creneauxMedecinJour]. Gli altri due parametri vengono ignorati;
  • riga 4: si recupera la fascia oraria da visualizzare nella riga del file [ListView];
  • riga 6: la riga viene costruita in base alla sua definizione XML
 

Il codice di [creneau_medecin.xml] è il seguente:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat" >

    <TextView
        android:id="@+id/txt_Creneau"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/txt_Client"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Creneau"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Creneau"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/btn_Valider"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Client"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Client"
        android:text="@string/btn_valider"
        android:textColor="@color/blue" />

</RelativeLayout>
 
  • righe 8-10: viene generata la fascia oraria [1];
  • righe 12-20: viene generata l'identità del cliente [2];
  • riga 23: se la fascia oraria non ha alcun appuntamento;
  • righe 25-26: viene creato il link [Ajouter] di colore blu;
  • righe 29-30: altrimenti si crea il link [Supprimer] di colore rosso;
  • righe 33-40: indipendentemente dalla natura del collegamento [Ajouter / Supprimer], sarà il metodo [doValider] della vista a gestire il clic sul collegamento. Il metodo riceverà due argomenti:
    1. il numero della fascia oraria su cui è stato cliccato,
    2. il testo del link su cui è stato cliccato;
  • riga 42: si restituisce la riga appena costruita.

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


  // clic su un link [Ajouter / Supprimer]
  public void doValider(int numCréneau, String texte) {
    // operazione in corso?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // si prende nota della posizione di scorrimento per tornarci
    // leggi [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posizione del primo elemento visibile completamente o parzialmente
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // offset Y di questo elemento rispetto alla parte superiore di ListView
    // misura l'altezza della parte eventualmente nascosta
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // si registra anche il numero della casella cliccata
    this.numCréneau = numCréneau;
    // a seconda del testo del link, non si esegue la stessa operazione
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • il metodo [doValider] riceve due informazioni:
    • il numero della fascia oraria su cui è stato cliccato;
    • il testo (Aggiungi / Elimina) del link su cui è stato cliccato;
  • righe 4-7: il clic sui link [Supprimer / Ajouter] è disabilitato se sono in corso attività asincrone. Si tratta di una scelta che facilita la scrittura del codice. È discutibile;
  • righe 11-15: si registrano le informazioni (firstPosition, top) relative a ListView delle fasce orarie nei campi del frammento, in modo che il metodo privato [updateAgenda] possa rigenerarlo con la stessa posizione di scorrimento;
  • riga 17: si registra il numero della fascia oraria cliccata;
  • righe 19-23: a seconda del testo del link cliccato, si effettua un'aggiunta o una cancellazione;

3.6.8.2.2. Metodo [doSupprimer]

Il metodo [doSupprimer] garantisce l’eliminazione dell’appuntamento relativo alla fascia oraria cliccata:


// cancellazione di un appuntamento
  private void doSupprimer() {
    // si attende il completamento di due attività
    beginWaiting(2);
    // l'appuntamento viene cancellato in background
    rdvSupprimé = false;
    // ID dell'appuntamento da eliminare
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // cancellazione tramite un'attività asincrona
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {

      @Override
      public void call(Response<Rv> responseRv) {
        // utilizzo del risultato
        consumeRv(responseRv);
      }
    });
  }

  // utilizzo di una risposta
  private void consumeRv(Response<Rv> responseRv) {
    // errore?
    if (responseRv.getStatus() != 0) {
      // messaggio
      showAlert(responseRv.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // si nota che l'appuntamento è stato cancellato
    rdvSupprimé = true;
    // si richiede l'agenda più recente
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {

        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // si elabora la risposta
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }

  // utilizzo di un calendario
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // errore?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // messaggio
      showAlert(responseAgendaMedecinJour.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // si inserisce l'agenda nella sessione
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // si aggiorna l'agenda della vista
    updateAgenda();
  }
  • riga 4: si segnala alla classe padre che si stanno per avviare due attività asincrone e si inizia ad attendere il completamento di entrambe;
  • riga 8: si recupera l'identificativo dell'appuntamento da eliminare. Il server, infatti, necessita di questa informazione;
  • righe 9-18: si richiede l’eliminazione dell’appuntamento tramite un’attività asincrona;
    • riga 10: il metodo [executeInBackground] richiede due parametri:
      • riga 10: il processo da eseguire e da osservare viene fornito dal metodo [mainActivity.supprimerRv(idRv)];
      • righe 10-17: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<Rv>]
    • riga 15: quando si riceve la risposta, questa viene passata al metodo [consumeRv] della riga 21;
  • righe 21-44: è stata ricevuta la risposta dall'attività asincrona. La si elabora;
  • righe 23-30: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
    • riga 25: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
    • riga 27: si annullano tutte le attività;
    • riga 29: si torna all'interfaccia utente;
  • riga 32: se non si è verificato alcun errore, si registra che l'appuntamento è stato cancellato;
  • righe 34-43: anziché limitarsi a eliminare l’appuntamento dall’agenda attualmente visualizzata dal frammento, si richiede la nuova agenda del medico. Infatti, l’applicazione è multiutente e anche altri utenti potrebbero aver modificato l’agenda del medico. Quindi è meglio disporre della versione più recente;
  • righe 34-43, 47-61: si ripete quanto fatto nel frammento [AccueilFragment], questa volta utilizzando le informazioni ricavate dalla sessione;

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


  // inizio dell'attesa
  protected void beginWaiting(int numberOfRunningTasks) {
    // si prepara l'avvio delle attività
    beginRunningTasks(numberOfRunningTasks);
    // stato dei pulsanti e dei menu
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • riga 4: si comunica al processo padre che si sta per avviare il processo [numberOfRunningTasks];
  • riga 6: si nascondono tutte le opzioni del menu;
  • riga 7: per rendere poi visibile l’opzione [Actions/Annuler];

3.6.8.2.3. Metodo [doAnnuler]

Il clic sull'opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:


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

3.6.8.2.4. Opzione di menu [Retour à la configuration]

Il clic sull’opzione di menu [Retour à la configuration] viene gestito nel modo seguente:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // si passa alla vista di configurazione
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • riga 4: si passa alla vista di configurazione con l'azione [NAVIGATION]. Ciò significa che si desidera ripristinare la vista di configurazione nello stato in cui era stata lasciata;

3.6.8.2.5. Opzione di menu [Retour à l'accueil]

Il clic sull’opzione di menu [Retour à l'accueil] viene gestito in modo simile:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // si passa alla vista iniziale
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. Gestione del ciclo di vita del frammento

Il frammento presenta il seguente stato [AgendaFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AgendaFragmentState extends CoreState {

  // titolo della vista
  private String titre;
  // ListView
  private int firstPosition;
  private int top;

  // costruttori
  public AgendaFragmentState() {

  }

  public AgendaFragmentState(String titre) {
    this.titre = titre;
  }

  // getter e setter
...
}
  • riga 10: il titolo visualizzato nella parte superiore della vista;
  • righe 12-13: consentono di restituire il scrolling del ListView relativo agli orari del medico;

Il ciclo di vita del frammento è implementato come segue:


// implementazione dei metodi della classe padre ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // salvataggio dello stato
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // si registra la posizione di scorrimento per tornarci
    // leggi [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posizione del primo elemento visibile completamente o parzialmente
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // offset Y di questo elemento rispetto alla parte superiore di ListView
    // misura l'altezza della parte eventualmente nascosta
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // si memorizza tutto questo
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AGENDA;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Prima visita?
    if (previousState != null) {
      // Non è la prima visita
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // e le informazioni da ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // si recupera l'agenda
    agenda = session.getAgenda();
    // si genera il titolo della pagina
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // stato del menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // si rigenera il titolo della pagina
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }

  @Override
  protected void notifyEndOfUpdates() {
    // si rigenera l'elenco delle fasce orarie
    updateAgenda();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // stato del menu
    initMenu();
    // in caso di annullamento, se l'appuntamento è stato cancellato, è necessario aggiornare l'agenda locale
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // si elimina l'appuntamento dall'agenda locale (non è stato possibile accedere all'agenda globale)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // si aggiorna l'interfaccia visiva
      updateAgenda();
    }
  }


  // metodi privati ------------------------------------------------
  private void initMenu() {
    // stato del menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • righe 2-19: quando la sua classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
    • riga 6: il titolo visualizzato nella parte superiore della vista;
    • righe 7-17: le informazioni (top, firstPosition) che consentiranno di ricreare il scrolling del ListView;
  • righe 21-24: il numero del frammento è [IMainActivity.VUE_AGENDA];
  • righe 26-35: vengono eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
    • righe 30-34: se non si tratta della prima visita al frammento, si recuperano le informazioni (top, firstPosition) che consentiranno di ricostituire il scrolling a partire dal ListView;
  • righe 38-40: vengono eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState == null) o ricostruita nelle volte successive (previousState != null). Qui non c'è nulla da fare perché il ListView degli slot verrà generato dal metodo privato [updateAgenda] (righe 61-65);
  • righe 42-52: eseguite quando si arriva al frammento tramite un'operazione [SUBMIT]. Si proviene quindi dalla vista [ACCUEIL];
    • riga 45: si recupera l'agenda attivata da [AccueilFragment];
    • righe 47-49: si genera il titolo della vista;
    • il ListView degli slot verrà generato dal metodo privato [updateAgenda] (righe 61-65);
  • righe 54-59: eseguite quando si arriva al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
    • righe 57-58: si rigenera il titolo della vista;
    • il ListView delle fasce orarie verrà generato dal metodo privato [updateAgenda] (righe 61-65);
  • righe 72-74: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. Si aggiorna il ListView delle fasce orarie poiché questo aggiornamento è necessario indipendentemente dal modo in cui si arriva al frammento;
  • righe 67-77: eseguite quando tutte le attività asincrone sono terminate;
    • riga 70: si riporta il menu allo stato predefinito (righe 82-86);
    • riga 72: c'erano due attività asincrone. Si verifica se la prima (l'eliminazione dell'appuntamento) è andata a buon fine, nonostante un annullamento;
    • riga 74: in caso affermativo, si elimina l'appuntamento dall'agenda locale
    • riga 75: e si aggiorna la visualizzazione dello stesso;

3.6.9. Gestione della schermata di aggiunta di un appuntamento

3.6.9.1. La schermata

La schermata di aggiunta di un appuntamento è la seguente:

Image

Gli elementi dell'interfaccia visiva sono i seguenti:

Type
Nom
1
TextView
txtTitre2
2
Spinner
spinnerClients

3.6.9.2. Il frammento

La schermata di aggiunta di un appuntamento è gestita dal seguente frammento [AjoutRvFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

import java.util.List;
import java.util.Locale;

@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {

  // gli elementi dell'interfaccia visiva
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;

  // i clienti
  private List<Client> clients;

  // dati locali
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;

  // convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...

  // implementazione dei metodi della classe padre ----------------------------------
...
}
  • riga 26: il frammento è associato al seguente menu [menu_ajout_rv]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
      <item
        android:id="@+id/navigationToAgenda"
        android:title="@string/navigationToAgenda"/>
    </menu>
  </item>
</menu>
  • righe 30-33: gli elementi dell'interfaccia visiva;
  • riga 36: l'elenco dei clienti;
  • riga 43: la fonte dati dello spinner dei clienti;

Il clic sul link [Valider] è gestito dal seguente metodo [doValider]:


  // i clienti
  private List<Client> clients;

  // dati locali
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// convalida della pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // si recupera il cliente selezionato
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // inizio dell'attesa di 2 attività asincrone
    beginWaiting(2);
    // si aggiunge il RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {

        @Override
        public void call(Response<Rv> responseRv) {
          // si elabora la risposta
          consumeRv(responseRv);
        }
      });
  }

  // elaborazione di un oggetto Response<Rv>
  void consumeRv(Response<Rv> responseRv) {
    // errore?
    if (responseRv.getStatus() != 0) {
      // messaggio
      showAlert(responseRv.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // si nota che l'appuntamento è stato aggiunto
    rdvAjouté = true;
    // si salva l'appuntamento
    this.rv = responseRv.getBody();
    // si richiede la nuova agenda
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // si elabora la risposta
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  // elaborazione di un oggetto Response<AgendaMedecinJour>
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // errore?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // messaggio
      showAlert(responseAgendaMedecinJour.getMessages());
      // annullamento
      doAnnuler();
      // ritorno a UI
      return;
    }
    // si inserisce l'agenda nella sessione
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • riga 13: all’avvio del metodo [doValider], i campi 2, 5, 6 e 9 sono stati inizializzati durante il ciclo di vita del frammento. Vedremo come;
  • riga 15: si recupera l’entità [Client] corrispondente all’elemento selezionato nel selettore dei clienti;
  • riga 17: si comunica alla classe padre che si stanno per avviare due attività asincrone e si prepara l'attesa;
  • riga 19: inizialmente l’appuntamento non è ancora stato aggiunto all’agenda del medico;
  • righe 20-30: si richiede al server l'aggiunta di un appuntamento;
    • riga 20: il metodo [executeInBackground] richiede due parametri:
      • riga 20: il processo da eseguire e osservare è fornito dal metodo [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
      • righe 22-29: il secondo parametro è un'istanza di tipo [Action1<T>] dove T è il tipo restituito dal processo monitorato, in questo caso [Response<Rv>]
    • riga 27: quando si riceve la risposta, questa viene passata al metodo [consumeRV] della riga 33;
  • righe 33-56: è stata ricevuta la risposta dal server. La si elabora;
    • righe 35-42: si verifica innanzitutto se il server ha segnalato un errore nel campo [status] della risposta;
    • riga 37: in caso di errore, si visualizzano i messaggi che il server ha inserito nel campo [messages] della risposta;
    • riga 39: si annullano tutte le attività;
    • riga 41 : si torna all'interfaccia utente;
    • riga 44: se non si è verificato alcun errore, si registra che l'appuntamento è stato aggiunto;
    • riga 46: si memorizza l'appuntamento aggiunto in un campo del frammento;
    • righe 47-55: come già fatto durante l'eliminazione di un appuntamento, dopo l'aggiunta dell'appuntamento si richiede al server l'agenda più recente del medico;
  • righe 47-56, 59-71: si tratta di un codice già incontrato più volte;

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


  // inizio dell'attesa
  protected void beginWaiting(int numberOfRunningTasks) {
    // si prepara l'avvio delle attività
    beginRunningTasks(numberOfRunningTasks);
    // stato dei pulsanti e dei menu
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • riga 4: si indica all’attività principale che si lanceranno le attività [numberOfRunningTasks];
  • riga 6: si nascondono tutte le opzioni del menu;
  • riga 7: per poi rendere visibile l’opzione [Actions/Annuler];

Il clic sull’opzione di menu [Annuler] è gestito dal metodo [doAnnuler]:


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

Le navigazioni indietro sono gestite dai tre metodi seguenti:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // si passa alla vista di configurazione
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // si passa alla vista di configurazione
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
    // si passa alla vista dell'agenda
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. Gestione del ciclo di vita del frammento

Il frammento presenta il seguente stato [AjoutRvFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

// stato del frammento AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {

  // posizione del cliente selezionato
  private int selectedClientPosition;
  // titolo della vista
  private String titre;
  // fonte dati dello spinner dei clienti
  private String[] spinnerClientsDataSource;

  // getter e setter
...
}

Il ciclo di vita del frammento è implementato nel modo seguente:


// implementazione dei metodi della classe padre ----------------------------------
  @Override
  public CoreState saveFragment() {
    // salvataggio della vista
    AjoutRvFragmentState state = new AjoutRvFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
    state.setSpinnerClientsDataSource(spinnerClientsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AJOUT_RV;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // recupero dei clienti in sessione
    clients = session.getClients();
    // Prima visita?
    if (previousState == null) {
      // si crea l'array visualizzato dallo spinner
      spinnerClientsDataSource = new String[clients.size()];
      int i = 0;
      for (Client client : clients) {
        spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        i++;
      }
    } else {
      // non è la prima visita
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
    // Associazione dello spinner alla sua fonte di dati
    ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
      spinnerClientsDataSource);
    dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerClients.setAdapter(dataAdapterClients);
    // Prima visita?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // si recupera il numero della fascia oraria da prenotare nella sessione
    int position = session.getPosition();
    // si recupera l'agenda del medico nella sessione
    AgendaMedecinJour agenda = session.getAgenda();
    // si recupera il medico e la fascia oraria in cui inserire un appuntamento
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // si crea il titolo 2 della pagina
    String jour = session.getJourRv();
    txtTitre2.setText(String.format(Locale.FRANCE,
      "Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
      medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
      creneau.getMfin()));
    // selezione cliente
    spinnerClients.setSelection(0);
    // menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // ripristino dello stato precedente
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // titolo
    txtTitre2.setText(state.getTitre());
    // indicatore di caricamento
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // stato menu
    initMenu();
    // vista successiva?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // c'è stata un'annullamento - appuntamento già aggiunto?
    if (rdvAjouté) {
      // si modifica l'agenda locale (non si è ricevuta l'agenda globale)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // si visualizza l'agenda
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }

  // metodi privati -------------------
  private void initMenu() {
    // stato del menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  • righe 2-10: quando la sua classe padre lo richiede, il frammento salva lo stato dei seguenti elementi:
    • riga 6: il titolo nella parte superiore della vista;
    • riga 7: la posizione dell'elemento selezionato nello spinner dei clienti;
    • riga 8: la fonte dati dello spinner dei clienti;
  • righe 12-15: il numero del frammento è [IMainActivity.VUE_AJOUT_RV];
  • righe 17-35: eseguite quando il frammento viene generato per la prima volta (previousState==null) o rigenerato nelle volte successive (previousState !=null);
    • riga 20: si recupera l'elenco dei clienti nella sessione per inserirlo in un campo del frammento;
    • righe 22-30: in caso di prima visita, viene costruita la fonte dati dello spinner dei clienti;
    • righe 32-33: per le visite successive, la fonte dati dello spinner dei clienti viene recuperata dallo stato precedente del frammento;
  • righe 37-49: eseguite quando la vista associata al frammento viene costruita per la prima volta (previousState==null) o ricostruita nelle volte successive (previousState !=null);
    • righe 40-43: in ogni caso, lo spinner dei clienti viene associato alla sua fonte dati;
    • righe 45-48: alla prima visita, viene visualizzato il menu senza l’azione [Annuler] (righe 107-111);
  • righe 51-70: eseguite quando si arriva al frammento tramite un'operazione [SUBMIT]. Si proviene quindi dalla vista [AGENDA];
    • riga 54: si recupera il numero della fascia oraria in cui si inserirà un appuntamento;
    • righe 56-59: si recuperano l'entità [Medecin] e l'entità [Creneau] necessarie per l'aggiunta di questo appuntamento e le si inseriscono nei campi del frammento;
    • righe 61-65: con queste informazioni, è possibile costruire il titolo della vista;
    • riga 67: lo spinner dei clienti viene posizionato sul primo elemento;
    • riga 69: il menu viene riportato allo stato iniziale (senza l’opzione [Annuler]);
  • righe 72-80: vengono eseguite quando si accede al frammento tramite un'operazione [NAVIGATION] o [RESTORE];
    • riga 77: si rigenera il titolo della vista;
    • riga 79: si riposiziona lo spinner dei clienti sull'ultimo cliente selezionato;
  • righe 82-84: eseguite quando tutti gli aggiornamenti precedenti sono stati completati. A questo punto non c'è altro da fare;
  • righe 86-104: vengono eseguite quando tutte le operazioni asincrone sono terminate;
    • riga 89: si riporta il menu allo stato predefinito;
    • righe 91-94: se le attività si sono concluse normalmente, si ritorna alla vista [AGENDA] tramite un [SUBMIT] (in questo caso, avrebbe potuto essere anche un'azione di tipo NAVIGATION);
    • righe 96-103: se le operazioni si sono concluse con un'annullamento, si verifica comunque se l'appuntamento è stato aggiunto (ciò significherebbe che è stato il recupero della nuova agenda a non andare a buon fine);
    • righe 98-99: se l'appuntamento è stato aggiunto;
      • righe 98-99: l’appuntamento restituito dal server viene aggiunto all’agenda corrente, quella attiva nella sessione;
      • riga 101: si ritorna alla vista [AGENDA] tramite un'azione [SUBMIT] (in questo caso, avrebbe potuto trattarsi anche di un'azione di tipo NAVIGATION);

3.7. Exécution

Eseguire i seguenti test:

  • utilizzare l'applicazione in condizioni normali e verificare che funzioni;
  • ruotare il dispositivo per ciascuna delle viste e verificare che ciascuna venga ripristinata correttamente;
  • impostare un tempo di attesa di alcuni secondi in [IMainActivity];
  • procedere quindi all'annullamento delle attività e verificare che il risultato ottenuto sia quello previsto;
  • ruotare il dispositivo durante i tempi di attesa e verificare che le attività vengano effettivamente annullate e che non si verifichino arresti anomali;
  • modificare l’adiacenza dei frammenti in [IMainActivity] e verificare che l’applicazione continui a funzionare;