Skip to content

3. Casestudy – Afspraakbeheer

3.1. Het project

In het document [Tutoriel AngularJS / Spring 4] is een client/server-applicatie ontwikkeld voor het beheer van doktersafspraken. Hierna zullen we naar dit document verwijzen als [rdvmedecins-angular]. De applicatie had twee soorten clients:

  • een client HTML / CSS / JS;
  • een Android-client;

De Android-client werd automatisch gegenereerd op basis van de versie HTML van de client met behulp van de tool [Cordova]. Het doel van dit project is om deze Android-client handmatig opnieuw te maken met behulp van de kennis die in de voorgaande hoofdstukken is opgedaan.

Er is een belangrijk verschil tussen de twee oplossingen:

  • de client die we gaan maken, zal alleen bruikbaar zijn op Android-tablets;
  • in de versie [rdvmedecins-angular] is de mobiele webclient (HTML / CSS / JS) is bruikbaar op elk platform (Android, IoS, Windows);

3.2. De weergaven van de Android-client

Er zijn vier weergaven.

Configuratieweergave

Image

Weergave voor het kiezen van de arts en de afspraakdatum

Image

Weergave voor de keuze van het tijdvak voor de afspraak

Image

Weergave van de keuze van de klant voor de afspraak

Image

3.3. De architectuur van het project

We zullen een client/server-architectuur hanteren die vergelijkbaar is met die van het voorbeeld [Exemple-15] (zie paragraaf 1.16) in dit document:

Image

De asynchrone communicatie tussen de client en de server wordt afgehandeld met de bibliotheek RxAndroid.

3.4. De database

Deze speelt geen fundamentele rol in dit document. We vermelden deze ter informatie. We noemen deze [dbrdvmedecins] . Het is een database MySQL5 met vier tabellen:

  

3.4.1. De tabel [MEDECINS]

Deze bevat informatie over de artsen die worden beheerd door de applicatie [RdvMedecins].

  • ID: identificatienummer van de arts – primaire sleutel van de tabel
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de achternaam van de arts
  • PRENOM: zijn of haar voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)

3.4.2. De tabel [CLIENTS]

De cliënten van de verschillende artsen worden opgeslagen in de tabel [CLIENTS]:

  • ID: identificatienummer van de patiënt – primaire sleutel van de tabel
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de naam van de klant
  • PRENOM: de voornaam van de klant
  • TITRE: de aanspreektitel (mevrouw, mevrouw, mijnheer)

3.4.3. De tabel [CRENEAUX]

Deze tabel geeft een overzicht van de tijdvakken waarin de RV mogelijk zijn:

  • ID: nummer dat het tijdvak identificeert – primaire sleutel van de tabel (regel 8)
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit getal wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • ID_MEDECIN: nummer dat de arts identificeert aan wie dit tijdslot toebehoort – vreemde sleutel op de kolom MEDECINS (ID).
  • HDEBUT: starttijd van het tijdslot
  • MDEBUT: minuten begin van het tijdslot
  • HFIN: eindtijd van het tijdvak
  • MFIN: eindminuten van het tijdslot

De tweede regel van de tabel [CRENEAUX] (zie [1] hierboven) geeft bijvoorbeeld aan dat tijdvak nr. 2 om 8.20 uur begint en om 8.40 uur eindigt en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER).

3.4.4. De tabel [RV]

Deze tabel geeft een overzicht van de RV die voor elke arts zijn vastgelegd:

  • ID: nummer dat de RV op unieke wijze identificeert – primaire sleutel
  • JOUR: dag van het RV
  • ID_CRENEAU: tijdvak van RV – externe sleutel op het veld [ID] van de tabel [CRENEAUX] – bepaalt zowel het tijdvak als de betreffende arts.
  • ID_CLIENT: nummer van de klant voor wie de reservering is gemaakt – externe sleutel op het veld [ID] van de tabel [CLIENTS]

Deze tabel heeft een uniekheids -beperking op de waarden van de gekoppelde kolommen (JOUR, ID_CRENEAU):

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

Als een rij in de tabel [RV] de waarde (JOUR1, ID_CRENEAU1) voor de kolommen (JOUR, ID_CRENEAU), dan mag deze waarde nergens anders voorkomen. Anders zou dit betekenen dat er twee RV’en tegelijkertijd voor dezelfde arts zijn vastgelegd. Vanuit het oogpunt van Java-programmering start de driver JDBC van de database een SQLException wanneer dit zich voordoet.

De regel met id gelijk aan 3 (zie [1] hierboven) betekent dat er op 23/08/2006 een RV is geboekt voor tijdvak nr. 20 en klant nr. 4. Uit de tabel [CRENEAUX] blijkt dat tijdvak nr. 20 overeenkomt met het tijdvak 16.20 - 16.40 uur en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER). Uit de tabel [CLIENTS] blijkt dat klant nr. 4 mevrouw Brigitte BISTROU is.

3.4.5. Het aanmaken van de database

Om de tabellen aan te maken en te vullen, kunt u het script [dbrdvmedecins.sql] gebruiken, dat u kunt vinden in het voorbeeldarchief |ICI|.

  

Met [WampServer] (zie paragraaf 6.15) kunt u als volgt te werk gaan:

 
  • in [1] klikt men op het pictogram van [WampServer] en kiest men de optie [PhpMyAdmin] [2],
  • in [3], selecteer je in het venster dat is geopend de link [Bases de données],
 
  • en [4-6], importeer je een bestand SQL,
  • in [7] selecteren we het script SQL en in [8] voeren we het uit,
  • in [9] zijn de tabellen van de database aangemaakt. We volgen een van de links,
 
  • in [10], de inhoud van de tabel.

Vervolgens zullen we niet meer op deze database terugkomen, maar de lezer wordt uitgenodigd om de ontwikkeling ervan tijdens de tests te volgen, vooral wanneer de applicatie niet werkt.

3.5. De webserver / jSON

Image

We richten ons hier op de server [1]. We gaan deze niet verder uitwerken. Deze is gedetailleerd beschreven in het document [Spring MVC et Thymeleaf par l'exemple]. De geïnteresseerde lezer kan daar naar verwijzen. Hij is op dezelfde manier ontwikkeld als die van de server uit voorbeeld 15. De broncode ervan is opgenomen in de voorbeelden. We gaan hier het binaire bestand ervan gebruiken:

  
  • [rdvmedecins-server-all-1.0.jar] is het binaire bestand van de server;

3.5.1. Implementatie

Ga in een opdrachtvenster naar de map die het binaire bestand van de server bevat:


...\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

om de server te starten, voer je vervolgens de volgende opdracht in (SGBD en MySQL moeten al gestart zijn):


...\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

De server geeft veel logberichten weer. Hierboven hebben we alleen die weergegeven die nuttig zijn om het proces te begrijpen:

  • regels 14-18: er wordt een ingebouwde Tomcat-server gestart op poort 8080 van de machine. Deze server voert de webapplicatie voor afsprakenbeheer uit. Deze applicatie is in feite een webservice / jSON: deze wordt opgevraagd via URL en reageert door een jSON-string te verzenden;
  • regel 24: de webservice is beveiligd met het [Spring Security]-framework. Toegang tot de URL van de webservice wordt verkregen door authenticatie;
  • regels 29-44: de URL die door de webservice worden aangeboden;

We zullen deze punten nader toelichten.

3.5.2. Beveiliging van de webservice

De door de webservice aangeboden URL-bestanden zijn beveiligd. De server verwacht in het HTTP-verzoek van de client de volgende header:

Authorization: Basic code

De verwachte code is de Base64-codering [http://fr.wikipedia.org/wiki/Base64] van de tekenreeks 'gebruiker:wachtwoord'. De webservice accepteert in de standaardinstelling alleen een gebruiker 'admin' met het wachtwoord 'admin'. De bovenstaande header wordt voor deze specifieke gebruiker de volgende regel:

Authorization: Basic YWRtaW46YWRtaW4=

Om deze header HTTP te kunnen verzenden, gebruiken we de client HTTP [Advanced Rest Client], een plug-in voor de Chrome-browser (zie paragraaf 6.13). We gaan de verschillende URL die door de webservice worden aangeboden handmatig testen om inzicht te krijgen in:

  • welke parameters de URL verwacht;
  • wat de exacte aard van het antwoord is;

3.5.3. Lijst met artsen

Met de URL [/getAllMedecins] kan de lijst met artsen worden opgevraagd:

  • in [1], de opgevraagde URL;
  • in [2], de methode HTTP die voor deze opvraging is gebruikt;
  • in [3], de beveiligingsheader HTTP van de gebruiker (admin, admin);
  • in [4] wordt het verzoek HTTP verzonden;

Het antwoord van de server is als volgt:

  • in [5] wordt het antwoord jSON van de server weergegeven, opgemaakt;
  • in [6], hetzelfde antwoord in onbewerkte vorm;

De vorm [5] maakt het mogelijk de structuur van het antwoord beter te zien. Alle antwoorden van de webservice zijn een instantie van de volgende klasse [Response]:


package rdvmedecins.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- eigenschappen
    // status van de bewerking
    private int status;
    // eventuele foutmeldingen
    private List<String> messages;
    // de inhoud van het antwoord
    private T body;

    // constructors
    public Response() {

    }

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

    // getters en setters
...
}
  • regel 9: de status van het antwoord. De waarde 0 betekent dat er geen fout is opgetreden, anders is er wel een fout opgetreden;
  • regel 11: een lijst met foutmeldingen als er een fout is opgetreden;
  • regel 13: het antwoord dat de client daadwerkelijk verwachtte;

Het antwoord op URL [/getAllMedecins] is de tekenreeks jSON van een object van het type [Response<List<Medecin>>]. De klasse [Medecin] is als volgt:


package rdvmedecins.android.dao.entities;

public class Medecin extends Personne {

    // standaardconstructor
    public Medecin() {
    }

    // constructor met parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}

Regel 3: de klasse [Medecin] is een uitbreiding van de volgende klasse [Personne]:


package rdvmedecins.android.dao.entities;

public class Personne extends AbstractEntity {
    // attributen van een persoon
    private String titre;
    private String nom;
    private String prenom;

    // standaardconstructor
    public Personne() {
    }

    // constructor met parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

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

    // getters en setters
    ...
}

Regel 3: de klasse [Personne] is een uitbreiding van de volgende klasse [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;
    }

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

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

    // getters en setters
    ...
}

Uiteindelijk is de structuur van een object [Medecin] als volgt:


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

en die van [Response<List<Medecin>>] is als volgt:

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

Vervolgens zullen we deze verkorte definities gebruiken om de reactie van de server te beschrijven. Overigens zullen we een tijdje geen schermafbeeldingen meer tonen. Het volstaat om te herhalen wat we zojuist hebben gezien. We zullen terugkomen op de schermafbeeldingen wanneer we een verzoek POST moeten uitvoeren. We zullen ook een uitvoervoorbeeld presenteren in de volgende vorm:

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. Klantenlijst

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;]

Voorbeeld:

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. Overzicht van de spreekuren van een arts

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]: identificatiecode van de arts waarvan de consulttijdvakken worden opgevraagd;
  • [hdebut]: aanvangstijd van het consult;
  • [mdebut]: minuut waarop het consult begint;
  • [hfin]: eindtijd van het consult;
  • [mfin]: de minuten waarop het consult eindigt;

Voor een tijdslot tussen 10.20 en 10.40 uur is [hdebut, mdebut, hfin, mfin] = [10, 20, 10, 40].

Voorbeeld:

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. Lijst met afspraken van een arts

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]: identificatiecode van de arts waarvan de afspraken worden opgevraagd;
  • URL [jour]: dag van de afspraken in de vorm 'jjjj-mm-dd';
  • Antwoord [jour]: idem, maar in de vorm van een Java-datum;
  • [client]: de klant van de afspraak. De structuur hiervan is eerder beschreven;
  • [idClient]: de klant-ID;
  • [creneau]: het tijdvak van de afspraak. De structuur hiervan is eerder beschreven;
  • [idCreneau]: de identificatiecode van het tijdvak;

Voorbeeld:

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. De agenda van een arts

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]: identificatiecode van de arts waarvan de afspraken worden opgevraagd;
  • URL [jour]: dag van de afspraken in de vorm 'jjjj-mm-dd';
  • [agenda]: agenda van de arts;
  • [medecin]: de betreffende arts. De structuur hiervan is eerder gedefinieerd;
  • Antwoord [jour]: de dag van de agenda in de vorm van een Java-datum;
  • [creneauxMedecinJour]: een array van elementen van het type [CreneauMedecinJour];
  • [creneau]: een tijdslot. De structuur hiervan is eerder beschreven;
  • [rv]: een afspraak. De structuur hiervan is eerder beschreven;

Voorbeeld:

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}]}}

Er is onderscheid gemaakt tussen het geval waarin er een afspraak in het tijdslot is en het geval waarin dat niet zo is.

3.5.8. Een arts opzoeken aan de hand van zijn identificatienummer

URL
/getMedecinById/{idMedecin}
Réponse

Response<Medecin> :[int status ; List<String> messages ; Medecin medecin]
  • [idMedecin]: de identificatiecode van de arts;

Voorbeeld 1:

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

Voorbeeld 2:

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

3.5.9. Een klant opzoeken op basis van zijn ID

URL
/getClientById/{idClient}
Réponse

Response<Client> :[int status ; List<String> messages ;
 Client client]
  • [idClient]: de klant-ID;

Voorbeeld 1:

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

Voorbeeld 2:

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

3.5.10. Een tijdvak opvragen op basis van de ID

URL
/getCreneauById/{idCreneau}
Réponse

Response<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [idCreneau]: de identificatiecode van het tijdvak;

Voorbeeld 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}}

Merk op dat in het antwoord niet de arts wordt vermeld die eigenaar is van het tijdvak, maar alleen zijn identificatiecode.

Voorbeeld 2:

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

3.5.11. Een afspraak maken met uw gebruikersnaam

URL
/getRvById/{idRv}
Réponse

Response<Rv> :[int status ; List<String> messages ; Rv rv]
  • [idRv]: de identificatiecode van de afspraak;

Voorbeeld 1:

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

Merk op dat in het antwoord noch de klant, noch het tijdvak van de afspraak wordt vermeld, maar alleen hun identificatiecodes.

Voorbeeld 2:

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

3.5.12. Een afspraak toevoegen

Met de URL [/ajouterRv] kunt u een afspraak toevoegen. De gegevens die hiervoor nodig zijn (de dag, het tijdvak en de klant) worden doorgegeven via een verzoek HTTP POST. We laten zien hoe je dit verzoek kunt uitvoeren met de tool [Advanced Rest Client].

Image

  • in [1] wordt de URL opgevraagd;
  • in [2] wordt deze opgevraagd door een POST;
  • in [3-4] wordt aan de server aangegeven dat de waarden die naar de server worden verzonden, de vorm hebben van een tekenreeks jSON;
  • in [4], de header HTTP voor de authenticatie;
  • in [5], de informatie die door POST wordt doorgegeven. Dit is een tekenreeks jSON die het volgende bevat:
    • [jour]: de dag van de afspraak in de vorm 'jjjj-mm-dd',
    • [idClient]: het identificatienummer van de cliënt voor wie de afspraak is gemaakt,
    • [idCreneau]: de identificatiecode van het tijdvak van de afspraak. Aangezien een tijdvak bij een specifieke arts hoort, wordt hiermee ook de arts aangeduid;
  • in [6] wordt het verzoek verzonden;

De string jSON die wordt verzonden, is die van het volgende object van het type [PostAjouterRv]:


public class PostAjouterRv {

  // gegevens van het bericht
  private String jour;
  private long idClient;
  private long idCreneau;

  // constructors
  public PostAjouterRv() {

  }

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

  // getters en setters
  ...
}

Het antwoord van de server is van het type [Response<Rv>] [int status; List<String> messages; Rv rv], waarbij [rv] de toegevoegde afspraak is.

Het antwoord van de server op het bovenstaande verzoek is als volgt:

 

Hierbij valt op dat bepaalde gegevens niet in [idClient, idCreneau] zijn opgenomen, maar wel te vinden zijn in de velden [client] en [creneau]. De belangrijke informatie is de ID van de toegevoegde afspraak (209). De webservice had volstaan met het terugsturen van alleen deze informatie.

3.5.13. Een afspraak verwijderen

Deze handeling gebeurt eveneens via een POST:

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

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

De geplaatste waarde is de tekenreeks jSON van een object van het type [PostSupprimerRv], namelijk:


public class PostSupprimerRv {

  // postgegevens
  private long idRv;

  // constructors
  public PostSupprimerRv() {

  }

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

  // getters en setters
  ...
}
  • regel 4: [idRv] is de ID van de afspraak die moet worden verwijderd.

Voorbeeld 1:

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

De afspraak met nr. 209 is inderdaad verwijderd vanwege [status=0].

Voorbeeld 2:

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

3.6. De Android-client

Image

Nu de server [1] in detail is beschreven en operationeel is, gaan we de Android-client [2] bekijken.

3.6.1. Architectuur van het Android Studio-project

Het project volgt de architectuur van het project [client-android-skel] (zie paragraaf 1.17). In de bovenstaande architectuur van de Android-client onderscheiden we drie blokken:

  • de [DAO]-laag, die verantwoordelijk is voor de communicatie met de webservice;
  • de [vues]-lagen die zorgen voor de communicatie met de gebruiker;
  • de [activité] die de verbinding vormt tussen de twee voorgaande blokken. De weergaven hebben geen kennis van de [DAO]-laag. Ze communiceren uitsluitend met de activiteit.

Deze architectuur komt tot uiting in de architectuur van het Android Studio-project van de Android-client:

 
  • het pakket [activity] implementeert de activiteit;
  • het pakket [architecture] bevat de architectuurelementen die we eerder hebben ontwikkeld;
  • het pakket [dao] implementeert de laag [DAO];
  • het pakket [fragments] implementeert de [vues];

3.6.2. Aanpassing van het project

  

De map [architecture / custom] bevat de aanpasbare elementen van de architectuur.

De interface [IMainActivity] ziet er als volgt uit:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // van weergave wisselen
  void navigateToView(int position, ISession.Action action);

  // afhandeling van wachtrijen
  void beginWaiting();

  void cancelWaiting();

  // applicatieconstanten -------------------------------------

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 000;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = false;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = true;

  // aantal fragmenten van de applicatie
  int FRAGMENTS_COUNT = 4;

  // aantal weergaven
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • regels 25, 28: aanpassing van de laag [DAO];
  • regel 31: deze applicatie voert geauthentificeerde toegangen tot de server uit;
  • regel 40: er is een laadafbeelding nodig;
  • regel 43: de applicatie heeft vier fragmenten;
  • regels 46-49: de nummers van de vier fragmenten;
  • regel 37: er zijn geen tabbladen;

De basisklasse [CoreState] voor de fragmentstatussen ziet er als volgt uit:


package client.android.architecture.custom;

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

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = AccueilFragmentState.class),
  @JsonSubTypes.Type(value = AgendaFragmentState.class),
  @JsonSubTypes.Type(value = AjoutRvFragmentState.class),
  @JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
  // fragment al dan niet bezocht
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regels 15-18: de vier fragmenten hebben een status:
  

Ten slotte bevat de sessie de gegevens die door de fragmenten worden gedeeld:


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 {
  // elementen die niet in jSON kunnen worden geserialiseerd, moeten de annotatie @JsonIgnore hebben

  // lijst met artsen
  private List<Medecin> médecins;
  // lijst met klanten
  private List<Client> clients;
  // agenda van een arts voor een bepaalde dag
  private AgendaMedecinJour agenda;
  // positie van het aangeklikte element in de agenda
  private int position;
  // dag van de afspraak in Engelse notatie "yyyy-MM-dd"
  private String dayRv;
  // dag van de afspraak in Franse notatie "dd-MM-yyyy"
  private String jourRv;

  // getters en setters
...
}
  • regels 17-28: de sessie slaat zes gegevens op. We zullen de rol hiervan uitleggen wanneer dat nodig is.

3.6.3. De laag [DAO]

  • in [1], de entiteiten die in de antwoorden van de server zijn ingekapseld. Deze zijn beschreven in paragraaf 3.5;
  • in [2], de elementen van de client die de communicatie met de server beheren;

We zullen niet terugkomen op de elementen [1]. Deze zijn al besproken. De lezer wordt verzocht indien nodig terug te gaan naar paragraaf 3.5. We gaan de implementatie van het pakket [service] bestuderen. Dit brengt ons ertoe om ook de implementatie van de beveiligde communicatie tussen de client en de server te bespreken.

3.6.3.1. Implementatie van de communicatie tussen client en server

  

De klasse [WebClient] is een component van AA die het volgende beschrijft:

  • de URL die door de webservice worden aangeboden;
  • hun parameters;
  • hun antwoorden;

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);

  // lijst met artsen
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();

  // lijst met klanten
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();

  // lijst met beschikbare tijdvakken van een arts
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);

  // lijst met afspraken van een arts
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);

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

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

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

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

  // een afspraak toevoegen RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);

  // Een afspraak verwijderen
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);

  // De agenda van een arts opvragen
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

}
  • regels 19-60: hier staan alle URL-componenten die in paragraaf 3.5 zijn besproken;
  • regel 16: de component [RestTemplate] van [Spring Android] waarop de communicatie tussen client en server is gebaseerd;

3.6.3.2. De interface [IDao]

  

De interface [IDao] van de laag [DAO] is als volgt:


package rdvmedecins.android.dao.service;

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

import java.util.List;

public interface IDao {
  // URL van de webservice
  public void setUrlServiceWebJson(String url);

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

  // time-out van de client
  public void setTimeout(int timeout);

  // lijst met klanten
  public Observable<List<Client>> getAllClients();

  // lijst met artsen
  public Observable<List<Medecin>> getAllMedecins();

  // lijst met tijdvakken van een arts
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);

  // lijst met afspraken van een arts op een bepaalde dag
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);

  // een klant zoeken op basis van zijn ID
  public Observable<Client> getClientById(long id);

  // een arts zoeken op basis van zijn ID
  public Observable<Medecin> getMedecinById(long id);

  // een afspraak zoeken op basis van de id
  public Observable<Rv> getRvById(long id);

  // een tijdslot zoeken op basis van de id
  public Observable<Creneau> getCreneauById(long id);

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

  // een RV verwijderen
  public Observable<Rv> supprimerRv(long idRv);

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

  // debugmodus
  void setDebugMode(boolean isDebugEnabled);
}
  • regel 10: om de URL van de webservice / jSON vast te leggen;
  • regel 13: om de gebruiker van de client/server-communicatie in te stellen. [user] is de gebruikersnaam, [mdp] het wachtwoord;
  • regel 16: om een maximale wachttijd voor het antwoord van de server vast te stellen;
  • regels 18-49: aan elke door de webservice blootgestelde URL komt een methode overeen. Deze nemen de handtekening over van de methoden met dezelfde naam van de component AA [WebClient];
  • regel 52: om de modus debug van de laag [DAO] te controleren;

3.6.3.3. De klasse [Dao]

  

De implementatie [DAO] van de voorgaande interface [IDao] is als volgt:


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 {

  // webserviceklant
  @RestService
  protected WebClient webClient;
  // beveiliging
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // de RestTemplate
  private RestTemplate restTemplate;
  // factory van de 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));
    }
    // authenticatie-interceptor?
    if (isBasicAuthentificationNeeded) {
      // de authenticatie-interceptor wordt toegevoegd
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }

  }

  // privé-methoden -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // implementatie van de interface IDao --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // logboek
    log("getAllClients");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    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");
    // resultaat
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }

}
  • regels 18-72: dit zijn de standaardregels in de klasse [Dao] van het project [client-android-skel];
  • regels 74-216: implementatie van de interface [IDao]. De methoden die de door de webservice blootgestelde URL-component opvragen, delegeren deze opvraging aan de component AA [WebClient] (regels 22-23);
  • regels 58-63: als de communicatie tussen client en server wordt geauthenticeerd via basisautorisatie, wordt er een interceptor toegevoegd aan de component [RestTemplate]. Dit heeft tot gevolg dat elk verzoek HTTP dat door de component [RestTemplate] wordt verzonden, wordt onderschept door de klasse [MyAuthInterceptor] (regels 25-26);

De klasse [MyAuthInterceptor] ziet er als volgt uit:


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 {

  // gebruiker
  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;
  }
}
  • regel 15: de klasse [MyAuthInterceptor] is een component AA van het type [singleton];
  • regel 16: de klasse [MyAuthInterceptor] is een uitbreiding van de Spring-interface [ClientHttpRequestInterceptor]. Deze interface heeft één methode, namelijk de methode [intercept] op regel 22. Deze interface wordt uitgebreid om elk HTTP-verzoek van de client te onderscheppen. De methode [intercept] ontvangt drie parameters;
    • [HtpRequest request]: het onderschepte verzoek HTTP,
    • [byte[] body]: de body ervan, indien aanwezig (bijvoorbeeld geposte waarden),
    • [ClientHttpRequestExecution execution]: de Spring-component die het verzoek uitvoert;

We onderscheppen alle HTTP-verzoeken van de Android-client om er de authenticatieheader HTTP aan toe te voegen, zoals beschreven in paragraaf 3.5.

  • regel 23: we halen de HTTP-headers uit het onderschepte verzoek;
  • regel 24: we maken de authenticatieheader HTTP aan. De gebruikte authenticatiemethode (base64-codering van de tekenreeks 'user:mdp') wordt geleverd door de Spring-klasse [HttpBasicAuthentication];
  • regel 25: de authenticatieheader die we zojuist hebben aangemaakt, wordt toegevoegd aan de huidige headers van het onderschepte verzoek;
  • regel 26: de uitvoering van het onderschepte verzoek wordt voortgezet. Samenvattend: het onderschepte verzoek is aangevuld met de authenticatieheader;

De implementaties van de methoden van de interface [IDao] volgen allemaal hetzelfde patroon. Laten we de methode [getAgendaMedecinJour] als voorbeeld nemen:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // log
    log("getAgendaMedecinJour");
    // resultaat
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • regel 2: de methode verwacht twee parameters:
    • [idMedecin]: de ID van de arts waarvan de agenda wordt opgevraagd;
    • [jour]: de dag waarvoor de agenda wordt opgevraagd;
  • regel 6: de methode [getResponse] van de bovenliggende klasse [AbstractDao] wordt aangeroepen. Deze methode verwacht een parameter van het type [IRequest<T>], waarbij T het type is dat wordt geretourneerd door de methode [getAgendaMedecinJour] in regel 2, in dit geval [Response<AgendaMedecinJour>]. De interface [IRequest] heeft slechts één methode: [getResponse] (regel 8);
  • regels 8-10: implementatie van de methode [IRequest.getResponse]. Deze methode moet het resultaat retourneren dat wordt verwacht door de methode [getAgendaMedecinJour] op regel 2 van het type [Response<AgendaMedecinJour>];
  • regel 9: het antwoord wordt geretourneerd door de methode [webClient.getAgendaMedecinJour]:

  // de agenda van een arts opvragen
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

De parameters die op regel 9 worden gebruikt, zijn dezelfde als die welke op regel 2 aan de methode [getAgendaMedecinJour] zijn doorgegeven. Daarom moeten deze parameters het attribuut final hebben;

3.6.4. De activiteit [MainActivity]

Serveur
  

De klasse [MainActivity] is als volgt:


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 {

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

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

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

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

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

  @Override
  protected void navigateOnTabSelected(int position) {

  }

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

  // interface IDao -----------------------------------------------------
...

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

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

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

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

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

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

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

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

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

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

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
    return dao.getAgendaMedecinJour(idMedecin, jour);
  }
}
  • regels 21-66: deze regels zijn standaard opgenomen in het sjabloon [client-android-skel];
  • regels 66-119: implementatie van de interface [IDao]. Alle methoden delegeren het werk aan de laag [DAO] vanaf regel 26;
  • regels 42-46: de methode [getFragments] retourneert de array met de vier fragmenten van de applicatie;
  • regels 58-61: de configuratieweergave is de eerste weergave die wordt getoond wanneer de applicatie wordt gestart;

3.6.5. De sessie

  

De klasse [Session] dient om de informatie op te slaan die tussen fragmenten moet worden doorgegeven. Deze ziet er als volgt uit:


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 {
  // lijst met artsen
  private List<Medecin> médecins;
  // lijst met klanten
  private List<Client> clients;
  // agenda
  private AgendaMedecinJour agenda;
  // positie van het aangeklikte element in de agenda
  private int position;
  // dag van de afspraak in Engelse notatie "yyyy-MM-dd"
  private String dayRv;
  // dag van de afspraak in Franse notatie "dd-MM-yyyy"
  private String jourRv;


  // getters en setters
...
}
  • regel 10: de klasse [Session] is een component AA waarvan één enkel exemplaar is geïnstantieerd;
  • regels 12-15: in deze casestudy gaan we ervan uit dat de lijsten met artsen en klanten niet veranderen. Deze worden bij het opstarten van de applicatie opgevraagd en in de sessie opgeslagen, zodat de fragmenten er gebruik van kunnen maken;
  • regels 20-23: de gewenste dag voor een afspraak. Deze wordt in twee vormen verwerkt: in Franse notatie (regel 23) binnen de Android-client en in Engelse notatie (regel 21) voor de communicatie met de server;
  • regel 19: de positie van het aangeklikte element (link ‘toevoegen’/‘verwijderen’) in de agenda;

3.6.6. Beheer van de configuratieweergave

3.6.6.1. De weergave

De configuratieweergave is de weergave die bij het opstarten van de applicatie wordt getoond:

Image

De elementen van de visuele interface zijn als volgt:

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

3.6.6.2. Het fragment

Het configuratiescherm wordt beheerd door het volgende fragment [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 {

  // de elementen van de visuele interface
  @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;

  // de invoervelden
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;

  // validatie van de pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // Implementatie van methoden van de bovenliggende klasse -------------------------------------------
 ...

}
  • regel 25: het fragment is gekoppeld aan het volgende 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>
  • regels 28-38: de elementen van de visuele interface;
  • regels 41-43: de drie invoervelden van het formulier;

Een klik op de menuoptie [Valider] wordt afgehandeld door de methode [doValider]:


// validatie van de pagina
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // eventuele eerdere foutmeldingen worden in de cache opgeslagen
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // de geldigheid van de invoer wordt getest
    if (!isPageValid()) {
      return;
    }
    // het URL van de webservice invullen
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // de gebruiker wordt ingevuld
    mainActivity.setUser(utilisateur, mdp);
    // begin van de wachttijd – er worden 2 asynchrone taken gestart
    beginWaiting(2);
    // artsen
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // het antwoord wordt verwerkt
        consumeMedecins(responseMedecins);
      }
    });
    // klanten
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // het antwoord wordt verwerkt
        consumeClients(responseClients);
      }
    });
  }


  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // fout?
    if (responseMedecins.getStatus() != 0) {
      // bericht
      showAlert(responseMedecins.getMessages());
      // annulering
      doAnnuler();
      // terug naar de UI
      return;
    }
    // artsen worden tijdens de sessie opgeslagen
    session.setMédecins(responseMedecins.getBody());
  }

  private void consumeClients(Response<List<Client>> responseClients) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // fout?
    if (responseClients.getStatus() != 0) {
      // bericht
      showAlert(responseClients.getMessages());
      // annulering
      doAnnuler();
      // terug naar UI
      return;
    }
    // klanten worden in de sessie opgeslagen
    session.setClients(responseClients.getBody());
  }
  • regels 8-10: de geldigheid van de drie invoervelden van het formulier wordt gecontroleerd. Als het formulier ongeldig is, wordt er niet verder gegaan;
  • regels 11-14: de gegevens die nodig zijn voor de laag [DAO] worden doorgegeven aan de activiteit;
  • regel 16: aan de bovenliggende klasse wordt aangegeven dat er twee asynchrone taken worden gestart en de wachtrij wordt voorbereid;
  • regels 17-24: de lijst met artsen wordt opgevraagd;
  • regel 18: de methode [executeInBackground] verwacht twee parameters:
    • regel 18: het uit te voeren en te observeren proces wordt geleverd door de methode [mainActivity.getAllMedecins()];
    • regels 18-24: de tweede parameter is een instantie van het type [Action1<T>], waarbij T het type is dat door het geobserveerde proces wordt geretourneerd, in dit geval [Response<List<Medecin>>]
  • regel 22: zodra het antwoord wordt ontvangen, wordt dit doorgegeven aan de methode [consumeMedecins] op regel 36;
  • regels 25-33: nadat een eerste asynchrone taak is gestart, wordt een tweede gestart om de lijst met klanten op te vragen. Er worden dus twee taken parallel uitgevoerd;
  • regels 36-52: we hebben het antwoord van de taak met betrekking tot de artsen ontvangen. We verwerken dit;
  • regels 42-49: we kijken eerst of de server een fout heeft gemeld in het veld [status] van het antwoord;
  • regel 44: als er een fout is, geven we de berichten weer die de server in het veld [messages] van het antwoord heeft geplaatst;
  • regel 46: alle taken worden geannuleerd;
  • regel 48: we keren terug naar de gebruikersinterface;
  • regel 51: als er geen fout is opgetreden, wordt de lijst met artsen in de sessie opgeslagen;

De geldigheid van de invoer (regel 8) wordt gecontroleerd met de volgende methode:


  private boolean isPageValid() {
    // de geldigheid van de ingevoerde gegevens wordt gecontroleerd
    boolean erreur;
    URI service;
    // geldigheid van de URL van de dienst 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) {
      // de fout wordt genoteerd
      erreur = true;
    }
    if (erreur) {
      // foutmelding weergeven
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // gebruiker
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // de fout wordt weergegeven
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // de fout wordt genoteerd
      erreur = true;
    }
    // wachtwoord
    mdp = edtMdp.getText().toString().trim();
    // terug
    return !erreur;
}

De methode [beginWaiting] (regel 16) is als volgt:


  // het wachten begint
  protected void beginWaiting(int numberOfRunningTasks) {
    // de taken worden voorbereid voor uitvoering
    beginRunningTasks(numberOfRunningTasks);
    // status van knoppen en menu's
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • regel 4: aan de bovenliggende taak wordt aangegeven dat [numberOfRunningTasks]-taken worden gestart;
  • regel 6: alle menuopties worden onzichtbaar gemaakt;
  • regel 7: om vervolgens de optie [Actions/Annuler] zichtbaar te maken;

Een klik op de menuoptie [Annuler] wordt afgehandeld door de methode [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken worden geannuleerd
    cancelRunningTasks();
}
  • regel 8: de bovenliggende klasse wordt gevraagd de asynchrone taken te annuleren;

3.6.6.3. Beheer van de levenscyclus van het fragment

Het fragment heeft de volgende status [ConfigFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // de zichtbaarheid van de twee foutmeldingen
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;

  // getters en setters
...
}
  • wanneer de bovenliggende klasse daarom vraagt, slaat het fragment de zichtbaarheid van zijn twee foutmeldingen op;

De levenscyclus van het fragment is als volgt geïmplementeerd:


// Implementatie van methoden van de bovenliggende klasse -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // fragmentstatus opslaan
    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) {
      // eerste bezoek
      // foutmeldingen worden in de cache opgeslagen
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // zichtbaarheid van foutmeldingen herstellen
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // niet het eerste bezoek - foutmeldingen weergeven
    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();
    // volgende weergave?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }

  // privé-methoden ------------------------------------------------
  private void initMenu(){
    // menustatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • regels 2-9: wanneer de bovenliggende klasse hierom vraagt, slaat het fragment de status van zijn twee foutmeldingen op;
  • regels 11-14: het fragmentnummer is [IMainActivity.VUE_CONFIG];
  • regels 16-19: worden uitgevoerd wanneer het fragment voor de eerste keer wordt gegenereerd (previousState == null) of bij de volgende keren opnieuw wordt gegenereerd (previousState != null). Hier hoeft niets te gebeuren;
  • regels 21-31: worden uitgevoerd wanneer de aan het fragment gekoppelde weergave voor de eerste keer wordt opgebouwd (previousState==null) of bij de volgende keren opnieuw wordt opgebouwd (previousState !=null);
    • regels 24-29: bij het eerste bezoek worden de foutmeldingen verborgen en wordt het menu weergegeven zonder de actie [Annuler] (regels 62-66);
  • regels 33-35: worden uitgevoerd wanneer het fragment wordt bereikt via een bewerking [SUBMIT]. Dit gebeurt hier nooit;
  • regels 37-44: worden uitgevoerd wanneer het fragment wordt bereikt via een bewerking [NAVIGATION] of [RESTORE]. De status van de foutmeldingen wordt hersteld op basis van de vorige status;
  • regels 47-49: worden uitgevoerd wanneer alle voorgaande updates zijn voltooid. Er valt niets meer te doen;
  • regels 51-59: worden uitgevoerd wanneer alle asynchrone taken zijn voltooid;
    • regels 53-54: het menu wordt teruggezet naar de standaardstatus;
    • regels 56-58: als de taken normaal zijn voltooid, gaat men naar de volgende weergave; anders blijft men in dezelfde weergave;

3.6.7. Beheer van de startpagina

3.6.7.1. De weergave

Het startscherm ziet er als volgt uit:

Image

De elementen van de visuele interface zijn als volgt:

Type
Nom
1
Spinner
spinnerMedecins
2
DatePicker
edtJourRv

3.6.7.2. Het fragment

De startpagina wordt beheerd door het volgende fragment [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 {

  // de elementen van de visuele interface
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;

  // lokale gegevens
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;

  // paginavalidatie
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...

  // Implementatie van methoden van de bovenliggende klasse -------------------------------------
...
}
  • regel 26: het fragment is gekoppeld aan het volgende 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>
  • regels 31-34: de elementen van de visuele interface;
  • regel 37: de lijst met artsen;
  • regel 38: een kalender;
  • regel 39: de gegevensbron van de spinner voor artsen;

Een klik op de link [Valider] wordt afgehandeld door de volgende methode [doValider]:


// paginavalidatie
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // de id van de geselecteerde arts wordt genoteerd
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // de dag wordt in de sessie opgeslagen
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // omzetten naar de datumnotatie jjjj-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // begin van de wachttijd – er wordt 1 asynchrone taak gestart
    beginWaiting(1);
    // de agenda van de arts wordt opgevraagd
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // het antwoord wordt verwerkt
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // fout?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // bericht
      showAlert(responseAgendaMedecinJour.getMessages());
      // annulering
      doAnnuler();
      // terug naar UI
      return;
    }
    // de agenda wordt in de sessie geplaatst
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • regel 5: de ID van de geselecteerde arts wordt opgehaald;
  • regels 7-8: de gekozen datum wordt in het Frans weergegeven;
  • regels 10-11: de gekozen datum wordt in het Engelse formaat weergegeven;
  • regel 13: we geven aan de bovenliggende klasse door dat we een asynchrone taak gaan starten en bereiden het wachten voor;
  • regels 15-22: de agenda van de arts wordt opgevraagd;
    • regel 15: de methode [executeInBackground] verwacht twee parameters:
      • regel 15: het uit te voeren en te observeren proces wordt geleverd door de methode [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
      • regels 15-22: de tweede parameter is een instantie van het type [Action1<T>], waarbij T het type is dat door het geobserveerde proces wordt geretourneerd, in dit geval [Response<AgendaMedecinJour>]
    • regel 20: zodra het antwoord wordt ontvangen, wordt dit doorgegeven aan de methode [consumeAgenda] in regel 25;
  • regels 25-37: de agenda van de arts is ontvangen. Deze wordt verwerkt;
  • regels 27-34: eerst wordt gekeken of de server een fout heeft gemeld in het veld [status] van het antwoord;
  • regel 29: als er een fout is, worden de berichten weergegeven die de server in het veld [messages] van het antwoord heeft geplaatst;
  • regel 31: alle taken worden geannuleerd;
  • regel 33: we keren terug naar de gebruikersinterface;
  • regel 36: als er geen fouten zijn opgetreden, wordt de agenda in de sessie geladen;

De methode [beginWaiting] (regel 13) is als volgt:


  // begin van de wachttijd
  protected void beginWaiting(int numberOfRunningTasks) {
    // de taken worden voorbereid voor uitvoering
    beginRunningTasks(numberOfRunningTasks);
    // status van de knoppen en menu's
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • regel 4: de bovenliggende taak wordt geïnformeerd dat [numberOfRunningTasks]-taken worden gestart;
  • regel 6: alle menuopties worden onzichtbaar gemaakt;
  • regel 7: om vervolgens de optie [Actions/Annuler] zichtbaar te maken;

Een klik op de menuoptie [Annuler] wordt afgehandeld door de methode [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken worden geannuleerd
    cancelRunningTasks();
}
  • regel 8: de bovenliggende klasse wordt gevraagd de asynchrone taken te annuleren;

Het klikken op de menuoptie [Retour à la configuration] wordt als volgt afgehandeld:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigeren naar het configuratiescherm
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • regel 4: er wordt naar de configuratieweergave genavigeerd met de actie [NAVIGATION]. Dit betekent dat men de configuratieweergave wil terugvinden in de toestand waarin deze is achtergelaten;

3.6.7.3. Beheer van de levenscyclus van het fragment

Het fragment heeft de volgende status: [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 {

  // status van fragment [Accueil]
  // positie van de geselecteerde arts
  private int selectedMedecinPosition;
  // geselecteerde datum
  private int year;
  private int month;
  private int dayOfMonth;
  // gegevensbron van de artsenkeuzelijst
  private String[] spinnerMedecinsDataSource;

  // constructors
  public AccueilFragmentState() {

  }

  // getters en setters
...
}
  • regel 11: hiermee wordt het geselecteerde element in de lijst met artsen weergegeven;
  • regels 13-15: hiermee kan de gekozen datum in de kalender worden weergegeven;
  • regel 17: hiermee wordt de gegevensbron van de lijst met artsen weergegeven;

De levenscyclus van het fragment is als volgt geïmplementeerd:


// implementatie van methoden van de bovenliggende klasse -------------------------------------
  @Override
  public CoreState saveFragment() {
    // de weergave wordt opgeslagen
    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) {
    // de artsen uit de sessie ophalen
    medecins = session.getMédecins();
    // Eerste bezoek?
    if (previousState == null) {
      // de tabel opbouwen die door de spinner wordt weergegeven
      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 {
      // geen eerste bezoek
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // de kalender
    calendrier = Calendar.getInstance();
  }

  @Override
  protected void initView(CoreState previousState) {
    // de spinner voor artsen wordt gekoppeld aan de bron van de oproepen
    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);
    // minimale datum van de kalender tot vandaag
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // Eerste bezoek?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

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

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // de huidige sessiestatus wordt hersteld
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // selectie van artsen in de spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // kalender
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // wordt aangeroepen nadat alle taken zijn voltooid of geannuleerd
    // menustatus
    initMenu();
    // volgende weergave?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }

  // privé-methoden ------------------------------------------------
  private void initMenu() {
    // menustatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • regels 2-9: wanneer de bovenliggende klasse hierom vraagt, slaat het fragment de status van de volgende elementen op:
    • regel 6: de geselecteerde positie in de lijst met artsen;
    • regels 7-9: de dag van de maand, de maand en het jaar van de in de kalender geselecteerde datum;
    • regel 10: de gegevensbron van de spinner voor artsen;
  • regels 14-17: het fragmentnummer is [IMainActivity.VUE_ACCUEIL];
  • regels 19-39: worden uitgevoerd wanneer het fragment voor de eerste keer wordt gegenereerd (previousState==null) of bij de volgende keren opnieuw wordt gegenereerd (previousState !=null);
    • regels 25-31: bij een eerste bezoek wordt de gegevensbron van de spinner met artsen opgebouwd;
    • regels 33-35: bij volgende bezoeken wordt de gegevensbron van de spinner opgehaald uit de vorige status van het fragment;
  • regels 41-54: worden uitgevoerd wanneer de aan het fragment gekoppelde weergave voor de eerste keer wordt opgebouwd (previousState==null) of bij volgende bezoeken opnieuw wordt opgebouwd (previousState !=null);
    • regels 50-53: bij het eerste bezoek wordt het menu weergegeven zonder de actie [Annuler] (regels 88-92);
    • regels 43-48: bij alle bezoeken, of het nu het eerste bezoek is of niet, wordt de spinner met artsen gekoppeld aan de bron (regels 44-46) en wordt de vroegste datum in de kalender ingesteld op de huidige datum (regel 48);
  • regels 56-60: worden uitgevoerd wanneer men via een bewerking [SUBMIT] bij het fragment komt. Men komt dan vanuit de weergave [CONFIG]. Het menu wordt in de begintoestand teruggezet;
  • regels 62-70: worden uitgevoerd wanneer men via een bewerking [NAVIGATION] of [RESTORE] bij het fragment komt;
    • regel 67: de spinner met artsen wordt teruggezet op de laatst geselecteerde arts;
    • regel 69: de kalender wordt ingesteld op de laatst gekozen datum;
  • regels 72-74: worden uitgevoerd wanneer alle voorgaande updates zijn voltooid. Er hoeft verder niets meer te gebeuren;
  • regels 76-85: worden uitgevoerd wanneer alle asynchrone taken zijn voltooid;
    • regel 80: het menu wordt teruggezet naar de standaardstatus;
    • regels 82-84: als de taken normaal zijn voltooid, gaat men naar de volgende weergave; anders blijft men in dezelfde weergave;

3.6.8. Beheer van de Agenda-weergave

3.6.8.1. De weergave

Het startscherm ziet er als volgt uit:

Image

De elementen van de visuele interface zijn als volgt:

Type
Nom
1
TextView
txtTitre2
2
ListView
lstCreneaux

3.6.8.2. Het fragment

De weergave Agenda wordt beheerd door het volgende fragment [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 {

  // de elementen van de visuele interface
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;

  // agenda weergegeven door het fragment
  private AgendaMedecinJour agenda;
  // informatie over de tijdvakken
  private int firstPosition;
  private int top;
  // afspraak al dan niet verwijderd
  private boolean rdvSupprimé;
  // nummer van het toegevoegde of verwijderde tijdvak
  private int numCréneau;

  // agenda bijgewerkt na toevoeging/verwijdering
  private void updateAgenda() {
  ...
  }

...

  // implementatie van methoden van de bovenliggende klasse ------------------------------------------------------
  ...
}
  • regel 27: het fragment is gekoppeld aan het volgende 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>
  • regels 32-35: de elementen van de visuele interface;
  • regels 37-45: globale gegevens voor de methoden;

3.6.8.2.1. Methode [updateAgenda]

Het (opnieuw) genereren van de lijst met agenda-tijdvakken is op verschillende plaatsen in de code nodig. Dit is ondergebracht in de volgende privé-methode [updateAgenda]:


  // agenda bijwerken na een toevoeging/verwijdering
  private void updateAgenda() {
    // (her)genereren van de tijdvakken in de agenda
    // de agenda wordt in de sessie opgenomen en opgeslagen in een veld van het fragment
    agenda = session.getAgenda();
    // opnieuw genereren van de tijdvakken in ListView
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // we gaan terug naar de juiste plaats in het ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • regel 5: de agenda wordt uit de sessie opgehaald en opgeslagen in het veld [agenda] van het fragment;
  • regels 7-9: de adapter van de component [ListView] wordt gedefinieerd. Deze adapter definieert zowel de gegevensbron van [ListView] als het weergavemodel van elk element daarvan. We zullen deze adapter binnenkort toelichten;
  • regel 11: we keren terug naar de vorige positie van de agenda. We zien namelijk slechts een deel van de tijdvakken van de dag. Als we een afspraak toevoegen of verwijderen in het laatste tijdvak, zal de bovenstaande code de pagina verversen om de nieuwe agenda weer te geven. Door deze verversing komt men dan weer op het eerste tijdvak terecht, wat niet wenselijk is. Regel 5 lost dit probleem op. De beschrijving van deze oplossing is te vinden in URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

De klasse [ListCreneauxAdapter] dient om een regel van de [ListView] te definiëren:

Image

Hierboven is te zien dat de weergave verschilt naargelang het tijdvak al dan niet een afspraak bevat. De code van de klasse [ListCreneauxAdapter] is als volgt:


...

public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {

    // de tabel met tijdvakken
    private CreneauMedecinJour[] creneauxMedecinJour;
    // de uitvoeringscontext
    private Context context;
    // de id van de weergavelay-out van een regel in de lijst met tijdvakken
    private int layoutResourceId;
    // klik-listener
    private AgendaFragment vue;

    // constructor
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // de gegevens worden opgeslagen
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // de tabel met tijdvakken wordt gesorteerd op tijdstip
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }

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

// sorteren van de tabel met tijdvakken
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • regel 3: de klasse [ListCreneauxAdapter] moet een vooraf gedefinieerde adapter uitbreiden voor de [ListView], in dit geval de klasse [ArrayAdapter], die, zoals de naam al aangeeft, de [ListView] voorziet van een array van objecten, in dit geval van het type [CreneauMedecinJour]. Laten we de code van deze entiteit nog eens bekijken:

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // velden
    private Creneau creneau;
    private Rv rv;
...  
}
  • de klasse [CreneauMedecinJour] bevat een tijdslot (regel 5) en een eventuele afspraak (regel 6) of null als er geen afspraak is;

Terug naar de code van de klasse [ListCreneauxAdapter]:

  • regel 15: de constructor ontvangt vier parameters:
    1. de huidige Android-activiteit,
    2. het bestand XML dat de inhoud van elk element van [ListView] definieert,
    3. de tabel met de beschikbare tijdvakken van de arts,
    4. de weergave zelf;
  • regel 24: de tabel met de tijdvakken is gesorteerd in oplopende volgorde van de tijdstippen;

De methode [getView] is verantwoordelijk voor het genereren van de weergave die overeenkomt met een regel van het bestand [ListView]. Deze bestaat uit drie elementen:

 
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

De code van de methode [getView] is als volgt:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // we selecteren het juiste tijdvak
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // de regel wordt aangemaakt
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // het tijdvak
        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()));
        // de klant
        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);
        // de link
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // toevoegen
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // verwijderen
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // luisteraar van de link
        btnValider.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // de gegevens worden doorgegeven aan de agendapagina
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // de regel wordt weergegeven
        return row;
    }
  • regel 2: 'position' is het regelnummer dat we gaan genereren in de tabel [ListView]. Dit is tevens het nummer van het tijdslot in de tabel [creneauxMedecinJour]. De overige twee parameters worden genegeerd;
  • regel 4: we halen het tijdvak op dat moet worden weergegeven in de regel van het bestand [ListView];
  • regel 6: de regel wordt samengesteld op basis van de definitie in XML
 

De code van [creneau_medecin.xml] is als volgt:


<?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>
 
  • regels 8-10: het tijdvak [1] wordt samengesteld;
  • regels 12-20: de klantidentiteit [2] wordt aangemaakt;
  • regel 23: als het tijdslot geen afspraak heeft;
  • regels 25-26: de blauwe link [Ajouter] wordt aangemaakt;
  • regels 29-30: anders wordt de link [Supprimer] in de kleur rood aangemaakt;
  • regels 33-40: ongeacht de aard van de link [Ajouter / Supprimer] is het de methode [doValider] van de weergave die de klik op de link afhandelt. De methode ontvangt twee argumenten:
    1. het nummer van het tijdvak waarop is geklikt,
    2. de tekst van de link waarop is geklikt;
  • regel 42: we geven de regel terug die we zojuist hebben opgebouwd.

Merk op dat het de methode [doValider] van het fragment [AgendaFragment] is die de links verwerkt. Deze is als volgt:


  // klik op een link [Ajouter / Supprimer]
  public void doValider(int numCréneau, String texte) {
    // bewerking bezig?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // de scrollpositie wordt opgeslagen om ernaar terug te keren
    // lees [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // positie van het eerste element: volledig zichtbaar of niet
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // Y-offset van dit element ten opzichte van de bovenkant van ListView
    // meet de hoogte van het eventueel verborgen gedeelte
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // ook het nummer van het aangeklikte vakje wordt genoteerd
    this.numCréneau = numCréneau;
    // afhankelijk van de tekst van de link wordt er iets anders gedaan
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • de methode [doValider] ontvangt twee gegevens:
    • het nummer van het tijdslot waarop is geklikt;
    • de tekst (Toevoegen / Verwijderen) van de link waarop is geklikt;
  • regels 4-7: het klikken op de links [Supprimer / Ajouter] wordt geblokkeerd als er asynchrone taken lopen. Dit is een keuze die het schrijven van de code vergemakkelijkt. Hierover kan worden gediscussieerd;
  • regels 11-15: de informatie (firstPosition, top) van de ListView van de tijdvakken wordt opgeslagen in velden van het fragment, zodat de privémethode [updateAgenda] deze kan regenereren met dezelfde scrollpositie;
  • regel 17: het nummer van het aangeklikte tijdslot wordt genoteerd;
  • regels 19-23: afhankelijk van de tekst van de aangeklikte link wordt er iets toegevoegd of verwijderd;

3.6.8.2.2. Methode [doSupprimer]

De methode [doSupprimer] zorgt ervoor dat de afspraak in het aangeklikte tijdvak wordt verwijderd:


// een afspraak verwijderen
  private void doSupprimer() {
    // wachten tot twee taken zijn voltooid
    beginWaiting(2);
    // de afspraak wordt op de achtergrond verwijderd
    rdvSupprimé = false;
    // ID van de af te spreken afspraak
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // verwijdering via een asynchrone taak
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {

      @Override
      public void call(Response<Rv> responseRv) {
        // verwerking van het resultaat
        consumeRv(responseRv);
      }
    });
  }

  // verwerking van een antwoord
  private void consumeRv(Response<Rv> responseRv) {
    // fout?
    if (responseRv.getStatus() != 0) {
      // bericht
      showAlert(responseRv.getMessages());
      // annulering
      doAnnuler();
      // terug naar UI
      return;
    }
    // er wordt aangegeven dat de afspraak is verwijderd
    rdvSupprimé = true;
    // de meest recente agenda wordt opgevraagd
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {

        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // het antwoord wordt verwerkt
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }

  // een agenda wordt verwerkt
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // fout?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // bericht
      showAlert(responseAgendaMedecinJour.getMessages());
      // annulering
      doAnnuler();
      // terug naar de UI
      return;
    }
    // de agenda wordt in de sessie geplaatst
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // de agenda van de weergave wordt bijgewerkt
    updateAgenda();
  }
  • regel 4: er wordt aan de bovenliggende klasse gemeld dat er twee asynchrone taken worden gestart en er wordt gewacht tot deze twee taken zijn voltooid;
  • regel 8: de ID van de afspraak die moet worden verwijderd, wordt opgehaald. De server heeft deze informatie namelijk nodig;
  • regels 9-18: we vragen om verwijdering van de afspraak via een asynchrone taak;
    • regel 10: de methode [executeInBackground] verwacht twee parameters:
      • regel 10: het uit te voeren en te observeren proces wordt geleverd door de methode [mainActivity.supprimerRv(idRv)];
      • regels 10-17: de tweede parameter is een instantie van het type [Action1<T>], waarbij T het type is dat door het geobserveerde proces wordt geretourneerd, in dit geval [Response<Rv>]
    • regel 15: wanneer het antwoord wordt ontvangen, wordt dit doorgegeven aan de methode [consumeRv] in regel 21;
  • regels 21-44: het antwoord van de asynchrone taak is ontvangen. Dit wordt verwerkt;
  • regels 23-30: eerst wordt gekeken of de server een fout heeft gemeld in het veld [status] van het antwoord;
    • regel 25: als er een fout is, worden de berichten weergegeven die de server in het veld [messages] van het antwoord heeft geplaatst;
    • regel 27: alle taken worden geannuleerd;
    • regel 29: we keren terug naar de gebruikersinterface;
  • regel 32: als er geen fout is opgetreden, wordt genoteerd dat de afspraak is verwijderd;
  • regels 34-43: in plaats van de afspraak simpelweg te verwijderen uit de agenda die momenteel door het fragment wordt weergegeven, wordt de nieuwe agenda van de arts opgevraagd. De applicatie is namelijk voor meerdere gebruikers bedoeld en andere gebruikers kunnen de agenda van de arts ook hebben gewijzigd. Het is dus beter om de meest recente versie te hebben;
  • regels 34-43, 47-61: we herhalen wat er in het fragment [AccueilFragment] is gedaan, maar deze keer met informatie uit de sessie;

De methode [beginWaiting] (regel 4) is als volgt:


  // begin van de wachttijd
  protected void beginWaiting(int numberOfRunningTasks) {
    // de taken worden voorbereid voor uitvoering
    beginRunningTasks(numberOfRunningTasks);
    // status van de knoppen en menu's
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • regel 4: we geven aan de bovenliggende taak door dat we de taak [numberOfRunningTasks] gaan starten;
  • regel 6: alle menuopties worden onzichtbaar gemaakt;
  • regel 7: om vervolgens de optie [Actions/Annuler] zichtbaar te maken;

3.6.8.2.3. Methode [doAnnuler]

Een klik op de menuoptie [Annuler] wordt afgehandeld door de methode [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken worden geannuleerd
    cancelRunningTasks();
}
  • regel 7: de bovenliggende klasse wordt gevraagd de asynchrone taken te annuleren;

3.6.8.2.4. Menuoptie [Retour à la configuration]

Een klik op de menuoptie [Retour à la configuration] wordt als volgt afgehandeld:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigeren naar de configuratieweergave
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • regel 4: we gaan naar het configuratiescherm met de actie [NAVIGATION]. Dit betekent dat we het configuratiescherm willen terugvinden in de staat waarin we het hebben achtergelaten;

3.6.8.2.5. Menuoptie [Retour à l'accueil]

Een klik op de menuoptie [Retour à l'accueil] wordt op dezelfde manier afgehandeld:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // navigeren naar de startpagina
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. Beheer van de levenscyclus van het fragment

Het fragment heeft de volgende status: [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 {

  // titel van de weergave
  private String titre;
  // ListView
  private int firstPosition;
  private int top;

  // constructors
  public AgendaFragmentState() {

  }

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

  // getters en setters
...
}
  • regel 10: de titel die bovenaan de weergave wordt weergegeven;
  • regels 12-13: hiermee wordt de scrolling van de ListView van de afspraken van de arts weergegeven;

De levenscyclus van het fragment is als volgt geïmplementeerd:


// implementatie van methoden van de bovenliggende klasse ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // status opslaan
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // de scrollpositie noteren om ernaar terug te keren
    // lezen [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // positie van het eerste element: volledig zichtbaar of niet
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // Y-offset van dit element ten opzichte van de bovenkant van het ListView
    // meet de hoogte van het eventueel verborgen gedeelte
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // dit alles wordt opgeslagen
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }

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

  @Override
  protected void initFragment(CoreState previousState) {
    // Eerste bezoek?
    if (previousState != null) {
      // niet het eerste bezoek
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // en de gegevens van ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // we halen de agenda op
    agenda = session.getAgenda();
    // we genereren de paginatitel
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // menustatus
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // de paginatitel wordt opnieuw gegenereerd
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }

  @Override
  protected void notifyEndOfUpdates() {
    // de lijst met tijdvakken opnieuw genereren
    updateAgenda();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menustatus
    initMenu();
    // Als de afspraak is geannuleerd en verwijderd, moet de lokale agenda worden bijgewerkt
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // de afspraak wordt verwijderd uit de lokale agenda (de algemene agenda was niet beschikbaar)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // de visuele interface wordt bijgewerkt
      updateAgenda();
    }
  }


  // privé-methoden ------------------------------------------------
  private void initMenu() {
    // menustatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • regels 2-19: wanneer de bovenliggende klasse hierom vraagt, slaat het fragment de status van de volgende elementen op:
    • regel 6: de titel die bovenaan de weergave wordt weergegeven;
    • regels 7-17: de informatie (top, firstPosition) waarmee de scrolling van de ListView kan worden gereconstrueerd;
  • regels 21-24: het fragmentnummer is [IMainActivity.VUE_AGENDA];
  • regels 26-35: worden uitgevoerd wanneer het fragment voor de eerste keer wordt gegenereerd (previousState == null) of bij de volgende keren opnieuw wordt gegenereerd (previousState != null);
    • regels 30-34: als dit niet het eerste bezoek aan het fragment is, worden de gegevens (top, firstPosition) opgehaald waarmee de scrolling van de ListView kan worden gereconstrueerd;
  • regels 38-40: worden uitgevoerd wanneer de aan het fragment gekoppelde weergave voor de eerste keer wordt opgebouwd (previousState == null) of bij de volgende keren opnieuw wordt opgebouwd (previousState != null). Hier hoeft niets te gebeuren, omdat de ListView van de slots wordt gegenereerd door de privémethode [updateAgenda] (regels 61-65);
  • regels 42-52: uitgevoerd wanneer men via een bewerking [SUBMIT] bij het fragment komt. Men komt dan vanuit de weergave [ACCUEIL];
    • regel 45: de agenda wordt opgehaald die door [AccueilFragment] in de sessie is geladen;
    • regels 47-49: de titel van de weergave wordt gegenereerd;
    • de ListView van de tijdvakken wordt gegenereerd door de privémethode [updateAgenda] (regels 61-65);
  • regels 54-59: worden uitgevoerd wanneer het fragment wordt bereikt via een bewerking [NAVIGATION] of [RESTORE];
    • regels 57-58: de titel van de weergave wordt opnieuw gegenereerd;
    • de ListView van de tijdvakken wordt gegenereerd door de privémethode [updateAgenda] (regels 61-65);
  • regels 72-74: worden uitgevoerd wanneer alle voorgaande updates zijn voltooid. Het ListView van de tijdvakken wordt bijgewerkt, omdat deze update nodig is, ongeacht de manier waarop men bij het fragment terechtkomt;
  • regels 67-77: worden uitgevoerd wanneer alle asynchrone taken zijn voltooid;
    • regel 70: het menu wordt teruggezet naar de standaardstatus (regels 82-86);
    • regel 72: er waren twee asynchrone taken. We controleren of de eerste (het verwijderen van de afspraak) is geslaagd, ondanks een annulering;
    • regel 74: zo ja, dan wordt de afspraak uit de lokale agenda verwijderd
    • regel 75: en wordt de weergave ervan bijgewerkt;

3.6.9. Beheer van het scherm voor het toevoegen van een afspraak

3.6.9.1. Het scherm

Het scherm voor het toevoegen van een afspraak ziet er als volgt uit:

Image

De elementen van de visuele interface zijn als volgt:

Type
Nom
1
TextView
txtTitre2
2
Spinner
spinnerClients

3.6.9.2. Het fragment

Het scherm voor het toevoegen van een afspraak wordt beheerd door het volgende fragment [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 {

  // de elementen van de visuele interface
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;

  // de klanten
  private List<Client> clients;

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

  // paginavalidatie
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...

  // Implementatie van methoden van de bovenliggende klasse ----------------------------------
...
}
  • regel 26: het fragment is gekoppeld aan het volgende 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>
  • regels 30-33: de elementen van de visuele interface;
  • regel 36: de lijst met klanten;
  • regel 43: de gegevensbron van de klantspinner;

Een klik op de link [Valider] wordt afgehandeld door de volgende methode [doValider]:


  // de klanten
  private List<Client> clients;

  // lokale gegevens
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// paginavalidatie
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // de geselecteerde klant wordt opgehaald
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // begin van het wachten op 2 asynchrone taken
    beginWaiting(2);
    // RV wordt toegevoegd
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {

        @Override
        public void call(Response<Rv> responseRv) {
          // het antwoord wordt verwerkt
          consumeRv(responseRv);
        }
      });
  }

  // een Response<Rv>-object wordt verwerkt
  void consumeRv(Response<Rv> responseRv) {
    // fout?
    if (responseRv.getStatus() != 0) {
      // bericht
      showAlert(responseRv.getMessages());
      // annulering
      doAnnuler();
      // terug naar de UI
      return;
    }
    // er wordt genoteerd dat de afspraak is toegevoegd
    rdvAjouté = true;
    // de afspraak wordt opgeslagen
    this.rv = responseRv.getBody();
    // de nieuwe agenda wordt opgevraagd
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // het antwoord wordt verwerkt
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  // verwerking van een Response<AgendaMedecinJour>-object
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // fout?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // bericht
      showAlert(responseAgendaMedecinJour.getMessages());
      // annulering
      doAnnuler();
      // terug naar de UI
      return;
    }
    // de agenda wordt in de sessie geplaatst
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • regel 13: wanneer de methode [doValider] begint, zijn de velden 2, 5, 6 en 9 geïnitialiseerd tijdens de levenscyclus van het fragment. We zullen zien hoe;
  • regel 15: we halen de entiteit [Client] op die overeenkomt met het element dat in de spinner van de klanten is geselecteerd;
  • regel 17: we geven aan de bovenliggende klasse door dat we twee asynchrone taken gaan starten en bereiden het wachten voor;
  • regel 19: in eerste instantie is de afspraak nog niet toegevoegd aan de agenda van de arts;
  • regels 20-30: er wordt aan de server gevraagd om een afspraak toe te voegen;
    • regel 20: de methode [executeInBackground] verwacht twee parameters:
      • regel 20: het uit te voeren en te observeren proces wordt geleverd door de methode [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
      • regels 22-29: de tweede parameter is een instantie van het type [Action1<T>], waarbij T het type is dat door het geobserveerde proces wordt geretourneerd, in dit geval [Response<Rv>]
    • regel 27: wanneer het antwoord wordt ontvangen, wordt dit doorgegeven aan de methode [consumeRV] in regel 33;
  • regels 33-56: het antwoord van de server is ontvangen. Dit wordt verwerkt;
    • regels 35-42: eerst wordt gekeken of de server een fout heeft gemeld in het veld [status] van het antwoord;
    • regel 37: als er een fout is, worden de berichten weergegeven die de server in het veld [messages] van het antwoord heeft geplaatst;
    • regel 39: alle taken worden geannuleerd;
    • regel 41 : we keren terug naar de gebruikersinterface;
    • regel 44: als er geen fout is opgetreden, wordt genoteerd dat de afspraak is toegevoegd;
    • regel 46: de toegevoegde afspraak wordt opgeslagen in een veld van het fragment;
    • regels 47-55: net zoals bij het verwijderen van een afspraak, wordt na het toevoegen van de afspraak de meest recente agenda van de arts opgevraagd bij de server;
  • regels 47-56, 59-71: dit is code die we al meerdere keren zijn tegengekomen;

De methode [beginWaiting] (regel 17) is als volgt:


  // begin van de wachttijd
  protected void beginWaiting(int numberOfRunningTasks) {
    // de taken worden voorbereid voor uitvoering
    beginRunningTasks(numberOfRunningTasks);
    // status van de knoppen en menu's
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • regel 4: we geven aan de bovenliggende taak door dat we de taak [numberOfRunningTasks] gaan starten;
  • regel 6: alle menuopties worden onzichtbaar gemaakt;
  • regel 7: om vervolgens de optie [Actions/Annuler] zichtbaar te maken;

Een klik op de menuoptie [Annuler] wordt afgehandeld door de methode [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken worden geannuleerd
    cancelRunningTasks();
}
  • regel 7: de bovenliggende klasse wordt gevraagd de asynchrone taken te annuleren;

Terugnavigatie wordt verzorgd door de volgende drie methoden:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // navigeren naar de configuratieweergave
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // navigeren naar de configuratieweergave
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
    // navigeren naar de agendaweergave
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. Beheer van de levenscyclus van het fragment

Het fragment heeft de volgende status: [AjoutRvFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

// status van het fragment AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {

  // positie van de geselecteerde klant
  private int selectedClientPosition;
  // titel van de weergave
  private String titre;
  // gegevensbron van de klantspinner
  private String[] spinnerClientsDataSource;

  // getters en setters
...
}

De levenscyclus van het fragment is als volgt geïmplementeerd:


// implementatie van methoden van de bovenliggende klasse ----------------------------------
  @Override
  public CoreState saveFragment() {
    // weergave opslaan
    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) {
    // klanten uit de sessie ophalen
    clients = session.getClients();
    // Eerste bezoek?
    if (previousState == null) {
      // de tabel opbouwen die door de spinner wordt weergegeven
      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 {
      // geen eerste bezoek
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
    // de spinner koppelen aan de gegevensbron
    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);
    // Eerste bezoek?
    if (previousState == null) {
      // menu
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // het nummer van het te reserveren tijdvak wordt tijdens de sessie opgehaald
    int position = session.getPosition();
    // de agenda van de arts wordt opgehaald in de sessie
    AgendaMedecinJour agenda = session.getAgenda();
    // de arts en het tijdvak waarvoor een afspraak wordt ingepland, worden opgehaald
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // de tweede titel van de pagina wordt samengesteld
    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()));
    // klant selecteren
    spinnerClients.setSelection(0);
    // menu
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Terug naar vorige status
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // titel
    txtTitre2.setText(state.getTitre());
    // lader
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menustatus
    initMenu();
    // volgende weergave?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // er is een annulering geweest - afspraak al toegevoegd?
    if (rdvAjouté) {
      // de lokale agenda wordt aangepast (de algemene agenda is niet ontvangen)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // de agenda wordt weergegeven
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }

  // privé-methoden -------------------
  private void initMenu() {
    // menustatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  • regels 2-10: wanneer de bovenliggende klasse hierom vraagt, slaat het fragment de status van de volgende elementen op:
    • regel 6: de titel bovenaan de weergave;
    • regel 7: de positie van het geselecteerde element in de spinner van de klanten;
    • regel 8: de gegevensbron van de klantenselector;
  • regels 12-15: het fragmentnummer is [IMainActivity.VUE_AJOUT_RV];
  • regels 17-35: worden uitgevoerd wanneer het fragment voor de eerste keer wordt gegenereerd (previousState==null) of bij volgende keren opnieuw wordt gegenereerd (previousState !=null);
    • regel 20: de lijst met klanten uit de sessie wordt opgehaald om deze in een veld van het fragment te plaatsen;
    • regels 22-30: bij een eerste bezoek wordt de gegevensbron voor de klantspinner opgebouwd;
    • regels 32-33: bij volgende bezoeken wordt de gegevensbron van de klantspinner opgehaald uit de vorige status van het fragment;
  • regels 37-49: worden uitgevoerd wanneer de aan het fragment gekoppelde weergave voor de eerste keer wordt opgebouwd (previousState==null) of bij volgende keren opnieuw wordt opgebouwd (previousState !=null);
    • regels 40-43: in alle gevallen wordt de klantspinner gekoppeld aan de bijbehorende gegevensbron;
    • regels 45-48: bij het eerste bezoek wordt het menu weergegeven zonder de actie [Annuler] (regels 107-111);
  • regels 51-70: worden uitgevoerd wanneer men via een bewerking [SUBMIT] bij het fragment komt. Men komt dan vanuit de weergave [AGENDA];
    • regel 54: het nummer van het tijdslot wordt opgehaald waarin een afspraak wordt ingepland;
    • regels 56-59: de entiteiten [Medecin] en [Creneau] worden opgehaald die nodig zijn voor het toevoegen van deze afspraak en deze worden in velden van het fragment geplaatst;
    • regels 61-65: met deze informatie kunnen we de titel van de weergave samenstellen;
    • regel 67: de spinner voor klanten wordt op het eerste element gezet;
    • regel 69: het menu wordt in de begintoestand gezet (zonder de optie [Annuler]);
  • regels 72-80: worden uitgevoerd wanneer het fragment wordt bereikt via een bewerking [NAVIGATION] of [RESTORE];
    • regel 77: de titel van de weergave wordt opnieuw gegenereerd;
    • regel 79: de spinner met klanten wordt opnieuw ingesteld op de laatst geselecteerde klant;
  • regels 82-84: worden uitgevoerd wanneer alle voorgaande updates zijn voltooid. Hier hoeft verder niets meer te gebeuren;
  • regels 86-104: worden uitgevoerd wanneer alle asynchrone taken zijn voltooid;
    • regel 89: het menu wordt teruggezet naar de standaardstatus;
    • regels 91-94: als de taken normaal zijn voltooid, keert men terug naar de weergave [AGENDA] via een [SUBMIT] (hier had het ook een actie van het type NAVIGATION kunnen zijn);
    • regels 96-103: als de taken zijn afgebroken, wordt toch gecontroleerd of de afspraak is toegevoegd (dit zou betekenen dat het ophalen van de nieuwe agenda is mislukt);
    • regels 98-99: als de afspraak is toegevoegd;
      • regels 98-99: de door de server teruggestuurde afspraak wordt toegevoegd aan de huidige agenda, die van de lopende sessie;
      • regel 101: we keren terug naar de weergave [AGENDA] via een [SUBMIT] (hier had het ook een actie van het type NAVIGATION kunnen zijn);

3.7. Exécution

Voer de volgende tests uit:

  • gebruik de applicatie onder normale omstandigheden en controleer of deze werkt;
  • draai het apparaat voor elk van de weergaven en controleer of ze allemaal correct worden hersteld;
  • stel een wachttijd van enkele seconden in in [IMainActivity];
  • annuleer vervolgens de taken en controleer of het resultaat overeenkomt met de verwachting;
  • draai het apparaat tijdens de wachttijden en controleer of de taken daadwerkelijk worden geannuleerd en of er geen crash optreedt;
  • de volgorde van de fragmenten in [IMainActivity] wijzigen en controleren of de applicatie blijft functioneren;