Skip to content

3. Fallstudie – Terminverwaltung

3.1. Das Projekt

Im Dokument [Tutoriel AngularJS / Spring 4] wurde eine Client-Server-Anwendung zur Verwaltung von Arztterminen entwickelt. Im Folgenden werden wir auf dieses Dokument unter der Bezeichnung [rdvmedecins-angular] verweisen. Die Anwendung verfügte über zwei Client-Typen:

  • einen Client HTML / CSS / JS;
  • einen Android-Client;

Der Android-Client wurde automatisch aus der Client-Version HTML mit dem Tool [Cordova] generiert. Das Ziel dieses Projekts ist es, diesen Android-Client manuell unter Verwendung der in den vorangegangenen Kapiteln erworbenen Kenntnisse neu zu erstellen.

Es ist ein wichtiger Unterschied zwischen den beiden Lösungen zu beachten:

  • Die von uns zu erstellende Lösung wird nur auf Android-Tablets nutzbar sein;
  • in der Version [rdvmedecins-angular] ist der mobile Web-Client (HTML / CSS / JS) auf jeder Plattform (Android, IoS, Windows) einsetzbar;

3.2. Die Ansichten des Android-Clients

Es gibt vier Ansichten.

Konfigurationsansicht

Image

Ansicht zur Auswahl des Arztes und des Termins

Image

Ansicht zur Auswahl des Terminzeitfensters

Image

Ansicht zur Auswahl des Kunden für den Termin

Image

3.3. Die Projektarchitektur

Es wird eine Client-Server-Architektur geben, die der im Beispiel [Exemple-15] (siehe Abschnitt 1.16) dieses Dokuments ähnelt:

Image

Der asynchrone Datenaustausch zwischen Client und Server wird mit der Bibliothek RxAndroid verwaltet.

3.4. Die Datenbank

Sie spielt in diesem Dokument keine wesentliche Rolle. Wir führen sie hier nur zur Information auf. Sie wird als [dbrdvmedecins] bezeichnet. Es handelt sich um eine Datenbank vom Typ MySQL5 mit vier Tabellen:

  

3.4.1. Die Tabelle [MEDECINS]

Sie enthält Informationen zu den Ärzten, die von der Anwendung [RdvMedecins] verwaltet werden.

  • ID: Identifikationsnummer des Arztes – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Arztes
  • PRENOM: sein Vorname
  • TITRE: sein/ihr Titel (Fräulein, Frau, Herr)

3.4.2. Die Tabelle [CLIENTS]

Die Patienten der verschiedenen Ärzte sind in der Tabelle [CLIENTS] gespeichert:

  • ID: Nummer zur Identifizierung des Kunden – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Kunden
  • PRENOM: sein Vorname
  • TITRE: Anrede (Fräulein, Frau, Herr)

3.4.3. Die Tabelle [CRENEAUX]

Sie listet die Zeitfenster auf, in denen RV möglich sind:

  • ID: Nummer zur Identifizierung des Zeitfensters – Primärschlüssel der Tabelle (Zeile 8)
  • VERSION: Nummer zur Identifizierung der Version der Zeile in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • ID_MEDECIN: Nummer zur Identifizierung des Arztes, zu dem dieses Zeitfenster gehört – Fremdschlüssel auf die Spalte MEDECINS (ID).
  • HDEBUT: Startzeit des Zeitfensters
  • MDEBUT: Minuten des Zeitfensterstarts
  • HFIN: Endzeit des Zeitfensters
  • MFIN: Minuten des Zeitfensterschlusses

Die zweite Zeile der Tabelle [CRENEAUX] (siehe [1] oben) gibt beispielsweise an, dass der Termin Nr. 2 um 8:20 Uhr beginnt und um 8:40 Uhr endet und der Ärztin Nr. 1 (Frau Marie PELISSIER).

3.4.4. Die Tabelle [RV]

Sie listet die für jeden Arzt vergebenen RV auf:

  • ID: Nummer, die den RV eindeutig identifiziert – Primärschlüssel
  • JOUR: Tag des RV
  • ID_CRENEAU: Zeitfenster des RV – Fremdschlüssel auf das Feld [ID] der Tabelle [CRENEAUX] – legt sowohl das Zeitfenster als auch den betreffenden Arzt fest.
  • ID_CLIENT: Kundennummer, für die die Reservierung vorgenommen wird – Fremdschlüssel auf das Feld [ID] der Tabelle [CLIENTS]

Diese Tabelle verfügt über eine Eindeutigkeits für die Werte der verknüpften Spalten (JOUR, ID_CRENEAU):

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

Wenn eine Zeile der Tabelle [RV] den Wert (JOUR1, ID_CRENEAU1) für die Spalten (JOUR, ID_CRENEAU) aufweist, darf dieser Wert an keiner anderen Stelle vorkommen. Andernfalls würde dies bedeuten, dass zwei RV gleichzeitig für denselben Arzt erfasst wurden. Aus Sicht der Java-Programmierung löst der Treiber JDBC der Datenbank in diesem Fall einen SQLException aus.

Die Zeile mit dem Wert 3 für id (siehe [1] oben) bedeutet, dass am 23.08.2006 ein RV für den Terminblock Nr. 20 und den Kunden Nr. 4 gebucht wurde. Aus der Tabelle [CRENEAUX] geht hervor, dass der Termin Nr. 20 dem Zeitfenster 16:20 – 16:40 Uhr entspricht und der Ärztin Nr. 1 (Frau Marie PELISSIER) zugeordnet ist. Aus der Tabelle [CLIENTS] geht hervor, dass es sich bei Patient Nr. 4 um Frau Brigitte BISTROU handelt.

3.4.5. Erstellung der Datenbank

Um die Tabellen anzulegen und zu füllen, kann das Skript [dbrdvmedecins.sql] verwendet werden, das sich im Beispielarchiv |ICI| befindet.

  

Mit [WampServer] (siehe Abschnitt 6.15) kann wie folgt vorgegangen werden:

 
  • In [1] klickt man auf das Symbol von [WampServer] und wählt die Option [PhpMyAdmin] [2],
  • in [3] wählt man im sich öffnenden Fenster den Link [Bases de données] aus,
 
  • in [4-6], importieren Sie eine Datei SQL,
  • in [7] wird das Skript SQL ausgewählt und in [8] ausgeführt,
  • in [9] wurden die Datenbanktabellen angelegt. Man folgt einem der Links,
 
  • in [10] den Inhalt der Tabelle.

Im weiteren Verlauf werden wir nicht mehr auf diese Datenbank zurückkommen, aber der Leser ist eingeladen, deren Entwicklung im Laufe der Tests zu verfolgen, insbesondere wenn die Anwendung nicht funktioniert.

3.5. Der Webserver / jSON

Image

Wir befassen uns hier mit dem Server [1]. Wir werden ihn nicht näher erläutern. Er wurde im Dokument [Spring MVC et Thymeleaf par l'exemple] ausführlich beschrieben. Interessierte Leser können dort nachschlagen. Er wurde wie der Server aus Beispiel 15 entwickelt. Sein Quellcode ist in den Beispielen enthalten. Wir werden hier dessen Binärdatei verwenden:

  
  • [rdvmedecins-server-all-1.0.jar] ist die Binärdatei des Servers;

3.5.1. Implementierung

Wechseln Sie in einem Befehlsfenster in den Ordner, der die Binärdatei des Servers enthält:


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

Um den Server zu starten, geben Sie dann den folgenden Befehl ein (SGBD und MySQL müssen bereits gestartet sein):


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

Der Server gibt zahlreiche Protokolleinträge aus. Wir haben oben nur diejenigen aufgeführt, die zum Verständnis notwendig sind:

  • Zeilen 14–18: Ein eingebetteter Tomcat-Server wird auf Port 8080 des Rechners gestartet. Dieser Server führt die Webanwendung zur Terminverwaltung aus. Diese Anwendung ist eigentlich ein Webdienst / jSON: Sie wird über URL abgefragt und antwortet mit der Übermittlung einer Zeichenkette jSON;
  • Zeile 24: Der Webdienst ist mit dem Framework [Spring Security] gesichert. Der Zugriff auf die URL des Webdienstes erfolgt nach Authentifizierung;
  • Zeilen 29–44: die vom Webdienst bereitgestellten URL;

Wir werden diese näher erläutern.

3.5.2. Sicherheit des Webdienstes

Die vom Webdienst bereitgestellten URL sind gesichert. Der Server erwartet in der Anfrage HTTP des Clients den folgenden Header:

Authorization: Basic code

Der erwartete Code ist die Base64-Kodierung [http://fr.wikipedia.org/wiki/Base64] der Zeichenfolge „Benutzername:Passwort“. Der Webdienst akzeptiert in seinem Ausgangszustand nur einen Benutzer „admin“ mit dem Passwort „admin“. Der oben genannte Header lautet für diesen speziellen Benutzer wie folgt:

Authorization: Basic YWRtaW46YWRtaW4=

Um diesen Header HTTP senden zu können, verwenden wir den Client HTTP [Advanced Rest Client], bei dem es sich um ein Plugin für den Chrome-Browser handelt (siehe Abschnitt 6.13). Wir werden die verschiedenen vom Webdienst bereitgestellten URL manuell testen, um zu verstehen:

  • welche Parameter der URL erwartet;
  • welche Art von Antwort es genau liefert;

3.5.3. Liste der Ärzte

Mit dem URL [/getAllMedecins] lässt sich die Liste der Ärzte abrufen:

  • in [1], die abgefragte URL;
  • in [2] die für diese Abfrage verwendete Methode HTTP;
  • in [3] der Sicherheitsheader HTTP des Benutzers (admin, admin);
  • in [4] wird die Anfrage HTTP gesendet;

Die Antwort des Servers lautet wie folgt:

  • in [5] die aufbereitete Antwort jSON des Servers;
  • in [6], dieselbe Antwort im Rohformat;

Die Form [5] ermöglicht einen besseren Überblick über die Struktur der Antwort. Alle Antworten des Webdienstes sind Instanzen der folgenden Klasse [Response]:


package rdvmedecins.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status des Vorgangs
    private int status;
    // etwaige Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

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

    // Getter und Setter
...
}
  • Zeile 9: Der Status der Antwort. Der Wert 0 bedeutet, dass kein Fehler aufgetreten ist, andernfalls liegt ein Fehler vor;
  • Zeile 11: eine Liste von Fehlermeldungen, falls ein Fehler aufgetreten ist;
  • Zeile 13: die vom Client tatsächlich erwartete Antwort;

Die Antwort auf URL [/getAllMedecins] ist die Zeichenkette jSON eines Objekts vom Typ [Response<List<Medecin>>]. Die Klasse [Medecin] sieht wie folgt aus:


package rdvmedecins.android.dao.entities;

public class Medecin extends Personne {

    // Standardkonstruktor
    public Medecin() {
    }

    // Konstruktor mit Parametern
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}

In Zeile 3 erweitert die Klasse [Medecin] die folgende Klasse [Personne]:


package rdvmedecins.android.dao.entities;

public class Personne extends AbstractEntity {
    // Attribute einer Person
    private String titre;
    private String nom;
    private String prenom;

    // Standardkonstruktor
    public Personne() {
    }

    // Konstruktor mit Parametern
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

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

    // Getter und Setter
    ...
}

In Zeile 3 erweitert die Klasse [Personne] die folgende 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;
    }

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

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

    // Getter und Setter
    ...
}

Letztendlich sieht die Struktur eines Objekts [Medecin] wie folgt aus:


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

und die von [Response<List<Medecin>>] wie folgt:

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

Im weiteren Verlauf werden wir diese verkürzten Definitionen verwenden, um die Antwort des Servers zu beschreiben. Außerdem werden wir für eine gewisse Zeit keine Screenshots mehr zeigen. Es reicht aus, das gerade Gesehene zu wiederholen. Wir werden zu den Screenshots zurückkehren, wenn eine Abfrage nach POST erforderlich ist. Wir werden außerdem ein Ausführungsbeispiel in folgender Form vorstellen:

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

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

Beispiel:

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. Liste der Sprechzeiten eines Arztes

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]: Kennung des Arztes, dessen Sprechzeiten abgefragt werden sollen;
  • [hdebut]: Beginn der Sprechstunde;
  • [mdebut]: Startminute der Sprechstunde;
  • [hfin]: Endzeit der Sprechstunde;
  • [mfin]: Ende der Sprechstunde in Minuten;

Für einen Termin zwischen 10:20 Uhr und 10:40 Uhr ergibt sich [hdebut, mdebut, hfin, mfin] = [10, 20, 10, 40].

Beispiel:

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. Liste der Arzttermine

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]: Kennung des Arztes, dessen Termine abgefragt werden sollen;
  • URL [jour]: Tag der Termine im Format „JJJJ-MM-TT“;
  • Antwort [jour]: dasselbe, jedoch im Java-Datumsformat;
  • [client]: der Kunde des Termins. Seine Struktur wurde zuvor beschrieben;
  • [idClient]: die ID des Kunden;
  • [creneau]: der Terminzeitraum. Seine Struktur wurde zuvor beschrieben;
  • [idCreneau]: die ID des Zeitfensters;

Beispiel:

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. Der Terminkalender eines Arztes

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]: Kennung des Arztes, dessen Termine abgefragt werden sollen;
  • URL [jour]: Tag der Termine im Format „JJJJ-MM-TT“;
  • [agenda]: Terminkalender des Arztes;
  • [medecin]: der betreffende Arzt. Seine Struktur wurde zuvor definiert;
  • Antwort [jour]: der Tag im Terminkalender im Format eines Java-Datums;
  • [creneauxMedecinJour]: ein Array von Elementen vom Typ [CreneauMedecinJour];
  • [creneau]: ein Terminfenster. Seine Struktur wurde zuvor beschrieben;
  • [rv]: ein Termin. Seine Struktur wurde zuvor beschrieben;

Beispiel:

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

Es wurde der Fall hervorgehoben, in dem ein Termin in diesem Zeitfenster vorliegt, sowie der Fall, in dem dies nicht der Fall ist.

3.5.8. Einen Arzt anhand seiner Kennung suchen

URL
/getMedecinById/{idMedecin}
Réponse

Response<Medecin> :[int status ; List<String> messages ; Medecin medecin]
  • [idMedecin]: die Identifikationsnummer des Arztes;

Beispiel 1:

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

Beispiel 2:

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

3.5.9. Einen Kunden anhand seiner ID abrufen

URL
/getClientById/{idClient}
Réponse

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

Beispiel 1:

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

Beispiel 2:

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

3.5.10. Einen Termin anhand seiner ID abrufen

URL
/getCreneauById/{idCreneau}
Réponse

Response<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [idCreneau]: die ID des Zeitfensters;

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

Es ist zu beachten, dass in der Antwort nicht der Arzt angegeben ist, dem der Termin gehört, sondern nur dessen Kennung.

Beispiel 2:

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

3.5.11. Termin über die ID vereinbaren

URL
/getRvById/{idRv}
Réponse

Response<Rv> :[int status ; List<String> messages ; Rv rv]
  • [idRv]: die Termin-ID;

Beispiel 1:

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

Es ist zu beachten, dass in der Antwort weder der Kunde noch der Terminzeitraum angegeben sind, sondern lediglich deren Identifikationsnummern.

Beispiel 2:

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

3.5.12. Termin hinzufügen

Mit dem URL [/ajouterRv] kann ein Termin hinzugefügt werden. Die für diese Hinzufügung erforderlichen Informationen (Tag, Zeitfenster und Kunde) werden über eine Anfrage HTTP POST übermittelt. Wir zeigen, wie diese Anfrage mit dem Tool [Advanced Rest Client] durchgeführt wird.

Image

  • in [1] wird die abgefragte Tabelle URL;
  • in [2] wird sie von einem POST abgefragt;
  • in [3-4] wird dem Server mitgeteilt, dass die an ihn gesendeten Werte in Form einer Zeichenkette jSON übermittelt werden;
  • in [4] der Header HTTP für die Authentifizierung;
  • in [5] die von POST übermittelten Informationen. Es handelt sich um eine Zeichenkette jSON, die Folgendes enthält:
    • [jour]: den Tag des Termins im Format „JJJJ-MM-TT“,
    • [idClient]: die ID des Kunden, für den der Termin vereinbart wurde,
    • [idCreneau]: die Kennung des Terminzeitfensters. Da ein Terminzeitfenster zu einem bestimmten Arzt gehört, wird damit auch der Arzt bezeichnet;
  • mit [6] wird die Anfrage gesendet;

Die gesendete Zeichenfolge jSON ist die des folgenden Objekts vom Typ [PostAjouterRv]:


public class PostAjouterRv {

  // Beitragsdaten
  private String jour;
  private long idClient;
  private long idCreneau;

  // Konstruktoren
  public PostAjouterRv() {

  }

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

  // Getter und Setter
  ...
}

Die Antwort des Servers ist vom Typ [Response<Rv>] [int status; List<String> messages; Rv rv], wobei [rv] der hinzugefügte Termin ist.

Die Antwort des Servers auf die oben genannte Anfrage lautet wie folgt:

 

Es ist zu beachten, dass oben bestimmte Informationen nicht in [idClient, idCreneau] enthalten sind, sondern in den Feldern [client] und [creneau] zu finden sind. Die wichtige Information ist die ID des hinzugefügten Termins (209). Der Webdienst hätte sich darauf beschränken können, nur diese eine Information zurückzugeben.

3.5.13. Einen Termin löschen

Dieser Vorgang erfolgt ebenfalls über einen POST:

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

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

Der gebuchte Wert ist die Zeichenfolge jSON eines Objekts vom Typ [PostSupprimerRv] wie folgt:


public class PostSupprimerRv {

  // Post-Daten
  private long idRv;

  // Konstruktoren
  public PostSupprimerRv() {

  }

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

  // Getter und Setter
  ...
}
  • Zeile 4: [idRv] ist die Kennung des zu löschenden Termins.

Beispiel 1:

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

Der Termin mit der Nummer 209 wurde tatsächlich gelöscht, da [status=0].

Beispiel 2:

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

3.6. Der Android-Client

Image

Nachdem der Server [1] nun detailliert beschrieben und betriebsbereit ist, werden wir uns mit dem Android-Client [2] befassen.

3.6.1. Architektur des Android-Studio-Projekts

Das Projekt übernimmt die Architektur des Projekts [client-android-skel] (siehe Abschnitt 1.17). In der oben dargestellten Architektur des Android-Clients lassen sich drei Blöcke unterscheiden:

  • die Schicht [DAO], die für die Kommunikation mit dem Webdienst zuständig ist;
  • die [vues], die für die Kommunikation mit dem Benutzer zuständig sind;
  • die [activité], die die Verbindung zwischen den beiden vorgenannten Blöcken herstellt. Die Ansichten haben keine Kenntnis von der Schicht [DAO]. Sie kommunizieren ausschließlich mit der Aktivität.

Diese Architektur spiegelt sich in der Architektur des Android-Studio-Projekts des Android-Clients wider:

 
  • Das Paket „[activity]“ implementiert die Aktivität;
  • das Paket [architecture] enthält die zuvor von uns entwickelten Architekturelemente;
  • Das Paket [dao] implementiert die Schicht [DAO];
  • Das Paket [fragments] implementiert die [vues];

3.6.2. Anpassung des Projekts

  

Der Ordner [architecture / custom] enthält die anpassbaren Elemente der Architektur.

Die Schnittstelle [IMainActivity] sieht wie folgt aus:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // Zugriff auf die Sitzung
  ISession getSession();

  // Wechsel der Ansicht
  void navigateToView(int position, ISession.Action action);

  // Wartungsverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Anwendungskonstanten -------------------------------------

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 000;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;

  // Fragment-Adjazenz
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = false;

  // Ladebild
  boolean IS_WAITING_ICON_NEEDED = true;

  // Anzahl der Anwendungsfragmente
  int FRAGMENTS_COUNT = 4;

  // Anzahl der Aufrufe
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • Zeilen 25, 28: Anpassung der Schicht [DAO];
  • Zeile 31: Diese Anwendung führt authentifizierte Zugriffe auf den Server durch;
  • Zeile 40: Es wird ein Ladebild benötigt;
  • Zeile 43: Die Anwendung hat vier Fragmente;
  • Zeilen 46–49: Die Nummern der vier Fragmente;
  • Zeile 37: Es gibt keine Registerkarten;

Die Basisklasse [CoreState] für die Fragmentzustände lautet wie folgt:


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 besucht oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeilen 15–18: Die vier Fragmente haben einen Status:
  

Schließlich enthält die Sitzung die zwischen den Fragmenten gemeinsam genutzten Daten:


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 {
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen

  // Liste der Ärzte
  private List<Medecin> médecins;
  // Liste der Patienten
  private List<Client> clients;
  // Terminkalender eines Arztes für einen bestimmten Tag
  private AgendaMedecinJour agenda;
  // Position des angeklickten Elements im Terminkalender
  private int position;
  // Termin-Datum im englischen Format „yyyy-MM-dd“
  private String dayRv;
  // Termin-Datum im französischen Format „dd-MM-yyyy“
  private String jourRv;

  // Getter und Setter
...
}
  • Zeilen 17–28: Die Sitzung speichert sechs Informationen. Wir werden deren Rolle bei Bedarf erläutern.

3.6.3. Die Schicht [DAO]

  • in [1] die in den Serverantworten gekapselten Entitäten. Diese wurden in Abschnitt 3.5 vorgestellt;
  • in [2] die Elemente des Clients, die den Datenaustausch mit dem Server verwalten;

Wir werden nicht noch einmal auf die Elemente [1] eingehen. Sie wurden bereits vorgestellt. Der Leser wird gebeten, bei Bedarf zu Abschnitt 3.5 zurückzukehren. Wir werden nun die Implementierung des Pakets [service] untersuchen. Dabei werden wir auch auf die Implementierung des sicheren Datenaustauschs zwischen Client und Server eingehen.

3.6.3.1. Implementierung des Datenaustauschs zwischen Client und Server

  

Die Klasse [WebClient] ist eine Komponente von AA, die Folgendes beschreibt:

  • die vom Webdienst bereitgestellten URL;
  • deren Parameter;
  • deren Antworten;

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

  // Liste der Ärzte
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();

  // Liste der Kunden
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();

  // Liste der Termine eines Arztes
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);

  // Liste der Termine eines Arztes
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);

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

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

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

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

  // einen Termin hinzufügen RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);

  // Termin löschen
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);

  // Terminkalender eines Arztes abrufen
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

}
  • Zeilen 19–60: Hier finden sich alle in Abschnitt 3.5 behandelten URL;
  • Zeile 16: die Komponente [RestTemplate] von [Spring Android], auf der die Client-Server-Kommunikation basiert;

3.6.3.2. Die Schnittstelle [IDao]

  

Die Schnittstelle [IDao] der Schicht [DAO] lautet wie folgt:


package rdvmedecins.android.dao.service;

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

import java.util.List;

public interface IDao {
  // URL des Webdienstes
  public void setUrlServiceWebJson(String url);

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

  // Client-Timeout
  public void setTimeout(int timeout);

  // Liste der Kunden
  public Observable<List<Client>> getAllClients();

  // Liste der Ärzte
  public Observable<List<Medecin>> getAllMedecins();

  // Liste der Terminfenster eines Arztes
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);

  // Liste der Termine eines Arztes an einem bestimmten Tag
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);

  // einen Kunden anhand seiner ID suchen
  public Observable<Client> getClientById(long id);

  // einen Arzt anhand seiner ID suchen
  public Observable<Medecin> getMedecinById(long id);

  // einen Termin anhand seiner ID suchen
  public Observable<Rv> getRvById(long id);

  // einen Terminblock anhand seiner ID suchen
  public Observable<Creneau> getCreneauById(long id);

  // einen RV hinzufügen
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);

  // einen Termin mit der ID RV löschen
  public Observable<Rv> supprimerRv(long idRv);

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

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);
}
  • Zeile 10: zur Festlegung des URL des Webdienstes / jSON;
  • Zeile 13: zur Festlegung des Benutzers für die Client-Server-Kommunikation. [user] ist die Benutzer-ID, [mdp] sein Passwort;
  • Zeile 16: zur Festlegung einer maximalen Wartezeit für die Antwort des Servers;
  • Zeilen 18–49: Jedem vom Webdienst bereitgestellten URL entspricht eine Methode. Sie übernehmen die Signatur der gleichnamigen Methoden der Komponente AA [WebClient];
  • Zeile 52: zur Steuerung des Modus debug der Schicht [DAO];

3.6.3.3. Die Klasse [Dao]

  

Die Implementierung [DAO] der oben genannten Schnittstelle [IDao] lautet wie folgt:


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 {

  // Webdienst-Client
  @RestService
  protected WebClient webClient;
  // Sicherheit
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // der RestTemplate
  private RestTemplate restTemplate;
  // Factory von 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));
    }
    // Authentifizierungs-Interceptor?
    if (isBasicAuthentificationNeeded) {
      // Der Authentifizierungs-Interceptor wird hinzugefügt
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }

  }

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

  // Implementierung der Schnittstelle IDao --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // Protokoll
    log("getAllClients");
    // Ergebnis
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // Protokoll
    log("getAllMedecins");
    // Ergebnis
    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) {
    // Protokoll
    log("getAllCreneaux");
    // Ergebnis
    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) {
    // Protokoll
    log("getRvMedecinJour");
    // Ergebnis
    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) {
    // Protokoll
    log("getClientById");
    // Ergebnis
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }

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

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

  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // Protokoll
    log("getCreneauById");
    // Ergebnis
    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) {
    // Protokoll
    log("ajouterRv");
    // Ergebnis
    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) {
    // Protokoll
    log("supprimerRv");
    // Ergebnis
    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) {
    // Protokoll
    log("getAgendaMedecinJour");
    // Ergebnis
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }

}
  • Zeilen 18–72: entsprechen den Standardzeilen in der Klasse [Dao] des Projekts [client-android-skel];
  • Zeilen 74–216: Implementierung der Schnittstelle [IDao]. Die Methoden, die die vom Webdienst bereitgestellten URL abfragen, delegieren diese Abfrage an die Komponente AA [WebClient] (Zeilen 22–23);
  • Zeilen 58–63: Wenn der Client-Server-Austausch durch eine Basic-Authentifizierung authentifiziert wird, wird der Komponente [RestTemplate] ein Interceptor hinzugefügt. Dies hat zur Folge, dass jede von der Komponente [RestTemplate] gesendete Anfrage an HTTP von der Klasse [MyAuthInterceptor] abgefangen wird (Zeilen 25–26);

Die Klasse [MyAuthInterceptor] lautet wie folgt:


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 {

  // Benutzer
  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;
  }
}
  • Zeile 15: Die Klasse [MyAuthInterceptor] ist eine Komponente AA vom Typ [singleton];
  • Zeile 16: Die Klasse [MyAuthInterceptor] erweitert die Spring-Schnittstelle [ClientHttpRequestInterceptor]. Diese Schnittstelle verfügt über eine Methode, nämlich die Methode [intercept] in Zeile 22. Diese Schnittstelle wird erweitert, um alle HTTP-Anfragen des Clients abzufangen. Die Methode [intercept] erhält drei Parameter;
    • [HtpRequest request]: die abgefangene Anfrage HTTP,
    • [byte[] body]: deren Payload, falls vorhanden (z. B. über POST übermittelte Werte),
    • [ClientHttpRequestExecution execution]: die Spring-Komponente, die die Anfrage ausführt;

Wir fangen alle HTTP-Anfragen des Android-Clients ab, um ihnen den in Abschnitt 3.5 vorgestellten Authentifizierungsheader HTTP hinzuzufügen.

  • Zeile 23: Wir extrahieren die Header HTTP aus der abgefangenen Anfrage;
  • Zeile 24: Wir erstellen den Authentifizierungsheader HTTP. Die verwendete Authentifizierungsmethode (Base64-Kodierung der Zeichenfolge „user:mdp“) wird von der Spring-Klasse [HttpBasicAuthentication] bereitgestellt;
  • Zeile 25: Der soeben erstellte Authentifizierungsheader wird zu den aktuellen Headern der abgefangenen Anfrage hinzugefügt;
  • Zeile 26: Die Ausführung der abgefangenen Anfrage wird fortgesetzt. Zusammenfassend lässt sich sagen, dass die abgefangene Anfrage um den Authentifizierungsheader erweitert wurde;

Die Implementierungen der Methoden der Schnittstelle [IDao] folgen alle demselben Muster. Nehmen wir als Beispiel die Methode [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // Protokoll
    log("getAgendaMedecinJour");
    // Ergebnis
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • Zeile 2: Die Methode erwartet zwei Parameter:
    • [idMedecin]: die ID des Arztes, dessen Terminkalender abgefragt werden soll;
    • [jour]: der Tag, für den der Terminkalender abgerufen werden soll;
  • Zeile 6: Die Methode [getResponse] der übergeordneten Klasse [AbstractDao] wird aufgerufen. Diese Methode erwartet einen Parameter vom Typ [IRequest<T>], wobei T der von der Methode [getAgendaMedecinJour] in Zeile 2 zurückgegebene Typ ist, hier [Response<AgendaMedecinJour>]. Die Schnittstelle [IRequest] verfügt nur über eine Methode: [getResponse] (Zeile 8);
  • Zeilen 8–10: Implementierung der Methode [IRequest.getResponse]. Diese Methode muss das von der Methode [getAgendaMedecinJour] in Zeile 2 erwartete Ergebnis vom Typ [Response<AgendaMedecinJour>] zurückgeben;
  • Zeile 9: Die Antwort wird von der Methode [webClient.getAgendaMedecinJour] zurückgegeben:

  // den Terminkalender eines Arztes abrufen
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

Die in Zeile 9 verwendeten Parameter sind diejenigen, die in Zeile 2 an die Methode [getAgendaMedecinJour] übergeben wurden. Aus diesem Grund müssen diese Parameter das Attribut final haben;

3.6.4. Die Aktivität [MainActivity]

Serveur
  

Die Klasse [MainActivity] lautet wie folgt:


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 {

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

  // Elternklasse ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    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;
  }

  // Schnittstelle 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);
  }
}
  • Zeilen 21–66: Diese Zeilen sind standardmäßig in der Vorlage [client-android-skel] enthalten;
  • Zeilen 66–119: Implementierung der Schnittstelle [IDao]. Alle Methoden delegieren die Arbeit an die Schicht [DAO] ab Zeile 26;
  • Zeilen 42–46: Die Methode [getFragments] gibt das Array mit den vier Fragmenten der Anwendung zurück;
  • Zeilen 58–61: Die Konfigurationsansicht ist die erste Ansicht, die beim Start der Anwendung angezeigt wird;

3.6.5. Die Sitzung

  

Die Klasse [Session] dient dazu, die Informationen zu speichern, die zwischen den Fragmenten übertragen werden sollen. Sie sieht wie folgt aus:


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 {
  // Ärzteverzeichnis
  private List<Medecin> médecins;
  // Kundenliste
  private List<Client> clients;
  // Terminkalender
  private AgendaMedecinJour agenda;
  // Position des angeklickten Elements im Kalender
  private int position;
  // Termin-Datum im englischen Format „yyyy-MM-dd“
  private String dayRv;
  // Tag des Termins im französischen Format „dd-MM-yyyy“
  private String jourRv;


  // Getter und Setter
...
}
  • Zeile 10: Die Klasse [Session] ist eine Komponente AA, von der ein einziges Exemplar instanziiert wird;
  • Zeilen 12–15: In dieser Fallstudie wird davon ausgegangen, dass sich die Listen der Ärzte und Kunden nicht ändern. Sie werden beim Start der Anwendung abgefragt und in der Sitzung gespeichert, damit die Fragmente darauf zugreifen können;
  • Zeilen 20–23: Der gewünschte Tag für einen Termin. Er wird in zwei Formen verarbeitet: in französischer Schreibweise (Zeile 23) innerhalb der Android-Client-App und in englischer Schreibweise (Zeile 21) für den Datenaustausch mit dem Server;
  • Zeile 19: Die Position des angeklickten Elements (Link „Hinzufügen“/„Löschen“) im Kalender;

3.6.6. Verwaltung der Konfigurationsansicht

3.6.6.1. Die Ansicht

Die Konfigurationsansicht ist die Ansicht, die beim Start der Anwendung angezeigt wird:

Image

Die Elemente der Benutzeroberfläche sind folgende:

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

3.6.6.2. Das Fragment

Die Konfigurationsansicht wird durch das folgende Fragment [ConfigFragment] verwaltet:

 

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 {

  // Elemente der visuellen Benutzeroberfläche
  @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;

  // Eingaben
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;

  // Validierung der Seite
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // Implementierung der Methoden der übergeordneten Klasse -------------------------------------------
 ...

}
  • Zeile 25: Das Fragment ist dem folgenden Menü [menu_config] zugeordnet:
  

<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>
  • Zeilen 28–38: die Elemente der Benutzeroberfläche;
  • Zeilen 41–43: die drei Eingabefelder des Formulars;

Der Klick auf die Menüoption [Valider] wird von der Methode [doValider] verarbeitet:


// Validierung der Seite
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // Eventuelle vorherige Fehlermeldungen werden ausgeblendet
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // Gültigkeit der Eingaben prüfen
    if (!isPageValid()) {
      return;
    }
    // Das URL des Webdienstes wird ausgefüllt
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // Der Benutzer wird angegeben
    mainActivity.setUser(utilisateur, mdp);
    // Wartephase beginnt – es werden 2 asynchrone Aufgaben gestartet
    beginWaiting(2);
    // Ärzte
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // Die Antwort wird verarbeitet
        consumeMedecins(responseMedecins);
      }
    });
    // Kunden
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // die Antwort wird verarbeitet
        consumeClients(responseClients);
      }
    });
  }


  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // Fehler?
    if (responseMedecins.getStatus() != 0) {
      // Meldung
      showAlert(responseMedecins.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Ärzte werden in der Sitzung gespeichert
    session.setMédecins(responseMedecins.getBody());
  }

  private void consumeClients(Response<List<Client>> responseClients) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // Fehler?
    if (responseClients.getStatus() != 0) {
      // Meldung
      showAlert(responseClients.getMessages());
      // Stornierung
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Kunden werden in der Sitzung gespeichert
    session.setClients(responseClients.getBody());
  }
  • Zeilen 8–10: Die Gültigkeit der drei Eingaben im Formular wird geprüft. Ist das Formular ungültig, wird der Vorgang nicht fortgesetzt;
  • Zeilen 11–14: Die für die Ebene [DAO] erforderlichen Eingaben werden an die Aktivität übergeben;
  • Zeile 16: Der übergeordneten Klasse wird mitgeteilt, dass zwei asynchrone Aufgaben gestartet werden, und die Wartezeit wird vorbereitet;
  • Zeilen 17–24: Die Liste der Ärzte wird angefordert;
  • Zeile 18: Die Methode [executeInBackground] erwartet zwei Parameter:
    • Zeile 18: Der auszuführende und zu beobachtende Prozess wird von der Methode [mainActivity.getAllMedecins()] bereitgestellt;
    • Zeilen 18–24: Der zweite Parameter ist eine Instanz vom Typ [Action1<T>], wobei T der vom beobachteten Prozess zurückgegebene Typ ist, hier [Response<List<Medecin>>]
  • Zeile 22: Sobald die Antwort empfangen wird, wird sie an die Methode [consumeMedecins] in Zeile 36 übergeben;
  • Zeilen 25–33: Nachdem eine erste asynchrone Aufgabe gestartet wurde, wird eine zweite gestartet, um die Kundenliste abzufragen. Es laufen also zwei Aufgaben parallel;
  • Zeilen 36–52: Die Antwort der Ärzte-Aufgabe ist eingegangen. Diese wird ausgewertet;
  • Zeilen 42–49: Zunächst wird geprüft, ob der Server im Feld [status] der Antwort einen Fehler gemeldet hat;
  • Zeile 44: Liegt ein Fehler vor, werden die Meldungen angezeigt, die der Server im Feld [messages] der Antwort hinterlegt hat;
  • Zeile 46: Alle Aufgaben werden abgebrochen;
  • Zeile 48: Man kehrt zur Benutzeroberfläche zurück;
  • Zeile 51: Wenn kein Fehler aufgetreten ist, wird die Liste der Ärzte für die Sitzung gespeichert;

Die Gültigkeit der Eingaben (Zeile 8) wird mit folgender Methode überprüft:


  private boolean isPageValid() {
    // Die Gültigkeit der eingegebenen Daten wird überprüft
    boolean erreur;
    URI service;
    // Gültigkeit des URL des Dienstes 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) {
      // Der Fehler wird vermerkt
      erreur = true;
    }
    if (erreur) {
      // Anzeige des Fehlers
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // Benutzer
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // Der Fehler wird angezeigt
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // Fehler wird vermerkt
      erreur = true;
    }
    // Passwort
    mdp = edtMdp.getText().toString().trim();
    // Zurück
    return !erreur;
}

Die Methode [beginWaiting] (Zeile 16) lautet wie folgt:


  // Wartezeit beginnt
  protected void beginWaiting(int numberOfRunningTasks) {
    // Vorbereitung des Aufrufs der Aufgaben
    beginRunningTasks(numberOfRunningTasks);
    // Status der Schaltflächen und Menüs
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • Zeile 4: Der übergeordneten Aufgabe wird mitgeteilt, dass die Aufgabe [numberOfRunningTasks] gestartet wird;
  • Zeile 6: Alle Menüoptionen werden ausgeblendet;
  • Zeile 7: Anschließend wird die Option [Actions/Annuler] sichtbar gemacht;

Der Klick auf die Menüoption [Annuler] wird von der Methode [doAnnuler] verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone Aufgaben werden abgebrochen
    cancelRunningTasks();
}
  • Zeile 8: Die übergeordnete Klasse wird aufgefordert, die asynchronen Aufgaben abzubrechen;

3.6.6.3. Verwaltung des Lebenszyklus des Fragments

Das Fragment hat den folgenden Status [ConfigFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // Sichtbarkeit der beiden Fehlermeldungen
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;

  // Getter und Setter
...
}
  • Wenn die übergeordnete Klasse dies anfordert, speichert das Fragment die Sichtbarkeit seiner beiden Fehlermeldungen;

Der Lebenszyklus des Fragments ist wie folgt implementiert:


// Implementierung der Methoden der übergeordneten Klasse -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // Speichern des Fragmentstatus
    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) {
      // Erster Besuch
      // Fehlermeldungen werden ausgeblendet
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // Menü
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Sichtbarkeit der Fehlermeldungen wiederherstellen
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // Nicht der erste Besuch – Fehlermeldungen werden wieder angezeigt
    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) {
    // Menü
    initMenu();
    // Nächste Ansicht?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }

  // Private Methoden ------------------------------------------------
  private void initMenu(){
    // Menüstatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • Zeilen 2–9: Wenn die übergeordnete Klasse dies anfordert, speichert das Fragment den Status seiner beiden Fehlermeldungen;
  • Zeilen 11–14: Die Nummer des Fragments lautet [IMainActivity.VUE_CONFIG];
  • Zeilen 16–19: werden ausgeführt, wenn das Fragment zum ersten Mal generiert wird (previousState == null) oder bei den folgenden Malen neu generiert wird (previousState != null). Hier gibt es nichts zu tun;
  • Zeilen 21–31: werden ausgeführt, wenn die dem Fragment zugeordnete Ansicht zum ersten Mal erstellt wird (previousState == null) oder bei den folgenden Malen neu erstellt wird (previousState != null);
    • Zeilen 24–29: Beim ersten Aufruf werden die Fehlermeldungen ausgeblendet und das Menü ohne die Aktion [Annuler] (Zeilen 62–66) angezeigt;
  • Zeilen 33–35: werden ausgeführt, wenn man über eine Operation [SUBMIT] zum Fragment gelangt. Dies kommt hier jedoch nie vor;
  • Zeilen 37–44: werden ausgeführt, wenn das Fragment über eine Operation [NAVIGATION] oder [RESTORE] aufgerufen wird. Der Status der Fehlermeldungen wird aus dem vorherigen Status wiederhergestellt;
  • Zeilen 47–49: werden ausgeführt, wenn alle vorherigen Aktualisierungen vorgenommen wurden. Es gibt nichts mehr zu tun;
  • Zeilen 51–59: werden ausgeführt, wenn alle asynchronen Aufgaben abgeschlossen sind;
    • Zeilen 53–54: Das Menü wird in seinen Standardzustand zurückgesetzt;
    • Zeilen 56–58: Wenn die Aufgaben normal abgeschlossen wurden, wechselt man zur nächsten Ansicht, andernfalls bleibt man in derselben Ansicht;

3.6.7. Verwaltung der Startansicht

3.6.7.1. Die Ansicht

Die Startansicht sieht wie folgt aus:

Image

Die Elemente der Benutzeroberfläche sind folgende:

Type
Nom
1
Spinner
spinnerMedecins
2
DatePicker
edtJourRv

3.6.7.2. Das Fragment

Die Startansicht wird durch das folgende Fragment [AccueilFragment] verwaltet:

 

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 {

  // Elemente der Benutzeroberfläche
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;

  // Lokale Daten
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;

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

  // Implementierung der Methoden der übergeordneten Klasse -------------------------------------
...
}
  • Zeile 26: Das Fragment ist dem folgenden Menü [menu_accueil] zugeordnet:
  

<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>
  • Zeilen 31–34: die Elemente der Benutzeroberfläche;
  • Zeile 37: die Liste der Ärzte;
  • Zeile 38: ein Kalender;
  • Zeile 39: die Datenquelle für den Arzt-Spinner;

Der Klick auf den Link [Valider] wird durch die folgende Methode [doValider] verarbeitet:


// Validierung der Seite
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // Die ID des ausgewählten Arztes wird notiert
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // Der Tag wird in der Sitzung gespeichert
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // Umstellung auf das Datumsformat yyyy-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // Wartezeit beginnt – es wird eine asynchrone Aufgabe gestartet
    beginWaiting(1);
    // Der Terminkalender des Arztes wird abgefragt
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // die Antwort wird verarbeitet
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // Fehler?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // Meldung
      showAlert(responseAgendaMedecinJour.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Der Kalender wird in die Sitzung aufgenommen
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • Zeile 5: Die ID des ausgewählten Arztes wird abgerufen;
  • Zeilen 7–8: Das ausgewählte Datum wird im französischen Format angezeigt;
  • Zeilen 10–11: Das ausgewählte Datum wird im englischen Format formatiert;
  • Zeile 13: Der übergeordneten Klasse wird mitgeteilt, dass eine asynchrone Aufgabe gestartet wird, und die Wartezeit wird vorbereitet;
  • Zeilen 15–22: Der Terminkalender des Arztes wird abgefragt;
    • Zeile 15: Die Methode [executeInBackground] erwartet zwei Parameter:
      • Zeile 15: Der auszuführende und zu beobachtende Prozess wird von der Methode [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)] bereitgestellt;
      • Zeilen 15–22: Der zweite Parameter ist eine Instanz vom Typ [Action1<T>], wobei T der vom beobachteten Prozess zurückgegebene Typ ist, hier [Response<AgendaMedecinJour>]
    • Zeile 20: Sobald die Antwort empfangen wird, wird sie an die Methode [consumeAgenda] in Zeile 25 übergeben;
  • Zeilen 25–37: Der Terminkalender des Arztes wurde empfangen. Er wird ausgewertet;
  • Zeilen 27–34: Zunächst wird geprüft, ob der Server im Feld [status] der Antwort einen Fehler gemeldet hat;
  • Zeile 29: Liegt ein Fehler vor, werden die Meldungen angezeigt, die der Server im Feld [messages] der Antwort hinterlegt hat;
  • Zeile 31: Alle Aufgaben werden abgebrochen;
  • Zeile 33: Man kehrt zur Benutzeroberfläche zurück;
  • Zeile 36: Wenn keine Fehler aufgetreten sind, wird der Kalender in den aktiven Modus versetzt;

Die Methode [beginWaiting] (Zeile 13) lautet wie folgt:


  // Wartephase beginnt
  protected void beginWaiting(int numberOfRunningTasks) {
    // Vorbereitung des Starts der Aufgaben
    beginRunningTasks(numberOfRunningTasks);
    // Status der Schaltflächen und Menüs
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • Zeile 4: Der übergeordneten Aufgabe wird mitgeteilt, dass die Aufgabe [numberOfRunningTasks] gestartet wird;
  • Zeile 6: Alle Menüoptionen werden ausgeblendet;
  • Zeile 7: Anschließend wird die Option [Actions/Annuler] sichtbar gemacht;

Der Klick auf die Menüoption [Annuler] wird von der Methode [doAnnuler] verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // Die asynchronen Aufgaben werden abgebrochen
    cancelRunningTasks();
}
  • Zeile 8: Die übergeordnete Klasse wird aufgefordert, die asynchronen Aufgaben abzubrechen;

Der Klick auf die Menüoption [Retour à la configuration] wird wie folgt verarbeitet:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // Wechsel zur Konfigurationsansicht
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Zeile 4: Mit der Aktion [NAVIGATION] wird zur Konfigurationsansicht navigiert. Das bedeutet, dass die Konfigurationsansicht in dem Zustand wiederhergestellt werden soll, in dem sie verlassen wurde;

3.6.7.3. Verwaltung des Lebenszyklus des Fragments

Das Fragment hat den folgenden 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 des Fragments [Accueil]
  // Position des ausgewählten Arztes
  private int selectedMedecinPosition;
  // Ausgewähltes Datum
  private int year;
  private int month;
  private int dayOfMonth;
  // Datenquelle des Arzt-Spinners
  private String[] spinnerMedecinsDataSource;

  // Konstruktoren
  public AccueilFragmentState() {

  }

  // Getter und Setter
...
}
  • Zeile 11: Dient dazu, das in der Ärzte-Liste ausgewählte Element wiederherzustellen;
  • Zeilen 13–15: Ermöglicht die Wiederherstellung des im Kalender ausgewählten Datums;
  • Zeile 17: dient zur Rückgabe der Datenquelle der Ärzte-Liste;

Der Lebenszyklus des Fragments ist wie folgt implementiert:


// Implementierung der Methoden der übergeordneten Klasse -------------------------------------
  @Override
  public CoreState saveFragment() {
    // Die Ansicht wird gespeichert
    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) {
    // Die Ärzte werden aus der Sitzung abgerufen
    medecins = session.getMédecins();
    // Erster Besuch?
    if (previousState == null) {
      // Erstellung der vom Spinner angezeigten Tabelle
      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 {
      // Kein erster Besuch
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // Der Kalender
    calendrier = Calendar.getInstance();
  }

  @Override
  protected void initView(CoreState previousState) {
    // Der Spinner für Ärzte wird mit seiner Datenquelle verknüpft
    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);
    // Mindestdatum des Kalenders bis heute
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // Erster Besuch?
    if (previousState == null) {
      // Menü
      initMenu();
    }
  }

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

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Der aktuelle Status der Sitzung wird wiederhergestellt
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // Auswahl der Ärzte im Spinner
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // Kalender
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // wird aufgerufen, nachdem alle Aufgaben abgeschlossen oder abgebrochen wurden
    // Menü-Status
    initMenu();
    // Nächste Ansicht?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }

  // private Methoden ------------------------------------------------
  private void initMenu() {
    // Menüstatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • Zeilen 2–9: Auf Anfrage seiner übergeordneten Klasse speichert das Fragment den Status der folgenden Elemente:
    • Zeile 6: die ausgewählte Position in der Ärzte-Liste;
    • Zeilen 7–9: den Tag des Monats, den Monat und das Jahr des im Kalender ausgewählten Datums;
    • Zeile 10: die Datenquelle des Arzt-Spinners;
  • Zeilen 14–17: Die Nummer des Fragments lautet [IMainActivity.VUE_ACCUEIL];
  • Zeilen 19–39: werden ausgeführt, wenn das Fragment zum ersten Mal generiert wird (previousState == null) oder bei den folgenden Generierungen neu generiert wird (previousState != null);
    • Zeilen 25–31: Bei einem ersten Besuch wird die Datenquelle für den Ärzte-Spinner aufgebaut;
    • Zeilen 33–35: Bei weiteren Besuchen wird die Datenquelle des Spinners aus dem vorherigen Zustand des Fragments abgerufen;
  • Zeilen 41–54: werden ausgeführt, wenn die dem Fragment zugeordnete Ansicht zum ersten Mal erstellt wird (previousState == null) oder bei den folgenden Aufrufen neu aufgebaut wird (previousState != null);
    • Zeilen 50–53: Beim ersten Besuch wird das Menü ohne die Aktion [Annuler] angezeigt (Zeilen 88–92);
    • Zeilen 43–48: Bei allen Besuchen, ob es sich um den ersten handelt oder nicht, wird der Spinner für die Ärzte mit seiner Quelle verknüpft (Zeilen 44–46) und das Mindestdatum im Kalender auf das heutige Datum gesetzt (Zeile 48);
  • Zeilen 56–60: werden ausgeführt, wenn man über eine Operation [SUBMIT] zum Fragment gelangt. Man kommt dann aus der Ansicht [CONFIG]. Das Menü wird in seinen Ausgangszustand versetzt;
  • Zeilen 62–70: werden ausgeführt, wenn man über eine Operation [NAVIGATION] oder [RESTORE] zum Fragment gelangt;
    • Zeile 67: Der Spinner für die Ärzte wird auf den zuletzt ausgewählten Arzt zurückgesetzt;
    • Zeile 69: Der Kalender wird auf das zuletzt ausgewählte Datum gesetzt;
  • Zeilen 72–74: werden ausgeführt, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Es gibt nichts mehr zu tun;
  • Zeilen 76–85: werden ausgeführt, sobald alle asynchronen Aufgaben abgeschlossen sind;
    • Zeile 80: Das Menü wird in seinen Standardzustand zurückgesetzt;
    • Zeilen 82–84: Wenn die Aufgaben normal abgeschlossen wurden, wechselt man zur nächsten Ansicht, andernfalls bleibt man in derselben Ansicht;

3.6.8. Verwaltung der Ansicht „Kalender“

3.6.8.1. Die Ansicht

Die Startansicht sieht wie folgt aus:

Image

Die Elemente der Benutzeroberfläche sind folgende:

Type
Nom
1
TextView
txtTitre2
2
ListView
lstCreneaux

3.6.8.2. Das Fragment

Die Ansicht „Agenda“ wird durch das folgende Fragment [AgendaFragment] verwaltet:

 

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 {

  // Elemente der visuellen Benutzeroberfläche
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;

  // vom Fragment angezeigter Kalender
  private AgendaMedecinJour agenda;
  // Informationen zu den Terminfenstern
  private int firstPosition;
  private int top;
  // Termin gelöscht oder nicht
  private boolean rdvSupprimé;
  // Nummer des hinzugefügten oder gelöschten Termins
  private int numCréneau;

  // Aktualisierung des Kalenders nach einem Hinzufügen/Löschen
  private void updateAgenda() {
  ...
  }

...

  // Implementierung der Methoden der übergeordneten Klasse ------------------------------------------------------
  ...
}
  • Zeile 27: Das Fragment ist dem folgenden Menü [menu_agenda] zugeordnet:
  

<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>
  • Zeilen 32–35: Elemente der Benutzeroberfläche;
  • Zeilen 37–45: globale Daten für die Methoden;

3.6.8.2.1. Methode [updateAgenda]

Die (Neu-)Generierung der Terminliste des Kalenders ist an mehreren Stellen im Code erforderlich. Sie wurde in die folgende private Methode [updateAgenda] ausgelagert:


  // Aktualisierung des Terminkalenders nach einem Hinzufügen/Löschen
  private void updateAgenda() {
    // (Neu-)Generierung der Termine im Kalender
    // Der Kalender wird in der Sitzung erfasst und in einem Feld des Fragments gespeichert
    agenda = session.getAgenda();
    // Neugenerierung des ListView der Termine
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // Es wird an die richtige Stelle im ListView gesprungen
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • Zeile 5: Der Kalender wird aus der Sitzung abgerufen und im Feld [agenda] des Fragments gespeichert;
  • Zeilen 7–9: Der Adapter der Komponente [ListView] wird definiert. Dieser Adapter legt sowohl die Datenquelle von [ListView] als auch das Anzeigemodell für jedes Element dieser Quelle fest. Wir werden diesen Adapter in Kürze vorstellen;
  • Zeile 11: Es wird zur vorherigen Position des Terminkalenders zurückgekehrt. Tatsächlich ist nur ein Teil der Zeitfenster des Tages sichtbar. Wenn im letzten Zeitfenster ein Termin hinzugefügt oder gelöscht wird, aktualisiert der obige Code die Seite, um den neuen Terminkalender anzuzeigen. Durch diese Aktualisierung springt die Anzeige wieder auf den ersten Terminblock, was nicht erwünscht ist. Zeile 5 behebt dieses Problem. Eine Beschreibung dieser Lösung findet sich in den Klassen URL und [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];

Die Klasse [ListCreneauxAdapter] dient dazu, eine Zeile der Klasse [ListView] zu definieren:

Image

Wie oben zu sehen ist, unterscheidet sich die Anzeige je nachdem, ob für das Zeitfenster ein Termin vorgesehen ist oder nicht. Der Code der Klasse [ListCreneauxAdapter] lautet wie folgt:


...

public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {

    // die Tabelle der Zeitfenster
    private CreneauMedecinJour[] creneauxMedecinJour;
    // der Ausführungskontext
    private Context context;
    // die ID des Anzeigelayouts einer Zeile in der Zeitfensterliste
    private int layoutResourceId;
    // Klick-Listener
    private AgendaFragment vue;

    // Konstruktor
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // Die Informationen werden gespeichert
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // Das Zeitfenster-Array wird nach Uhrzeit sortiert
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }

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

// Sortierung der Zeitfenster-Tabelle
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • Zeile 3: Die Klasse [ListCreneauxAdapter] muss einen vordefinierten Adapter für die [ListView] erweitern, hier die Klasse [ArrayAdapter], die, wie der Name schon sagt, die Klasse [ListView] mit einem Array von Objekten versorgt, in diesem Fall vom Typ [CreneauMedecinJour]. Hier noch einmal der Code dieser Entität:

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // Felder
    private Creneau creneau;
    private Rv rv;
...  
}
  • Die Klasse [CreneauMedecinJour] enthält ein Zeitfenster (Zeile 5) und einen möglichen Termin (Zeile 6) oder null, falls kein Termin vorliegt;

Zurück zum Code der Klasse [ListCreneauxAdapter]:

  • Zeile 15: Der Konstruktor erhält vier Parameter:
    1. die aktuelle Android-Aktivität,
    2. die Datei XML, die den Inhalt jedes Elements von [ListView] definiert,
    3. das Array mit den Terminfenstern des Arztes,
    4. die Ansicht selbst;
  • Zeile 24: Das Array der Zeitfenster ist in aufsteigender Reihenfolge der Uhrzeiten sortiert;

Die Methode [getView] ist dafür zuständig, die Ansicht zu generieren, die einer Zeile der Datei [ListView] entspricht. Diese umfasst drei Elemente:

 
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

Der Code der Methode [getView] lautet wie folgt:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // Man positioniert sich auf dem richtigen Zeitfenster
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // Die Zeile wird angelegt
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // das Zeitfenster
        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()));
        // der Kunde
        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);
        // der Link
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // hinzufügen
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // löschen
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // Link-Listener
        btnValider.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // Die Informationen werden an die Kalenderansicht weitergeleitet
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // die Zeile wird dargestellt
        return row;
    }
  • Zeile 2: „position“ ist die Zeilennummer, die in der Tabelle [ListView] generiert wird. Dies ist gleichzeitig die Nummer des Zeitfensters in der Tabelle [creneauxMedecinJour]. Die beiden anderen Parameter werden ignoriert;
  • Zeile 4: Wir rufen das anzuzeigende Zeitfenster in der Zeile von [ListView] ab;
  • Zeile 6: Die Zeile wird anhand ihrer Definition in XML erstellt
 

Der Code für [creneau_medecin.xml] lautet wie folgt:


<?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>
 
  • Zeilen 8–10: Das Zeitfenster [1] wird gebildet;
  • Zeilen 12–20: Die Kunden-ID [2] wird gebildet;
  • Zeile 23: Wenn der Zeitblock keinen Termin enthält;
  • Zeilen 25–26: Der Link [Ajouter] wird in blauer Farbe erstellt;
  • Zeilen 29–30: Andernfalls wird der Link [Supprimer] in roter Farbe erstellt;
  • Zeilen 33–40: Unabhängig von der Art des Links [Ajouter / Supprimer] wird der Klick auf den Link von der Methode [doValider] der Ansicht verarbeitet. Die Methode erhält zwei Argumente:
    1. die Nummer des angeklickten Termins,
    2. den Text des angeklickten Links;
  • Zeile 42: Die soeben erstellte Zeile wird zurückgegeben.

Es ist zu beachten, dass die Methode [doValider] des Fragments [AgendaFragment] die Links verarbeitet. Diese lautet wie folgt:


  // Klick auf einen Link [Ajouter / Supprimer]
  public void doValider(int numCréneau, String texte) {
    // Vorgang läuft?
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // Die Scrollposition wird gespeichert, um dorthin zurückzukehren
    // Lesen: [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // Position des ersten Elements: vollständig sichtbar oder nicht
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // Y-Versatz dieses Elements relativ zum oberen Rand von ListView
    // misst die Höhe des möglicherweise verdeckten Teils
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // Außerdem wird die Nummer des angeklickten Zeitfensters notiert
    this.numCréneau = numCréneau;
    // Je nach Text des Links wird nicht dasselbe getan
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • Die Methode [doValider] erhält zwei Informationen:
    • die Nummer des angeklickten Zeitfensters;
    • den Text („Hinzufügen“ / „Löschen“) des angeklickten Links;
  • Zeilen 4–7: Das Anklicken der Links [Supprimer / Ajouter] wird unterbunden, wenn asynchrone Aufgaben laufen. Dies ist eine Entscheidung, die das Schreiben des Codes erleichtert. Sie kann diskutiert werden;
  • Zeilen 11–15: Die Informationen (firstPosition, top) des ListView der Zeitfenster werden in Feldern des Fragments gespeichert, damit die private Methode [updateAgenda] diese mit derselben Scrollposition wiederherstellen kann;
  • Zeile 17: Die Nummer des angeklickten Zeitfensters wird notiert;
  • Zeilen 19–23: Je nach Text des angeklickten Links wird ein Eintrag hinzugefügt oder gelöscht;

3.6.8.2.2. Methode [doSupprimer]

Die Methode [doSupprimer] sorgt für die Löschung des Termins in dem angeklickten Zeitfenster:


// Löschen eines Termins
  private void doSupprimer() {
    // Warten auf den Abschluss von zwei Aufgaben
    beginWaiting(2);
    // Der Termin wird im Hintergrund gelöscht
    rdvSupprimé = false;
    // ID des zu löschenden Termins
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // Löschung durch eine asynchrone Aufgabe
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {

      @Override
      public void call(Response<Rv> responseRv) {
        // Verarbeitung des Ergebnisses
        consumeRv(responseRv);
      }
    });
  }

  // Verarbeitung einer Antwort
  private void consumeRv(Response<Rv> responseRv) {
    // Fehler?
    if (responseRv.getStatus() != 0) {
      // Meldung
      showAlert(responseRv.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Es wird vermerkt, dass der Termin gelöscht wurde
    rdvSupprimé = true;
    // Der aktuellste Kalender wird angefordert
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {

        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // Die Antwort wird verarbeitet
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }

  // Abruf eines Terminkalenders
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // Fehler?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // Meldung
      showAlert(responseAgendaMedecinJour.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Der Kalender wird in die Sitzung aufgenommen
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // Der Kalender der Ansicht wird aktualisiert
    updateAgenda();
  }
  • Zeile 4: Der übergeordneten Klasse wird mitgeteilt, dass zwei asynchrone Aufgaben gestartet werden, und es wird auf den Abschluss dieser beiden Aufgaben gewartet;
  • Zeile 8: Die ID des zu löschenden Termins wird abgerufen. Der Server benötigt diese Information;
  • Zeilen 9–18: Das Löschen des Termins wird über eine asynchrone Aufgabe angefordert;
    • Zeile 10: Die Methode [executeInBackground] erwartet zwei Parameter:
      • Zeile 10: Der auszuführende und zu beobachtende Prozess wird von der Methode [mainActivity.supprimerRv(idRv)] bereitgestellt;
      • Zeilen 10–17: Der zweite Parameter ist eine Instanz vom Typ [Action1<T>], wobei T der vom beobachteten Prozess zurückgegebene Typ ist, hier [Response<Rv>]
    • Zeile 15: Sobald die Antwort empfangen wird, wird sie an die Methode [consumeRv] in Zeile 21 übergeben;
  • Zeilen 21–44: Die Antwort der asynchronen Aufgabe wurde empfangen. Sie wird ausgewertet;
  • Zeilen 23–30: Zunächst wird geprüft, ob der Server im Feld [status] der Antwort einen Fehler gemeldet hat;
    • Zeile 25: Liegt ein Fehler vor, werden die Meldungen angezeigt, die der Server im Feld [messages] der Antwort hinterlegt hat;
    • Zeile 27: Alle Aufgaben werden abgebrochen;
    • Zeile 29: Man kehrt zur Benutzeroberfläche zurück;
  • Zeile 32: Wenn kein Fehler aufgetreten ist, wird vermerkt, dass der Termin gelöscht wurde;
  • Zeilen 34–43: Anstatt den Termin einfach aus dem derzeit vom Fragment angezeigten Kalender zu löschen, wird der neue Kalender des Arztes angefordert. Da es sich um eine Mehrbenutzeranwendung handelt, könnten auch andere Benutzer den Kalender des Arztes geändert haben. Daher ist es sinnvoll, die aktuellste Version zu verwenden;
  • Zeilen 34–43, 47–61: Wir wiederholen die im Fragment [AccueilFragment] durchgeführten Schritte, wobei diesmal Informationen aus der Sitzung verwendet werden;

Die Methode [beginWaiting] (Zeile 4) lautet wie folgt:


  // Wartephase beginnt
  protected void beginWaiting(int numberOfRunningTasks) {
    // Vorbereitung des Starts der Aufgaben
    beginRunningTasks(numberOfRunningTasks);
    // Status der Schaltflächen und Menüs
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • Zeile 4: Der übergeordneten Aufgabe wird mitgeteilt, dass die Aufgabe [numberOfRunningTasks] gestartet wird;
  • Zeile 6: Alle Menüoptionen werden ausgeblendet;
  • Zeile 7: Anschließend wird die Option „[Actions/Annuler]“ sichtbar gemacht;

3.6.8.2.3. Methode [doAnnuler]

Der Klick auf die Menüoption [Annuler] wird von der Methode [doAnnuler] verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // Asynchrone Aufgaben werden abgebrochen
    cancelRunningTasks();
}
  • Zeile 7: Die übergeordnete Klasse wird aufgefordert, die asynchronen Aufgaben abzubrechen;

3.6.8.2.4. Menüoption [Retour à la configuration]

Ein Klick auf die Menüoption [Retour à la configuration] wird wie folgt verarbeitet:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // Wechsel zur Konfigurationsansicht
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • Zeile 4: Mit der Aktion [NAVIGATION] wechselt man zur Konfigurationsansicht. Das bedeutet, dass man die Konfigurationsansicht in dem Zustand wiederherstellen möchte, in dem man sie verlassen hat;

3.6.8.2.5. Menüoption [Retour à l'accueil]

Ein Klick auf die Menüoption [Retour à l'accueil] wird ähnlich behandelt:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // Wechsel zur Startansicht
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. Verwaltung des Lebenszyklus des Fragments

Das Fragment hat den folgenden 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 der Ansicht
  private String titre;
  // ListView
  private int firstPosition;
  private int top;

  // Konstruktoren
  public AgendaFragmentState() {

  }

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

  // Getter und Setter
...
}
  • Zeile 10: Der oben in der Ansicht angezeigte Titel;
  • Zeilen 12–13: dienen zur Darstellung des scrolling aus dem ListView der Arzttermine;

Der Lebenszyklus des Fragments ist wie folgt implementiert:


// Implementierung der Methoden der übergeordneten Klasse ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // Zustandsspeicherung
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // Speichern der Scrollposition, um dorthin zurückzukehren
    // Lesen: [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // Position des ersten Elements: vollständig sichtbar oder nicht
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // Y-Versatz dieses Elements relativ zum oberen Rand von ListView
    // misst die Höhe des möglicherweise verdeckten Teils
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // All dies wird gespeichert
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }

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

  @Override
  protected void initFragment(CoreState previousState) {
    // Erster Besuch?
    if (previousState != null) {
      // Nicht der erste Besuch
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // und die Informationen aus ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // Wir rufen den Kalender ab
    agenda = session.getAgenda();
    // Wir generieren den Seitentitel
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // Menüstatus
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Der Seitentitel wird neu generiert
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }

  @Override
  protected void notifyEndOfUpdates() {
    // Die Liste der Zeitfenster wird neu generiert
    updateAgenda();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // Menüstatus
    initMenu();
    // Bei einer Stornierung, bei der der Termin bereits gelöscht wurde, muss der lokale Kalender aktualisiert werden
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // Der Termin wird im lokalen Kalender gelöscht (der globale Kalender war nicht verfügbar)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // Die Benutzeroberfläche wird aktualisiert
      updateAgenda();
    }
  }


  // Private Methoden ------------------------------------------------
  private void initMenu() {
    // Menüstatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • Zeilen 2–19: Auf Anforderung durch seine übergeordnete Klasse speichert das Fragment den Status der folgenden Elemente:
    • Zeile 6: den oben in der Ansicht angezeigten Titel;
    • Zeilen 7–17: die Informationen (top, firstPosition), die es ermöglichen, das scrolling des ListView wiederherzustellen;
  • Zeilen 21–24: Die Nummer des Fragments lautet [IMainActivity.VUE_AGENDA];
  • Zeilen 26–35: werden ausgeführt, wenn das Fragment zum ersten Mal generiert wird (previousState == null) oder bei den folgenden Generierungen neu generiert wird (previousState != null);
    • Zeilen 30–34: Wenn es sich nicht um den ersten Aufruf des Fragments handelt, werden die Informationen (top, firstPosition) abgerufen, mit denen das scrolling aus dem ListView wiederhergestellt werden kann;
  • Zeilen 38–40: werden ausgeführt, wenn die dem Fragment zugeordnete Ansicht zum ersten Mal erstellt wird (previousState == null) oder bei den folgenden Malen neu erstellt wird (previousState != null). Hier ist nichts zu tun, da das ListView der Zeitfenster von der privaten Methode [updateAgenda] (Zeilen 61–65) generiert wird;
  • Zeilen 42–52: werden ausgeführt, wenn man über eine Operation [SUBMIT] zu diesem Fragment gelangt. Man kommt dann aus der Ansicht [ACCUEIL];
    • Zeile 45: Der von [AccueilFragment] in die Sitzung geladene Kalender wird abgerufen;
    • Zeilen 47–49: Der Titel der Ansicht wird generiert;
    • die ListView für die Termine wird durch die private Methode [updateAgenda] generiert (Zeilen 61–65);
  • Zeilen 54–59: werden ausgeführt, wenn man über eine Operation [NAVIGATION] oder [RESTORE] zu diesem Fragment gelangt;
    • Zeilen 57–58: Der Titel der Ansicht wird neu generiert;
    • das ListView der Zeitfenster wird durch die private Methode [updateAgenda] (Zeilen 61–65) generiert;
  • Zeilen 72–74: werden ausgeführt, sobald alle vorherigen Aktualisierungen abgeschlossen sind. Das ListView der Zeitfenster wird aktualisiert, da diese Aktualisierung unabhängig davon erforderlich ist, wie man zum Fragment gelangt;
  • Zeilen 67–77: werden ausgeführt, sobald alle asynchronen Aufgaben abgeschlossen sind;
    • Zeile 70: Das Menü wird in seinen Standardzustand zurückgesetzt (Zeilen 82–86);
    • Zeile 72: Es gab zwei asynchrone Aufgaben. Es wird geprüft, ob die erste (das Löschen des Termins) trotz einer Stornierung erfolgreich war;
    • Zeile 74: Wenn ja, wird der Termin aus dem lokalen Kalender gelöscht
    • Zeile 75: und die Anzeige des Termins wird aktualisiert;

3.6.9. Verwaltung der Ansicht zum Hinzufügen eines Termins

3.6.9.1. Die Ansicht

Die Ansicht zum Hinzufügen eines Termins sieht wie folgt aus:

Image

Die Elemente der Benutzeroberfläche sind folgende:

Type
Nom
1
TextView
txtTitre2
2
Spinner
spinnerClients

3.6.9.2. Das Fragment

Die Ansicht zum Hinzufügen eines Termins wird durch das folgende Fragment [AjoutRvFragment] verwaltet:

 

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 {

  // Elemente der Benutzeroberfläche
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;

  // die Kunden
  private List<Client> clients;

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

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

  // Implementierung der Methoden der übergeordneten Klasse ----------------------------------
...
}
  • Zeile 26: Das Fragment ist mit dem folgenden Menü [menu_ajout_rv] verknüpft:
  

<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>
  • Zeilen 30–33: die Elemente der Benutzeroberfläche;
  • Zeile 36: die Kundenliste;
  • Zeile 43: die Datenquelle für den Kunden-Spinner;

Der Klick auf den Link [Valider] wird durch die folgende Methode [doValider] verarbeitet:


  // die Clients
  private List<Client> clients;

  // lokale Daten
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// Validierung der Seite
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // Der ausgewählte Client wird abgerufen
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // Beginn des Wartens auf 2 asynchrone Aufgaben
    beginWaiting(2);
    // Hinzufügen von RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {

        @Override
        public void call(Response<Rv> responseRv) {
          // Die Antwort wird verarbeitet
          consumeRv(responseRv);
        }
      });
  }

  // Verarbeitung eines Response<Rv>-Objekts
  void consumeRv(Response<Rv> responseRv) {
    // Fehler?
    if (responseRv.getStatus() != 0) {
      // Meldung
      showAlert(responseRv.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Es wird vermerkt, dass der Termin hinzugefügt wurde
    rdvAjouté = true;
    // Der Termin wird gespeichert
    this.rv = responseRv.getBody();
    // Der neue Terminkalender wird angefordert
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // die Antwort wird verarbeitet
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  // Verarbeitung eines Objekts vom Typ „Response<AgendaMedecinJour>“
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // Fehler?
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // Meldung
      showAlert(responseAgendaMedecinJour.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zu UI
      return;
    }
    // Der Terminkalender wird in die Sitzung aufgenommen
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • Zeile 13: Zu Beginn der Methode [doValider] wurden die Felder 2, 5, 6 und 9 während des Lebenszyklus des Fragments initialisiert. Wir werden sehen, wie;
  • Zeile 15: Es wird die Entität [Client] abgerufen, die dem im Kunden-Spinner ausgewählten Element entspricht;
  • Zeile 17: Der übergeordneten Klasse wird mitgeteilt, dass zwei asynchrone Aufgaben gestartet werden sollen, und die Wartezeit wird vorbereitet;
  • Zeile 19: Zu Beginn ist der Termin noch nicht im Kalender des Arztes eingetragen;
  • Zeilen 20–30: Der Server wird aufgefordert, einen Termin hinzuzufügen;
    • Zeile 20: Die Methode [executeInBackground] erwartet zwei Parameter:
      • Zeile 20: Der auszuführende und zu beobachtende Prozess wird von der Methode [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())] bereitgestellt;
      • Zeilen 22–29: Der zweite Parameter ist eine Instanz vom Typ [Action1<T>], wobei T der vom beobachteten Prozess zurückgegebene Typ ist, hier [Response<Rv>]
    • Zeile 27: Sobald die Antwort empfangen wird, wird sie an die Methode [consumeRV] in Zeile 33 übergeben;
  • Zeilen 33–56: Die Antwort vom Server wurde empfangen. Sie wird ausgewertet;
    • Zeilen 35–42: Zunächst wird geprüft, ob der Server im Feld [status] der Antwort einen Fehler gemeldet hat;
    • Zeile 37: Liegt ein Fehler vor, werden die Meldungen angezeigt, die der Server im Feld [messages] der Antwort hinterlegt hat;
    • Zeile 39: Alle Aufgaben werden abgebrochen;
    • Zeile 41 : Man kehrt zur Benutzeroberfläche zurück;
    • Zeile 44: Wenn kein Fehler aufgetreten ist, wird vermerkt, dass der Termin hinzugefügt wurde;
    • Zeile 46: Der hinzugefügte Termin wird in einem Feld des Fragments gespeichert;
    • Zeilen 47–55: Wie bereits beim Löschen eines Termins wird nach dem Hinzufügen des Termins der aktuellste Terminkalender des Arztes vom Server angefordert;
  • Zeilen 47–56, 59–71: Hier handelt es sich um einen Code, der bereits mehrfach vorkam;

Die Methode [beginWaiting] (Zeile 17) lautet wie folgt:


  // Wartephase beginnt
  protected void beginWaiting(int numberOfRunningTasks) {
    // Vorbereitung des Starts der Aufgaben
    beginRunningTasks(numberOfRunningTasks);
    // Status der Schaltflächen und Menüs
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • Zeile 4: Der übergeordneten Aufgabe wird mitgeteilt, dass die Aufgabe [numberOfRunningTasks] gestartet wird;
  • Zeile 6: Alle Menüoptionen werden ausgeblendet;
  • Zeile 7: Anschließend wird die Option [Actions/Annuler] sichtbar gemacht;

Der Klick auf die Menüoption [Annuler] wird von der Methode [doAnnuler] verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // Die asynchronen Aufgaben werden abgebrochen
    cancelRunningTasks();
}
  • Zeile 7: Die übergeordnete Klasse wird aufgefordert, die asynchronen Aufgaben abzubrechen;

Die Zurück-Navigation wird durch die folgenden drei Methoden gewährleistet:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // Wechsel zur Konfigurationsansicht
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // Wechsel zur Konfigurationsansicht
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
    // Wechsel zur Kalenderansicht
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. Verwaltung des Lebenszyklus des Fragments

Das Fragment hat den folgenden Status [AjoutRvFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

// Status des Fragments AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {

  // Position des ausgewählten Kunden
  private int selectedClientPosition;
  // Titel der Ansicht
  private String titre;
  // Datenquelle des Kunden-Spinners
  private String[] spinnerClientsDataSource;

  // Getter und Setter
...
}

Der Lebenszyklus des Fragments ist wie folgt implementiert:


// Implementierung der Methoden der übergeordneten Klasse ----------------------------------
  @Override
  public CoreState saveFragment() {
    // Ansicht speichern
    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) {
    // Kunden aus der Sitzung abrufen
    clients = session.getClients();
    // Erster Besuch?
    if (previousState == null) {
      // Erstellung des vom Spinner angezeigten Arrays
      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 {
      // kein erster Besuch
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
    // Zuordnung des Spinners zu seiner Datenquelle
    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);
    // Erster Besuch?
    if (previousState == null) {
      // Menü
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // Die Nummer des zu reservierenden Termins wird in der Sitzung abgerufen
    int position = session.getPosition();
    // Der Terminkalender des Arztes wird in der Sitzung abgerufen
    AgendaMedecinJour agenda = session.getAgenda();
    // Abruf des Arztes und des Termins, für den ein Termin vereinbart werden soll
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // Erstellung von Titel 2 der Seite
    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()));
    // Kundenauswahl
    spinnerClients.setSelection(0);
    // Menü
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Vorherigen Zustand wiederherstellen
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // Überschrift
    txtTitre2.setText(state.getTitre());
    // Ladebalken
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // Menü-Status
    initMenu();
    // Nächste Ansicht?
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // Es gab eine Stornierung – Termin bereits hinzugefügt?
    if (rdvAjouté) {
      // Der lokale Kalender wird bearbeitet (der globale Kalender war nicht verfügbar)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // Der Kalender wird angezeigt
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }

  // private Methoden -------------------
  private void initMenu() {
    // Menüstatus
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  • Zeilen 2–10: Auf Anforderung seiner übergeordneten Klasse speichert das Fragment den Status der folgenden Elemente:
    • Zeile 6: den Titel oben in der Ansicht;
    • Zeile 7: die Position des ausgewählten Elements im Kunden-Spinner;
    • Zeile 8: die Datenquelle des Kunden-Spinners;
  • Zeilen 12–15: Die Nummer des Fragments lautet [IMainActivity.VUE_AJOUT_RV];
  • Zeilen 17–35: werden ausgeführt, wenn das Fragment zum ersten Mal generiert wird (previousState == null) oder bei den folgenden Generierungen neu generiert wird (previousState != null);
    • Zeile 20: Die Liste der Kunden aus der Sitzung wird abgerufen und in ein Feld des Fragments übernommen;
    • Zeilen 22–30: Bei einem ersten Besuch wird die Datenquelle für den Kunden-Spinner erstellt;
    • Zeilen 32–33: Bei weiteren Besuchen wird die Datenquelle für den Kunden-Spinner aus dem vorherigen Zustand des Fragments abgerufen;
  • Zeilen 37–49: werden ausgeführt, wenn die dem Fragment zugeordnete Ansicht zum ersten Mal erstellt wird (previousState == null) oder bei den folgenden Malen neu erstellt wird (previousState != null);
    • Zeilen 40–43: In jedem Fall wird der Kunden-Spinner mit seiner Datenquelle verknüpft;
    • Zeilen 45–48: Beim ersten Besuch wird das Menü ohne die Aktion [Annuler] (Zeilen 107–111) angezeigt;
  • Zeilen 51–70: werden ausgeführt, wenn man über eine Operation [SUBMIT] zum Fragment gelangt. Man kommt dann aus der Ansicht [AGENDA];
    • Zeile 54: Die Nummer des Zeitfensters wird abgerufen, in das ein Termin eingefügt werden soll;
    • Zeilen 56–59: Die für das Hinzufügen dieses Termins erforderlichen Entitäten [Medecin] und [Creneau] werden abgerufen und in Felder des Fragments eingefügt;
    • Zeilen 61–65: Mit diesen Informationen kann der Titel der Ansicht erstellt werden;
    • Zeile 67: Der Spinner für die Kunden wird auf sein erstes Element gesetzt;
    • Zeile 69: Das Menü wird in seinen Ausgangszustand versetzt (ohne die Option [Annuler]);
  • Zeilen 72–80: werden ausgeführt, wenn das Fragment über eine Operation [NAVIGATION] oder [RESTORE] aufgerufen wird;
    • Zeile 77: Der Titel der Ansicht wird neu generiert;
    • Zeile 79: Der Spinner für die Kunden wird auf den zuletzt ausgewählten Kunden zurückgesetzt;
  • Zeilen 82–84: werden ausgeführt, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts mehr zu tun;
  • Zeilen 86–104: werden ausgeführt, wenn alle asynchronen Aufgaben abgeschlossen sind;
    • Zeile 89: Das Menü wird in seinen Standardzustand zurückgesetzt;
    • Zeilen 91–94: Wenn die Aufgaben normal abgeschlossen wurden, kehrt man über einen [SUBMIT] zur Ansicht [AGENDA] zurück (hier hätte es sich auch um eine Aktion vom Typ NAVIGATION handeln können);
    • Zeilen 96–103: Wenn die Aufgaben mit einem Abbruch beendet wurden, wird dennoch geprüft, ob der Termin hinzugefügt wurde (dies würde bedeuten, dass das Abrufen des neuen Kalenders fehlgeschlagen ist);
    • Zeilen 98–99: Wenn der Termin hinzugefügt wurde;
      • Zeilen 98–99: Der vom Server zurückgegebene Termin wird zum aktuellen Kalender hinzugefügt, also zu dem, der gerade in der Sitzung geöffnet ist;
      • Zeile 101: Man kehrt über einen [SUBMIT] zur Ansicht [AGENDA] zurück (hier hätte es sich auch um eine Aktion vom Typ NAVIGATION handeln können);

3.7. Exécution

Führen Sie folgende Tests durch:

  • Verwenden Sie die Anwendung unter normalen Bedingungen und überprüfen Sie, ob sie funktioniert;
  • Drehen Sie das Gerät für jede der Ansichten und überprüfen Sie, ob jede korrekt wiederhergestellt wird;
  • Legen Sie in [IMainActivity] eine Wartezeit von einigen Sekunden fest;
  • brechen Sie anschließend die Aufgaben ab und überprüfen Sie, ob das Ergebnis den Erwartungen entspricht;
  • Drehen Sie das Gerät während der Wartezeiten und überprüfen Sie, ob die Aufgaben tatsächlich abgebrochen werden und es zu keinem Absturz kommt;
  • Ändern Sie die Anordnung der Fragmente in [IMainActivity] und überprüfen Sie, ob die Anwendung weiterhin funktioniert;