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

Ansicht zur Auswahl des Arztes und des Termins

Ansicht zur Auswahl des Terminzeitfensters

Ansicht zur Auswahl des Kunden für den Termin

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

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

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:
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:
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:
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:
3.5.4. Kundenliste
| |
|
Beispiel:
3.5.5. Liste der Sprechzeiten eines Arztes
|
- [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:
3.5.6. Liste der Arzttermine
|
- [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:
3.5.7. Der Terminkalender eines Arztes
|
- [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:
|
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
|
- [idMedecin]: die Identifikationsnummer des Arztes;
Beispiel 1:
Beispiel 2:
3.5.9. Einen Kunden anhand seiner ID abrufen
|
- [idClient]: die Kunden-ID;
Beispiel 1:
Beispiel 2:
3.5.10. Einen Termin anhand seiner ID abrufen
|
- [idCreneau]: die ID des Zeitfensters;
Beispiel 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:
3.5.11. Termin über die ID vereinbaren
|
- [idRv]: die Termin-ID;
Beispiel 1:
Es ist zu beachten, dass in der Antwort weder der Kunde noch der Terminzeitraum angegeben sind, sondern lediglich deren Identifikationsnummern.
Beispiel 2:
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.

- 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:
|
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:
Der Termin mit der Nummer 209 wurde tatsächlich gelöscht, da [status=0].
Beispiel 2:
3.6. Der Android-Client

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:

Die Elemente der Benutzeroberfläche sind folgende:
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:

Die Elemente der Benutzeroberfläche sind folgende:
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;
- Zeile 15: Die Methode [executeInBackground] erwartet zwei Parameter:
- 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:

Die Elemente der Benutzeroberfläche sind folgende:
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:

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:
- die aktuelle Android-Aktivität,
- die Datei XML, die den Inhalt jedes Elements von [ListView] definiert,
- das Array mit den Terminfenstern des Arztes,
- 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:
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:
- die Nummer des angeklickten Termins,
- 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;
- Zeile 10: Die Methode [executeInBackground] erwartet zwei Parameter:
- 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:

Die Elemente der Benutzeroberfläche sind folgende:
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;
- Zeile 20: Die Methode [executeInBackground] erwartet zwei Parameter:
- 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;












































