3. Studium przypadku – Zarządzanie terminami spotkań
3.1. Projekt
W dokumencie [Tutoriel AngularJS / Spring 4] opracowano aplikację klient-serwer do zarządzania wizytami u lekarzy. W dalszej części będziemy odnosić się do tego dokumentu jako [rdvmedecins-angular]. Aplikacja miała dwa typy klientów:
- klienta HTML / CSS / JS;
- klienta na Androida;
Klient na Androida był generowany automatycznie na podstawie wersji klienta HTML za pomocą narzędzia [Cordova]. Celem tego projektu będzie ręczne odtworzenie tego klienta na Androida, wykorzystując wiedzę zdobytą w poprzednich rozdziałach.
Należy zwrócić uwagę na istotną różnicę między tymi dwoma rozwiązaniami:
- ten, który zamierzamy stworzyć, będzie działał wyłącznie na tabletach z systemem Android;
- w wersji [rdvmedecins-angular] mobilna aplikacja internetowa (HTML / CSS / JS) działa na dowolnej platformie (Android, IoS, Windows);
3.2. Widoki klienta na Androida
Dostępne są cztery widoki.
Widok konfiguracji

Ekran wyboru lekarza i daty wizyty

Ekran wyboru przedziału czasowego wizyty

Widok wyboru pacjenta na wizytę

3.3. Architektura projektu
Architektura klienta/serwera będzie analogiczna do tej przedstawionej w przykładzie [Exemple-15] (patrz punkt 1.16) niniejszego dokumentu:

Wymiana danych asynchroniczna między klientem a serwerem będzie obsługiwana za pomocą biblioteki RxAndroid.
3.4. Baza danych
Nie odgrywa ona zasadniczej roli w niniejszym dokumencie. Podajemy ją wyłącznie w celach informacyjnych. Nazwiemy ją [dbrdvmedecins] . Jest to baza danych MySQL5 zawierająca cztery tabele:
![]() |
3.4.1. Tabela [MEDECINS]
Zawiera informacje o lekarzach zarządzanych przez aplikację [RdvMedecins].
![]() | ![]() |
- ID: numer identyfikacyjny lekarza – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko lekarza
- PRENOM: jego imię
- TITRE: jego tytuł (panna, pani, pan)
3.4.2. Tabela [CLIENTS]
Pacjenci poszczególnych lekarzy są zapisani w tabeli [CLIENTS]:
![]() | ![]() |
- ID: numer identyfikacyjny pacjenta – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko klienta
- PRENOM: imię klienta
- TITRE: tytuł (Panna, Pani, Pan)
3.4.3. Tabela [CRENEAUX]
Zawiera listę przedziałów czasowych, w których możliwe jest użycie RV:
![]() |
![]() | ![]() | ![]() |
- ID: numer identyfikujący przedział czasowy – klucz główny tabeli (wiersz 8)
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS (ID).
- HDEBUT: godzina rozpoczęcia terminu
- MDEBUT: minuty początku przedziału czasowego
- HFIN: godzina zakończenia przedziału czasowego
- MFIN: minuty zakończenia przedziału czasowego
Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).
3.4.4. Tabela [RV]
Zawiera listę RV przypisanych każdemu lekarzowi:
![]() | ![]() |
- ID: numer jednoznacznie identyfikujący RV – klucz główny
- JOUR: dzień, na który przypada RV
- ID_CRENEAU: przedział czasowy dla RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
- ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]
Ta tabela posiada , która wymusza unikalność wartości połączonych kolumn (JOUR, ID_CRENEAU):
Jeśli wiersz w tabeli [RV] ma wartość (JOUR1, ID_CRENEAU1) w kolumnach (JOUR, ID_CRENEAU), to wartość ta nie może występować nigdzie indziej. W przeciwnym razie oznaczałoby to, że dwa rekordy o wartości RV zostały zarejestrowane w tym samym czasie dla tego samego lekarza. Z punktu widzenia programowania w Javie sterownik bazy danych o wartości JDBC uruchamia sterownik o wartości SQLException, gdy wystąpi taka sytuacja.
Wiersz o wartości id równej 3 (por. [1] powyżej) oznacza, że rezerwacja o numerze RV została dokonana na przedział czasowy nr 20 dla klienta nr 4 w dniu 23.08.2006 r. Z tabeli [CRENEAUX] wynika, że przedział nr 20 odpowiada przedziałowi czasowemu 16:20–16:40 i należy do lekarza nr 1 (pani Marie PELISSIER). Z tabeli [CLIENTS] wynika, że klient nr 4 to panna Brigitte BISTROU.
3.4.5. Tworzenie bazy danych
Aby utworzyć tabele i wypełnić je danymi, można skorzystać ze skryptu [dbrdvmedecins.sql], który znajduje się w archiwum przykładów |ICI|.
![]() |
Korzystając ze skryptu [WampServer] (patrz punkt 6.15), można postępować w następujący sposób:
![]() | ![]() |
- w [1] należy kliknąć ikonę [WampServer] i wybrać opcję [PhpMyAdmin] [2],
- w [3], w oknie, które się otworzyło, należy wybrać link [Bases de données],
![]() |
- do [4-6], importujemy plik SQL,
![]() | ![]() | ![]() |
- w pliku [7] wybieramy skrypt SQL, a w pliku [8] uruchamiamy go,
- w pliku [9] utworzono tabele bazy danych. Klikamy jeden z linków,
![]() |
- w [10], zawartość tabeli.
W dalszej części nie będziemy już wracać do tej bazy danych, ale zachęcamy czytelnika do śledzenia jej zmian w trakcie testów, zwłaszcza gdy aplikacja nie działa.
3.5. Serwer WWW / jSON

W tym miejscu skupiamy się na serwerze [1]. Nie będziemy go tutaj omawiać. Został on szczegółowo opisany w dokumencie [Spring MVC et Thymeleaf par l'exemple]. Zainteresowany czytelnik może się z nim zapoznać. Został on opracowany tak samo jak serwer z przykładu 15. Jego kod źródłowy znajduje się w przykładach. W tym miejscu wykorzystamy jego plik binarny:
![]() |
- [rdvmedecins-server-all-1.0.jar] to plik binarny serwera;
3.5.1. Wdrożenie
W oknie poleceń należy przejść do folderu zawierającego plik binarny serwera:
...\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
następnie, aby uruchomić serwer, należy wpisać następujące polecenie (serwer SGBD MySQL musi być już uruchomiony):
...\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
Serwer wyświetla wiele wpisów w dzienniku. Powyżej uwzględniliśmy tylko te, które są przydatne do zrozumienia:
- wiersze 14–18: na porcie 8080 komputera uruchomiono wbudowany serwer Tomcat. To właśnie ten serwer uruchamia aplikację internetową do zarządzania terminami. Aplikacja ta jest w rzeczywistości usługą internetową / jSON: jest ona wywoływana za pomocą URL i odpowiada, wysyłając ciąg znaków jSON;
- wiersz 24: usługa internetowa jest zabezpieczona za pomocą frameworl [Spring Security]. Dostęp do URL usługi internetowej uzyskuje się poprzez uwierzytelnienie;
- wiersze 29–44: URL udostępniane przez serwis internetowy;
Omówimy je szczegółowo.
3.5.2. Zabezpieczenie serwisu internetowego
Pliki URL udostępniane przez serwis internetowy są zabezpieczone. Serwer oczekuje w żądaniu HTTP od klienta następującego nagłówka:
Oczekiwanym kodem jest zakodowana w base64 wartość [http://fr.wikipedia.org/wiki/Base64] ciągu znaków „użytkownik:hasło”. W stanie początkowym serwis internetowy akceptuje wyłącznie użytkownika „admin” z hasłem „admin”. Powyższy nagłówek dla tego konkretnego użytkownika przyjmuje następującą postać:
Aby móc wysłać ten nagłówek HTTP, korzystamy z klienta HTTP [Advanced Rest Client], który jest wtyczką przeglądarki Chrome (patrz punkt 6.13). Ręcznie przetestujemy różne URL udostępniane przez serwis internetowy, aby zrozumieć:
- jakich parametrów oczekuje usługa URL;
- jakiego rodzaju jest jego odpowiedź;
3.5.3. Lista lekarzy
URL [/getAllMedecins] umożliwia uzyskanie listy lekarzy:
![]() |
- w [1], zapytanie URL;
- w [2] – metoda HTTP użyta do tego zapytania;
- w [3] – nagłówek bezpieczeństwa użytkownika (admin, admin) o numerze HTTP;
- w [4] wysyłane jest zapytanie HTTP;
Odpowiedź serwera jest następująca:
![]() |
- w [5] znajduje się sformatowana odpowiedź serwera jSON;
![]() |
- w [6], ta sama odpowiedź w postaci surowej;
Format [5] pozwala lepiej dostrzec strukturę odpowiedzi. Wszystkie odpowiedzi serwisu internetowego są instancjami następującej klasy [Response]:
package rdvmedecins.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
// konstruktory
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// metody pobierające i ustawiające
...
}
- wiersz 9: status odpowiedzi. Wartość 0 oznacza, że nie wystąpił błąd, w przeciwnym razie błąd wystąpił;
- wiersz 11: lista komunikatów o błędach, jeśli wystąpił błąd;
- wiersz 13: rzeczywista odpowiedź oczekiwana przez klienta;
Odpowiedzią na URL [/getAllMedecins] jest ciąg znaków jSON obiektu typu [Response<List<Medecin>>]. Klasa [Medecin] ma następującą postać:
package rdvmedecins.android.dao.entities;
public class Medecin extends Personne {
// konstruktor domyślny
public Medecin() {
}
// konstruktor z parametrami
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
W wierszu 3 klasa [Medecin] rozszerza następującą klasę [Personne]:
package rdvmedecins.android.dao.entities;
public class Personne extends AbstractEntity {
// atrybuty osoby
private String titre;
private String nom;
private String prenom;
// konstruktor domyślny
public Personne() {
}
// konstruktor z parametrami
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);
}
// metody pobierające i ustawiające
...
}
W wierszu 3 klasa [Personne] rozszerza następującą klasę [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;
}
// inicjalizacja
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;
}
// metody pobierające i ustawiające
...
}
Ostatecznie struktura obiektu [Medecin] wygląda następująco:
[Long id; Long version; String titre; String nom; String prenom;]
a obiektu [Response<List<Medecin>>] wygląda następująco:
W dalszej części będziemy używać tych skróconych definicji do opisania odpowiedzi serwera. Ponadto przez pewien czas nie będziemy już pokazywać zrzutów ekranu. Wystarczy powtórzyć to, co właśnie omówiliśmy. Powrócimy do zrzutów ekranu, gdy zajdzie potrzeba wykonania zapytania POST. Przedstawimy również przykład wykonania w następującej postaci:
3.5.4. Lista klientów
| |
|
Przykład:
3.5.5. Lista terminów wizyt u lekarza
|
- [idMedecin]: identyfikator lekarza, którego terminy wizyt są potrzebne;
- [hdebut]: godzina rozpoczęcia wizyty;
- [mdebut]: minuty rozpoczęcia wizyty;
- [hfin]: godzina zakończenia wizyty;
- [mfin]: minuty zakończenia wizyty;
W przypadku przedziału czasowego między 10:20 a 10:40 otrzymamy [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].
Przykład:
3.5.6. Lista wizyt u lekarza
|
- [idMedecin]: identyfikator lekarza, którego terminy wizyt są potrzebne;
- URL [jour]: dzień wizyt w formacie „rrrr-mm-dd”;
- Odpowiedź [jour]: to samo, ale w formacie daty Java;
- [client]: klient, dla którego przeznaczona jest wizyta. Jego struktura została opisana wcześniej;
- [idClient]: identyfikator klienta;
- [creneau]: przedział czasowy spotkania. Jego struktura została opisana wcześniej;
- [idCreneau]: identyfikator przedziału czasowego;
Przykład:
3.5.7. Kalendarz lekarza
|
- [idMedecin]: identyfikator lekarza, którego terminy wizyt są potrzebne;
- URL [jour]: dzień wizyt w formacie „rrrr-mm-dd”;
- [agenda]: kalendarz lekarza;
- [medecin]: dany lekarz. Jego struktura została zdefiniowana wcześniej;
- Odpowiedź [jour]: dzień z terminarza w formacie daty Java;
- [creneauxMedecinJour]: tablica elementów typu [CreneauMedecinJour];
- [creneau]: przedział czasowy. Jego struktura została opisana wcześniej;
- [rv]: spotkanie. Jego struktura została opisana wcześniej;
Przykład:
|
Wyróżniono przypadek, w którym w danym przedziale czasowym jest wizyta, oraz przypadek, w którym jej nie ma.
3.5.8. Wyszukanie lekarza na podstawie identyfikatora
|
- [idMedecin]: identyfikator lekarza;
Przykład 1:
Przykład 2:
3.5.9. Pobierz klienta na podstawie jego identyfikatora
|
- [idClient]: identyfikator klienta;
Przykład 1:
Przykład 2:
3.5.10. Pobieranie terminu na podstawie identyfikatora
|
- [idCreneau]: identyfikator terminu;
Przykład 1:
Należy zauważyć, że w odpowiedzi nie podano nazwiska lekarza, który jest właścicielem terminu, a jedynie jego identyfikator.
Przykład 2:
3.5.11. Umów się na wizytę, podając swój identyfikator
|
- [idRv]: identyfikator wizyty;
Przykład 1:
Warto zauważyć, że w odpowiedzi nie ma ani nazwy klienta, ani terminu spotkania, a jedynie ich identyfikatory.
Przykład 2:
3.5.12. Dodaj spotkanie
URL [/ajouterRv] umożliwia dodanie spotkania. Informacje niezbędne do dodania spotkania (dzień, przedział czasowy i klient) są przekazywane za pomocą żądania HTTP POST. Pokazujemy, jak wykonać to żądanie za pomocą narzędzia [Advanced Rest Client].

- w [1], zapytanie URL;
- w [2] jest ona wywoływana przez POST;
- w [3-4] informuje się serwer, że wartości przesyłane do niego mają postać ciągu znaków jSON;
- w [4] – nagłówek uwierzytelniający HTTP;
- w [5] – informacje przekazane przez POST. Jest to ciąg znaków jSON zawierający:
- [jour]: dzień spotkania w formacie „aaaa-mm-dd”,
- [idClient]: identyfikator pacjenta, dla którego umówiono wizytę,
- [idCreneau]: identyfikator przedziału czasowego wizyty. Ponieważ przedział czasowy należy do konkretnego lekarza, określa on również lekarza;
- w [6] wysyłane jest żądanie;
Wysyłany ciąg znaków jSON odnosi się do następującego obiektu typu [PostAjouterRv]:
public class PostAjouterRv {
// dane wpisu
private String jour;
private long idClient;
private long idCreneau;
// konstruktory
public PostAjouterRv() {
}
public PostAjouterRv(String jour, long idCreneau, long idClient) {
this.jour = jour;
this.idClient = idClient;
this.idCreneau = idCreneau;
}
// metody pobierające i ustawiające
...
}
Odpowiedź serwera ma typ [Response<Rv>] [int status; List<String> messages; Rv rv], gdzie [rv] to dodane spotkanie.
Odpowiedź serwera na powyższe żądanie jest następująca:
![]() |
Należy zauważyć, że niektóre informacje nie są podane w polu [idClient, idCreneau], ale znajdują się w polach [client] i [creneau]. Istotną informacją jest identyfikator dodanego terminu (209). Serwis internetowy mógłby po prostu zwrócić tylko tę jedną informację.
3.5.13. Usunięcie spotkania
Operacja ta jest również realizowana za pomocą kodu POST:
|
Wartością przesłaną jest ciąg znaków jSON z następującego obiektu typu [PostSupprimerRv]:
public class PostSupprimerRv {
// dane z postu
private long idRv;
// konstruktory
public PostSupprimerRv() {
}
public PostSupprimerRv(long idRv) {
this.idRv = idRv;
}
// metody pobierające i ustawiające
...
}
- w wierszu 4, [idRv] to identyfikator spotkania, które ma zostać usunięte.
Przykład 1:
Spotkanie nr 209 zostało rzeczywiście usunięte, ponieważ [status=0].
Przykład 2:
3.6. Klient na Androida

Teraz, gdy serwer [1] został szczegółowo opisany i działa, przyjrzymy się klientowi na Androida [2].
3.6.1. Architektura projektu w Android Studio
Projekt opiera się na architekturze projektu [client-android-skel] (patrz punkt 1.17). W powyższej architekturze klienta na Androida wyróżniamy trzy bloki:
- warstwę [DAO] odpowiedzialną za komunikację z serwisem internetowym;
- warstwy [vues] odpowiedzialne za komunikację z użytkownikiem;
- [activité], która łączy dwa poprzednie bloki. Widoki nie mają dostępu do warstwy [DAO]. Komunikują się wyłącznie z aktywnością.
Architektura ta znajduje odzwierciedlenie w strukturze projektu Android Studio dla klienta Android:
![]() |
- pakiet [activity] implementuje aktywność;
- pakiet [architecture] zawiera elementy architektury, które opracowaliśmy wcześniej;
- pakiet [dao] implementuje warstwę [DAO];
- pakiet [fragments] implementuje pakiet [vues];
3.6.2. Dostosowanie projektu
![]() |
Folder [architecture / custom] zawiera elementy architektury, które można dostosować.
Interfejs [IMainActivity] wygląda następująco:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// dostęp do sesji
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// obsługa oczekiwania
void beginWaiting();
void cancelWaiting();
// stałe aplikacji -------------------------------------
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 000;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = false;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = true;
// liczba fragmentów aplikacji
int FRAGMENTS_COUNT = 4;
// liczba wyświetleń
int VUE_CONFIG = 0;
int VUE_ACCUEIL = 1;
int VUE_AGENDA = 2;
int VUE_AJOUT_RV = 3;
}
- wiersze 25, 28: dostosowanie warstwy [DAO];
- wiersz 31: ta aplikacja nawiązuje uwierzytelnione połączenia z serwerem;
- wiersz 40: potrzebny jest obrazek oczekiwania;
- wiersz 43: aplikacja składa się z czterech fragmentów;
- wiersze 46–49: numery czterech fragmentów;
- wiersz 37: nie ma zakładek;
Klasa bazowa [CoreState] stanów fragmentów będzie wyglądać następująco:
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 odwiedzony lub nie
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersze 15–18: wszystkie cztery fragmenty mają status:
![]() |
Wreszcie sesja zawiera dane wspólne dla fragmentów:
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 {
// elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
// lista lekarzy
private List<Medecin> médecins;
// lista klientów
private List<Client> clients;
// kalendarz lekarza na dany dzień
private AgendaMedecinJour agenda;
// pozycja klikniętego elementu w terminarzu
private int position;
// dzień wizyty w formacie angielskim „yyyy-MM-dd”
private String dayRv;
// dzień wizyty w notacji francuskiej „dd-MM-yyyy”
private String jourRv;
// metody pobierające i ustawiające
...
}
- wiersze 17–28: sesja przechowuje sześć informacji. Wyjaśnimy ich rolę w odpowiednim momencie.
3.6.3. Warstwa [DAO]
![]() |
![]() | ![]() |
- w [1] – elementy zawarte w odpowiedziach serwera. Zostały one przedstawione w punkcie 3.5;
- w [2] – elementy klienta zarządzające komunikacją z serwerem;
Nie będziemy ponownie omawiać elementów [1]. Zostały one już omówione. W razie potrzeby czytelnik może powrócić do punktu 3.5. Przeanalizujemy implementację pakietu [service]. W ten sposób omówimy również implementację bezpiecznej komunikacji między klientem a serwerem.
3.6.3.1. Implementacja komunikacji klient–serwer
![]() |
Klasa [WebClient] jest komponentem AA, który opisuje:
- komponenty URL udostępniane przez serwis internetowy;
- ich parametry;
- ich odpowiedzi;
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);
// lista lekarzy
@Get("/getAllMedecins")
public Response<List<Medecin>> getAllMedecins();
// lista klientów
@Get("/getAllClients")
public Response<List<Client>> getAllClients();
// lista terminów u lekarza
@Get("/getAllCreneaux/{idMedecin}")
public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);
// lista wizyt u lekarza
@Get("/getRvMedecinJour/{idMedecin}/{jour}")
public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);
// Klient
@Get("/getClientById/{id}")
public Response<Client> getClientById(@Path long id);
// Lekarz
@Get("/getMedecinById/{id}")
public Response<Medecin> getMedecinById(@Path long id);
// Wizyta
@Get("/getRvById/{id}")
public Response<Rv> getRvById(@Path long id);
// Przedział czasowy
@Get("/getCreneauById/{id}")
public Response<Creneau> getCreneauById(@Path long id);
// dodać RV
@Post("/ajouterRv")
public Response<Rv> ajouterRv(@Body PostAjouterRv post);
// usunąć wizytę
@Post("/supprimerRv")
public Response<Rv> supprimerRv(@Body PostSupprimerRv post);
// pobierz harmonogram wizyt lekarza
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
}
- wiersze 19–60: znajdują się tu wszystkie elementy URL omówione w punkcie 3.5;
- wiersz 16: komponent [RestTemplate] z [Spring Android], na którym opiera się komunikacja klient–serwer;
3.6.3.2. Interfejs [IDao]
![]() |
Interfejs [IDao] warstwy [DAO] ma następujący wygląd:
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import rx.Observable;
import java.util.List;
public interface IDao {
// Adres URL serwisu internetowego
public void setUrlServiceWebJson(String url);
// użytkownik
public void setUser(String user, String mdp);
// limit czasu klienta
public void setTimeout(int timeout);
// lista klientów
public Observable<List<Client>> getAllClients();
// lista lekarzy
public Observable<List<Medecin>> getAllMedecins();
// lista terminów wizyt u lekarza
public Observable<List<Creneau>> getAllCreneaux(long idMedecin);
// lista wizyt u lekarza w danym dniu
public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);
// znalezienie klienta na podstawie jego identyfikatora
public Observable<Client> getClientById(long id);
// znalezienie lekarza na podstawie jego identyfikatora
public Observable<Medecin> getMedecinById(long id);
// znalezienie wizyty na podstawie jej identyfikatora
public Observable<Rv> getRvById(long id);
// znalezienie przedziału czasowego na podstawie jego identyfikatora
public Observable<Creneau> getCreneauById(long id);
// dodaj RV
public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);
// usunąć RV
public Observable<Rv> supprimerRv(long idRv);
// rodzaj działalności
public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);
// tryb debugowania
void setDebugMode(boolean isDebugEnabled);
}
- wiersz 10: w celu ustalenia identyfikatora URL usługi internetowej / jSON;
- wiersz 13: w celu ustalenia użytkownika komunikacji klient–serwer. [user] to identyfikator użytkownika, a [mdp] to jego hasło;
- wiersz 16: w celu ustalenia maksymalnego czasu oczekiwania na odpowiedź serwera;
- wiersze 18–49: każdemu identyfikatorowi URL udostępnianemu przez serwis internetowy odpowiada jedna metoda. Przyjmują one sygnaturę metod o tych samych nazwach z komponentu AA [WebClient];
- wiersz 52: w celu sterowania trybem debug warstwy [DAO];
3.6.3.3. Klasa [Dao]
![]() |
Implementacja [DAO] poprzedniego interfejsu [IDao] wygląda następująco:
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 {
// klient serwisu internetowego
@RestService
protected WebClient webClient;
// bezpieczeństwo
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// fabryka 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));
}
// przechwytywacz uwierzytelniania?
if (isBasicAuthentificationNeeded) {
// dodajemy interceptor uwierzytelniania
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// metody prywatne -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// implementacja interfejsu IDao --------------------------------------------------------------------
@Override
public Observable<Response<List<Client>>> getAllClients() {
// log
log("getAllClients");
// wynik
return getResponse(new IRequest<Response<List<Client>>>() {
@Override
public Response<List<Client>> getResponse() {
return webClient.getAllClients();
}
});
}
@Override
public Observable<Response<List<Medecin>>> getAllMedecins() {
// log
log("getAllMedecins");
// wynik
return getResponse(new IRequest<Response<List<Medecin>>>() {
@Override
public Response<List<Medecin>> getResponse() {
return webClient.getAllMedecins();
}
});
}
@Override
public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
// log
log("getAllCreneaux");
// wynik
return getResponse(new IRequest<Response<List<Creneau>>>() {
@Override
public Response<List<Creneau>> getResponse() {
return webClient.getAllCreneaux(idMedecin);
}
});
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
// log
log("getRvMedecinJour");
// wynik
return getResponse(new IRequest<Response<List<Rv>>>() {
@Override
public Response<List<Rv>> getResponse() {
return webClient.getRvMedecinJour(idMedecin, jour);
}
});
}
@Override
public Observable<Response<Client>> getClientById(final long id) {
// log
log("getClientById");
// wynik
return getResponse(new IRequest<Response<Client>>() {
@Override
public Response<Client> getResponse() {
return webClient.getClientById(id);
}
});
}
@Override
public Observable<Response<Medecin>> getMedecinById(final long id) {
// log
log("getMedecinById");
// wynik
return getResponse(new IRequest<Response<Medecin>>() {
@Override
public Response<Medecin> getResponse() {
return webClient.getMedecinById(id);
}
});
}
@Override
public Observable<Response<Rv>> getRvById(final long id) {
// log
log("getRvById");
// wynik
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.getRvById(id);
}
});
}
@Override
public Observable<Response<Creneau>> getCreneauById(final long id) {
// log
log("getCreneauById");
// wynik
return getResponse(new IRequest<Response<Creneau>>() {
@Override
public Response<Creneau> getResponse() {
return webClient.getCreneauById(id);
}
});
}
@Override
public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
// log
log("ajouterRv");
// wynik
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
}
});
}
@Override
public Observable<Response<Rv>> supprimerRv(final long idRv) {
// log
log("supprimerRv");
// wynik
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.supprimerRv(new PostSupprimerRv(idRv));
}
});
}
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// wynik
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
}
- wiersze 18–72: są to wiersze domyślnie obecne w klasie [Dao] projektu [client-android-skel];
- wiersze 74–216: implementacja interfejsu [IDao]. Metody, które wysyłają zapytania do komponentu URL udostępnianego przez serwis internetowy, przekazują te zapytania do komponentu AA [WebClient] (wiersze 22–23);
- wiersze 58–63: jeśli komunikacja między klientem a serwerem jest uwierzytelniana za pomocą autoryzacji typu podstawowego, do komponentu [RestTemplate] dodawany jest interceptor. Spowoduje to, że każde żądanie HTTP wysyłane przez komponent [RestTemplate] zostanie przechwycone przez klasę [MyAuthInterceptor] (wiersze 25–26);
Klasa [MyAuthInterceptor] ma następującą postać:
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 {
// użytkownik
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;
}
}
- wiersz 15: klasa [MyAuthInterceptor] jest komponentem AA typu [singleton];
- wiersz 16: klasa [MyAuthInterceptor] rozszerza interfejs Spring [ClientHttpRequestInterceptor]. Interfejs ten posiada jedną metodę – metodę [intercept] z wiersza 22. Rozszerzamy ten interfejs, aby przechwytywać wszystkie żądania HTTP od klienta. Metoda [intercept] przyjmuje trzy parametry;
- [HtpRequest request]: przechwycone żądanie HTTP,
- [byte[] body]: jej treść, jeśli taka istnieje (na przykład wartości przesłane metodą POST),
- [ClientHttpRequestExecution execution]: komponent Spring, który wykonuje żądanie;
Przechwytujemy wszystkie żądania HTTP od klienta z systemem Android, aby dodać do nich nagłówek uwierzytelniający HTTP przedstawiony w paragrafie 3.5.
- wiersz 23: pobieramy nagłówki HTTP z przechwyconego żądania;
- wiersz 24: tworzymy nagłówek uwierzytelniający HTTP. Stosowany tryb uwierzytelniania (kodowanie base64 ciągu znaków „user:mdp”) jest dostarczany przez klasę Spring [HttpBasicAuthentication];
- wiersz 25: nagłówek uwierzytelniający, który właśnie utworzyliśmy, zostaje dodany do aktualnych nagłówków przechwyconego żądania;
- wiersz 26: kontynuujemy wykonywanie przechwyconego żądania. Podsumowując, przechwycone żądanie zostało wzbogacone o nagłówek uwierzytelniający;
Wszystkie implementacje metod interfejsu [IDao] są oparte na tym samym wzorcu. Weźmy na przykład metodę [getAgendaMedecinJour]:
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// wynik
return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
- wiersz 2: metoda oczekuje dwóch parametrów:
- [idMedecin]: identyfikator lekarza, którego kalendarz ma zostać wyświetlony;
- [jour]: dzień, dla którego chcemy uzyskać kalendarz;
- wiersz 6: wywołuje się metodę [getResponse] klasy nadrzędnej [AbstractDao]. Metoda ta oczekuje parametru typu [IRequest<T>], gdzie T jest typem zwracanym przez metodę [getAgendaMedecinJour] w wierszu 2, w tym przypadku [Response<AgendaMedecinJour>]. Interfejs [IRequest] posiada tylko jedną metodę: [getResponse] (wiersz 8);
- wiersze 8–10: implementacja metody [IRequest.getResponse]. Metoda ta musi zwracać wynik oczekiwany przez metodę [getAgendaMedecinJour] w wierszu 2 typu [Response<AgendaMedecinJour>];
- wiersz 9: odpowiedź jest zwracana przez metodę [webClient.getAgendaMedecinJour]:
// uzyskaj harmonogram wizyt u lekarza
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
Parametry użyte w wierszu 9 są tymi samymi, które zostały przekazane do metody [getAgendaMedecinJour] w wierszu 2. Z tego powodu parametry te muszą posiadać atrybut final;
3.6.4. Aktywność [MainActivity]
Serveur ![]() |
![]() |
Klasa [MainActivity] ma następującą postać:
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 {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// klasa nadrzędna ---------------------------------------
@Override
protected void onCreateActivity() {
// dziennik
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;
}
// interfejs 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);
}
}
- wiersze 21–66: te wiersze są domyślnie zawarte w szablonie [client-android-skel];
- wiersze 66–119: implementacja interfejsu [IDao]. Wszystkie metody przekazują zadania do warstwy [DAO] z wiersza 26;
- wiersze 42–46: metoda [getFragments] zwraca tablicę zawierającą cztery fragmenty aplikacji;
- wiersze 58–61: widok konfiguracji jest pierwszym widokiem wyświetlanym po uruchomieniu aplikacji;
3.6.5. Sesja
![]() |
Klasa [Session] służy do przechowywania informacji, które muszą być przekazywane między fragmentami. Ma ona następujący wygląd:
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 {
// lista lekarzy
private List<Medecin> médecins;
// lista klientów
private List<Client> clients;
// kalendarz
private AgendaMedecinJour agenda;
// pozycja klikniętego elementu w kalendarzu
private int position;
// dzień wizyty w formacie angielskim „yyyy-MM-dd”
private String dayRv;
// dzień spotkania w notacji francuskiej „dd-MM-yyyy”
private String jourRv;
// metody pobierające i ustawiające
...
}
- wiersz 10: klasa [Session] jest komponentem AA, którego utworzono jedną instancję;
- wiersze 12–15: w niniejszym studium przypadku założymy, że listy lekarzy i klientów nie ulegają zmianie. Zostaną one pobrane przy uruchomieniu aplikacji i zapisane w sesji, aby fragmenty mogły z nich korzystać;
- wiersze 20–23: żądany dzień wizyty. Jest on przetwarzany w dwóch formach: w notacji francuskiej (wiersz 23) w kliencie na Androida oraz w notacji angielskiej (wiersz 21) w komunikacji z serwerem;
- wiersz 19: pozycja klikniętego elementu (link „Dodaj” / „Usuń”) w kalendarzu;
3.6.6. Zarządzanie widokiem konfiguracji
3.6.6.1. Widok
Widok konfiguracji to widok wyświetlany po uruchomieniu aplikacji:

Elementy interfejsu graficznego są następujące:
3.6.6.2. Fragment
Widok konfiguracji jest obsługiwany przez następujący fragment [ConfigFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.net.URI;
import java.util.List;
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {
// elementy interfejsu wizualnego
@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;
// dane wprowadzane
private String urlServiceRest;
private String utilisateur;
private String mdp;
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
..
// implementacja metod klasy nadrzędnej -------------------------------------------
...
}
- wiersz 25: fragment jest powiązany z następującym menu [menu_config]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
</menu>
- wiersze 28–38: elementy interfejsu wizualnego;
- wiersze 41–43: trzy pola formularza;
Kliknięcie opcji menu [Valider] jest obsługiwane przez metodę [doValider]:
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
// ukrywanie ewentualnych poprzednich komunikatów o błędach
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
// sprawdzamy poprawność wprowadzonych danych
if (!isPageValid()) {
return;
}
// wypełnianie pola URL serwisu internetowego
mainActivity.setUrlServiceWebJson(urlServiceRest);
// wprowadzamy dane użytkownika
mainActivity.setUser(utilisateur, mdp);
// początek oczekiwania – uruchomimy 2 zadania asynchroniczne
beginWaiting(2);
// lekarze
executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
@Override
public void call(Response<List<Medecin>> responseMedecins) {
// odbieramy odpowiedź
consumeMedecins(responseMedecins);
}
});
// klienci
executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
@Override
public void call(Response<List<Client>> responseClients) {
// odpowiedź jest przetwarzana
consumeClients(responseClients);
}
});
}
private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
// log
if (isDebugEnabled) {
Log.d(className, "consume médecins");
}
// błąd?
if (responseMedecins.getStatus() != 0) {
// komunikat
showAlert(responseMedecins.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// lekarze są zapisywani w sesji
session.setMédecins(responseMedecins.getBody());
}
private void consumeClients(Response<List<Client>> responseClients) {
// log
if (isDebugEnabled) {
Log.d(className, "consume clients");
}
// błąd?
if (responseClients.getStatus() != 0) {
// komunikat
showAlert(responseClients.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// zapisywanie klientów w sesji
session.setClients(responseClients.getBody());
}
- wiersze 8–10: sprawdzana jest poprawność trzech wpisów w formularzu. Jeśli formularz jest nieprawidłowy, nie kontynuuje się dalszych czynności;
- wiersze 11–14: dane niezbędne dla warstwy [DAO] są przekazywane do aktywności;
- wiersz 16: informuje się klasę nadrzędną o zamiarze uruchomienia dwóch zadań asynchronicznych i przygotowuje się oczekiwanie;
- wiersze 17–24: pobierana jest lista lekarzy;
- wiersz 18: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 18: proces, który ma zostać wykonany i obserwowany, jest dostarczany przez metodę [mainActivity.getAllMedecins()];
- wiersze 18–24: drugim parametrem jest instancja typu [Action1<T>], gdzie T jest typem zwracanym przez obserwowany proces, w tym przypadku [Response<List<Medecin>>]
- wiersz 22: po otrzymaniu odpowiedzi przekazujemy ją do metody [consumeMedecins] z wiersza 36;
- wiersze 25–33: po uruchomieniu pierwszego zadania asynchronicznego uruchamia się drugie, aby uzyskać listę klientów. Będziemy więc mieli dwa zadania wykonywane równolegle;
- wiersze 36–52: otrzymano odpowiedź z zadania dotyczącego lekarzy. Jest ona przetwarzana;
- wiersze 42–49: najpierw sprawdzamy, czy serwer zgłosił błąd w polu [status] odpowiedzi;
- wiersz 44: jeśli wystąpił błąd, wyświetlamy komunikaty, które serwer umieścił w polu [messages] odpowiedzi;
- wiersz 46: anulujemy wszystkie zadania;
- wiersz 48: powrót do interfejsu użytkownika;
- wiersz 51: jeśli nie wystąpił błąd, lista lekarzy jest zapisywana w sesji;
Poprawność wprowadzonych danych (wiersz 8) jest sprawdzana za pomocą następującej metody:
private boolean isPageValid() {
// sprawdzanie poprawności wprowadzonych danych
boolean erreur;
URI service;
// poprawność URL usługi 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) {
// odnotowuje się błąd
erreur = true;
}
if (erreur) {
// wyświetlanie błędu
txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
}
// użytkownik
utilisateur = edtUtilisateur.getText().toString().trim();
if (utilisateur.length() == 0) {
// wyświetlany jest błąd
txtErrorUtilisateur.setVisibility(View.VISIBLE);
// odnotowano błąd
erreur = true;
}
// hasło
mdp = edtMdp.getText().toString().trim();
// powrót
return !erreur;
}
Metoda [beginWaiting] (wiersz 16) wygląda następująco:
// rozpoczęcie oczekiwania
protected void beginWaiting(int numberOfRunningTasks) {
// przygotowuje się uruchomienie zadań
beginRunningTasks(numberOfRunningTasks);
// stan przycisków i menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- wiersz 4: informujemy zadanie nadrzędne, że uruchomimy zadania [numberOfRunningTasks];
- wiersz 6: ukrywa się wszystkie opcje menu;
- wiersz 7: a następnie ujawnia się opcję [Actions/Annuler];
Kliknięcie opcji menu [Annuler] jest obsługiwane przez metodę [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
- wiersz 8: prosimy klasę nadrzędną o anulowanie zadań asynchronicznych;
3.6.6.3. Zarządzanie cyklem życia fragmentu
Fragment ma następujący stan: [ConfigFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// widoczność obu komunikatów o błędzie
private boolean txtErrorUrlServiceRestVisible;
private boolean txtErrorUtilisateurVisible;
// metody pobierające i ustawiające
...
}
- gdy klasa nadrzędna zwróci się z taką prośbą, fragment zapisze informacje o widoczności swoich dwóch komunikatów o błędach;
Cykl życia fragmentu jest zaimplementowany w następujący sposób:
// implementacja metod klasy nadrzędnej -------------------------------------------
@Override
public CoreState saveFragment() {
// zapis stanu fragmentu
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) {
// pierwsza wizyta
// ukrywanie komunikatów o błędach
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// przywrócenie widoczności komunikatów o błędach
ConfigFragmentState state = (ConfigFragmentState) previousState;
// nie jest to pierwsza wizyta – przywracamy komunikaty o błędach
txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// następny widok?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
}
}
// metody prywatne ------------------------------------------------
private void initMenu(){
// stan menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- wiersze 2–9: gdy klasa nadrzędna o to poprosi, fragment zapisuje stan swoich dwóch komunikatów o błędach;
- wiersze 11–14: numer fragmentu to [IMainActivity.VUE_CONFIG];
- wiersze 16–19: wykonywane, gdy fragment jest generowany po raz pierwszy (previousState == null) lub ponownie generowany w kolejnych przypadkach (previousState != null). W tym przypadku nie ma nic do zrobienia;
- wiersze 21–31: wykonywane, gdy widok powiązany z fragmentem jest tworzony po raz pierwszy (previousState == null) lub odtwarzany przy kolejnych okazjach (previousState != null);
- wiersze 24–29: przy pierwszej wizycie ukrywa się komunikaty o błędach i wyświetla menu bez akcji [Annuler] (wiersze 62–66);
- wiersze 33–35: wykonywane, gdy do fragmentu dochodzi się poprzez operację [SUBMIT]. Tutaj to się nigdy nie zdarza;
- wiersze 37–44: wykonywane, gdy do fragmentu dochodzi się poprzez operację [NAVIGATION] lub [RESTORE]. Przywraca się stan komunikatów o błędach na podstawie poprzedniego stanu;
- wiersze 47–49: wykonywane po zakończeniu wszystkich poprzednich aktualizacji. Nie ma już nic więcej do zrobienia;
- wiersze 51–59: wykonywane po zakończeniu wszystkich zadań asynchronicznych;
- wiersze 53–54: przywracamy menu do stanu domyślnego;
- wiersze 56–58: jeśli zadania zakończyły się pomyślnie, przechodzi się do następnego widoku, w przeciwnym razie pozostaje się na tym samym widoku;
3.6.7. Zarządzanie widokiem głównym
3.6.7.1. Widok
Ekran powitalny wygląda następująco:

Elementy interfejsu wizualnego są następujące:
3.6.7.2. Fragment
Strona główna jest obsługiwana przez następujący fragment [AccueilFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {
// elementy interfejsu wizualnego
@ViewById(R.id.spinnerMedecins)
protected Spinner spinnerMedecins;
@ViewById(R.id.edt_JourRv)
protected DatePicker edtJourRv;
// dane lokalne
private List<Medecin> medecins;
private Calendar calendrier;
private String[] spinnerMedecinsDataSource;
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementacja metod klasy nadrzędnej -------------------------------------
...
}
- wiersz 26: fragment jest powiązany z następującym menu [menu_accueil]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
</menu>
</item>
</menu>
- wiersze 31–34: elementy interfejsu graficznego;
- wiersz 37: lista lekarzy;
- wiersz 38: kalendarz;
- wiersz 39: źródło danych dla listy rozwijanej lekarzy;
Kliknięcie linku [Valider] jest obsługiwane przez następującą metodę [doValider]:
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
// zapisujemy identyfikator wybranego lekarza
Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
// zapisywanie dnia w sesji
String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
session.setJourRv(jourRv);
// przechodzimy na format daty yyyy-MM-dd
String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
session.setDayRv(dayRv);
// początek oczekiwania – uruchomimy 1 zadanie asynchroniczne
beginWaiting(1);
// pobieramy harmonogram lekarza
executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// odbieramy odpowiedź
consumeAgenda(responseAgendaMedecinJour);
}
});
}
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// błąd?
if (responseAgendaMedecinJour.getStatus() != 0) {
// komunikat
showAlert(responseAgendaMedecinJour.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// dodajemy kalendarz do sesji
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- wiersz 5: pobierany jest identyfikator wybranego lekarza;
- wiersze 7–8: wprowadzamy wybraną datę w formacie francuskim;
- wiersze 10–11: zapisuje się wybraną datę w formacie angielskim;
- wiersz 13: informujemy klasę nadrzędną, że zamierzamy uruchomić zadanie asynchroniczne i przygotowujemy się do oczekiwania;
- wiersze 15–22: pobierany jest harmonogram lekarza;
- wiersz 15: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 15: proces, który ma zostać wykonany i obserwowany, jest dostarczany przez metodę [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)];
- wiersze 15–22: drugim parametrem jest instancja typu [Action1<T>], gdzie T jest typem zwracanym przez obserwowany proces, w tym przypadku [Response<AgendaMedecinJour>]
- wiersz 20: po otrzymaniu odpowiedzi przekazujemy ją do metody [consumeAgenda] z wiersza 25;
- wiersz 15: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersze 25–37: otrzymano harmonogram lekarza. Jest on analizowany;
- wiersze 27–34: najpierw sprawdzamy, czy serwer zgłosił błąd w polu [status] odpowiedzi;
- wiersz 29: jeśli wystąpił błąd, wyświetlamy komunikaty, które serwer umieścił w polu [messages] odpowiedzi;
- wiersz 31: anulujemy wszystkie zadania;
- wiersz 33: powrót do interfejsu użytkownika;
- wiersz 36: jeśli nie wystąpiły żadne błędy, kalendarz jest uruchamiany;
Metoda [beginWaiting] (wiersz 13) wygląda następująco:
// początek oczekiwania
protected void beginWaiting(int numberOfRunningTasks) {
// przygotowuje się uruchomienie zadań
beginRunningTasks(numberOfRunningTasks);
// stan przycisków i menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- wiersz 4: informujemy zadanie nadrzędne, że uruchomimy zadania [numberOfRunningTasks];
- wiersz 6: ukrywa się wszystkie opcje menu;
- wiersz 7: a następnie ujawnia opcję [Actions/Annuler];
Kliknięcie opcji menu [Annuler] jest obsługiwane przez metodę [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
- wiersz 8: prosimy klasę nadrzędną o anulowanie zadań asynchronicznych;
Kliknięcie opcji menu [Retour à la configuration] jest obsługiwane w następujący sposób:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// przechodzimy do widoku konfiguracji
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- wiersz 4: przechodzi się do widoku konfiguracji za pomocą akcji [NAVIGATION]. Oznacza to, że chce się przywrócić widok konfiguracji do stanu, w jakim został pozostawiony;
3.6.7.3. Zarządzanie cyklem życia fragmentu
Fragment ma następujący stan: [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 {
// stan fragmentu [Accueil]
// pozycja wybranego lekarza
private int selectedMedecinPosition;
// wybrana data
private int year;
private int month;
private int dayOfMonth;
// źródło danych z listy rozwijanej lekarzy
private String[] spinnerMedecinsDataSource;
// konstruktorzy
public AccueilFragmentState() {
}
// metody pobierające i ustawiające
...
}
- wiersz 11: pozwala przywrócić element wybrany z listy lekarzy;
- wiersze 13–15: umożliwiają przywrócenie wybranej daty z kalendarza;
- wiersz 17: umożliwia wyświetlenie źródła danych z listy lekarzy;
Cykl życia fragmentu jest zaimplementowany w następujący sposób:
// implementacja metod klasy nadrzędnej -------------------------------------
@Override
public CoreState saveFragment() {
// zapisujemy widok
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) {
// pobieranie lekarzy z sesji
medecins = session.getMédecins();
// Pierwsza wizyta?
if (previousState == null) {
// tworzymy tablicę wyświetlaną przez spinner
spinnerMedecinsDataSource = new String[medecins.size()];
int i = 0;
for (Medecin medecin : medecins) {
spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
i++;
}
} else {
// nie jest to pierwsza wizyta
AccueilFragmentState state = (AccueilFragmentState) previousState;
spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
}
// kalendarz
calendrier = Calendar.getInstance();
}
@Override
protected void initView(CoreState previousState) {
// powiązanie suwaka wyboru lekarzy z jego źródłem danych
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);
// minimalna data w kalendarzu to dzisiejszy dzień
edtJourRv.setMinDate(calendrier.getTimeInMillis());
// pierwsza wizyta?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// przywracamy stan bieżącej sesji
AccueilFragmentState state = (AccueilFragmentState) previousState;
// wybór lekarzy z listy rozwijanej
spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
// kalendarz
edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// wywoływana po zakończeniu lub anulowaniu wszystkich zadań
// stan menu
initMenu();
// następny widok?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
}
}
// metody prywatne ------------------------------------------------
private void initMenu() {
// stan menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- wiersze 2–9: na żądanie klasy nadrzędnej fragment zapisuje stan następujących elementów:
- wiersz 6: wybraną pozycję na liście lekarzy;
- wiersze 7–9: dzień miesiąca, miesiąc i rok daty wybranej w kalendarzu;
- wiersz 10: źródło danych dla listy rozwijanej lekarzy;
- wiersze 14–17: numer fragmentu to [IMainActivity.VUE_ACCUEIL];
- wiersze 19–39: wykonywane, gdy fragment jest generowany po raz pierwszy (previousState==null) lub ponownie generowany w kolejnych przypadkach (previousState !=null);
- wiersze 25–31: w przypadku pierwszej wizyty tworzone jest źródło danych dla spinnera lekarzy;
- wiersze 33–35: w przypadku kolejnych wizyt źródło danych listy rozwijanej jest pobierane z poprzedniego stanu fragmentu;
- wiersze 41–54: wykonywane, gdy widok powiązany z fragmentem jest tworzony po raz pierwszy (previousState==null) lub odtwarzany przy kolejnych wizytach (previousState !=null);
- wiersze 50–53: podczas pierwszej wizyty wyświetla się menu bez akcji [Annuler] (wiersze 88–92);
- wiersze 43–48: dla wszystkich wizyt, niezależnie od tego, czy jest to pierwsza wizyta, czy nie, przypisuje się listę lekarzy do jej źródła (wiersze 44–46) i ustawia minimalną datę w kalendarzu na dzisiejszą datę (wiersz 48);
- wiersze 56–60: wykonywane po przejściu do fragmentu za pomocą operacji [SUBMIT]. Przechodzi się wówczas z widoku [CONFIG]. Menu przywraca się do stanu początkowego;
- wiersze 62–70: wykonywane po przejściu do fragmentu za pomocą operacji [NAVIGATION] lub [RESTORE];
- wiersz 67: ustawia się suwak wyboru lekarzy na ostatnio wybranego lekarza;
- wiersz 69: kalendarz jest ustawiany na ostatnią wybraną datę;
- wiersze 72–74: wykonywane po zakończeniu wszystkich poprzednich aktualizacji. Nie ma już nic więcej do zrobienia;
- wiersze 76–85: wykonywane po zakończeniu wszystkich zadań asynchronicznych;
- wiersz 80: przywraca się domyślny stan menu;
- wiersze 82–84: jeśli zadania zakończyły się pomyślnie, przechodzi się do następnego widoku, w przeciwnym razie pozostaje się na tym samym widoku;
3.6.8. Zarządzanie widokiem Kalendarza
3.6.8.1. Widok
Ekran powitalny wygląda następująco:

Elementy interfejsu graficznego są następujące:
3.6.8.2. Fragment
Widok Kalendarz jest obsługiwany przez następujący fragment [AgendaFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {
// elementy interfejsu wizualnego
@ViewById(R.id.txt_titre2_agenda)
protected TextView txtTitre2;
@ViewById(R.id.listViewAgenda)
protected ListView lstCreneaux;
// kalendarz wyświetlany przez fragment
private AgendaMedecinJour agenda;
// informacje o przedziałach czasowych
private int firstPosition;
private int top;
// czy spotkanie zostało usunięte
private boolean rdvSupprimé;
// numer dodanego lub usuniętego terminu
private int numCréneau;
// aktualizacja kalendarza po dodaniu / usunięciu
private void updateAgenda() {
...
}
...
// implementacja metod klasy nadrzędnej ------------------------------------------------------
...
}
- wiersz 27: fragment jest powiązany z następującym menu [menu_agenda]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionAgenda"
android:title="@string/actionAgenda"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
</menu>
</item>
</menu>
- wiersze 32–35: elementy interfejsu wizualnego;
- wiersze 37–45: dane globalne dla metod;
3.6.8.2.1. Metoda [updateAgenda]
(Ponowne) generowanie listy terminów w kalendarzu jest wymagane w kilku miejscach kodu. Zostało ono wyodrębnione do następującej prywatnej metody [updateAgenda]:
// aktualizacja kalendarza po dodaniu / usunięciu
private void updateAgenda() {
// (ponowne) wygenerowanie przedziałów czasowych w kalendarzu
// kalendarz jest pobierany w ramach sesji i zapisywany w polu fragmentu
agenda = session.getAgenda();
// regeneracja ListView przedziałów czasowych
ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour(), this);
lstCreneaux.setAdapter(adapter);
// przechodzimy do właściwego miejsca w ListView
lstCreneaux.setSelectionFromTop(firstPosition, top);
}
- wiersz 5: kalendarz jest pobierany z sesji i zapisywany w polu [agenda] fragmentu;
- wiersze 7–9: definiuje się adapter komponentu [ListView]. Adapter ten określa zarówno źródło danych [ListView], jak i szablon wyświetlania każdego elementu tego źródła. Adapter ten zostanie omówiony wkrótce;
- wiersz 11: powrót do poprzedniej pozycji w kalendarzu. Widoczna jest bowiem tylko część przedziałów czasowych danego dnia. Jeśli dodamy lub usuniemy spotkanie w ostatnim przedziale, powyższy kod odświeży stronę, aby wyświetlić nowy kalendarz. To odświeżenie powoduje, że ponownie znajdujemy się w pierwszym przedziale czasowym, co nie jest pożądane. Wiersz 5 rozwiązuje ten problem. Opis tego rozwiązania można znaleźć w plikach URL i [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];
Klasa [ListCreneauxAdapter] służy do zdefiniowania wiersza w pliku [ListView]:

Jak widać powyżej, w zależności od tego, czy w danym przedziale czasowym jest zaplanowane spotkanie, czy nie, wyświetlany zawartość różni się. Kod klasy [ListCreneauxAdapter] jest następujący:
...
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
// tabela przedziałów czasowych
private CreneauMedecinJour[] creneauxMedecinJour;
// kontekst wykonania
private Context context;
// identyfikator układu wyświetlania wiersza na liście przedziałów czasowych
private int layoutResourceId;
// detektor kliknięć
private AgendaFragment vue;
// konstruktor
public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaFragment vue) {
super(context, layoutResourceId, creneauxMedecinJour);
// zapisujemy informacje
this.creneauxMedecinJour = creneauxMedecinJour;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.vue = vue;
// sortowanie tablicy terminów według pory dnia
Arrays.sort(creneauxMedecinJour, new MyComparator());
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
// sortowanie tabeli przedziałów czasowych
class MyComparator implements Comparator<CreneauMedecinJour> {
...
}
}
- wiersz 3: klasa [ListCreneauxAdapter] musi rozszerzać predefiniowany adapter dla klasy [ListView], w tym przypadku klasy [ArrayAdapter], która – jak wskazuje jej nazwa – dostarcza do klasy [ListView] tablicę obiektów, w tym przypadku typu [CreneauMedecinJour]. Przypomnijmy kod tej jednostki:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// pola
private Creneau creneau;
private Rv rv;
...
}
- klasa [CreneauMedecinJour] zawiera przedział czasowy (wiersz 5) oraz ewentualne spotkanie (wiersz 6) lub null, jeśli nie ma spotkania;
Wracając do kodu klasy [ListCreneauxAdapter]:
- wiersz 15: konstruktor otrzymuje cztery parametry:
- bieżącą aktywność systemu Android,
- plik XML definiujący zawartość każdego elementu [ListView],
- tabela terminów wizyt u lekarza,
- sam widok;
- wiersz 24: tablica terminów jest posortowana w porządku rosnącym według godzin;
Metoda [getView] odpowiada za wygenerowanie widoku odpowiadającego wierszowi z pliku [ListView]. Składa się on z trzech elementów:
Kod metody [getView] jest następujący:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// wybieramy właściwy przedział czasowy
CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
// tworzy się wiersz
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// przedział czasowy
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()));
// klient
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);
// link
final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
if (creneauMedecin.getRv() == null) {
// dodaj
btnValider.setText(R.string.btn_ajouter);
btnValider.setTextColor(context.getResources().getColor(R.color.blue));
} else {
// usuń
btnValider.setText(R.string.btn_supprimer);
btnValider.setTextColor(context.getResources().getColor(R.color.red));
}
// słuchacz linku
btnValider.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// przekazujemy informacje do widoku kalendarza
vue.doValider(position, btnValider.getText().toString());
}
});
// wyświetlanie wiersza
return row;
}
- wiersz 2: pozycja to numer wiersza, który zostanie wygenerowany w [ListView]. Jest to również numer przedziału czasowego w tabeli [creneauxMedecinJour]. Pozostałe dwa parametry są ignorowane;
- wiersz 4: pobieramy przedział czasowy, który ma zostać wyświetlony w wierszu pliku [ListView];
- wiersz 6: wiersz jest tworzony na podstawie jego definicji w tabeli XML
![]() |
Kod [creneau_medecin.xml] jest następujący:
<?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>
- wiersze 8–10: tworzony jest przedział czasowy o kodzie [1];
- wiersze 12–20: tworzona jest tożsamość klienta [2];
- wiersz 23: jeśli w przedziale czasowym nie ma spotkania;
- wiersze 25–26: tworzony jest link [Ajouter] w kolorze niebieskim;
- wiersze 29–30: w przeciwnym razie tworzony jest link [Supprimer] w kolorze czerwonym;
- wiersze 33–40: niezależnie od rodzaju linku [Ajouter / Supprimer] to metoda [doValider] widoku będzie obsługiwać kliknięcie na link. Metoda otrzyma dwa argumenty:
- numer przedziału, który został kliknięty,
- nazwę linku, który został kliknięty;
- wiersz 42: zwracamy właśnie utworzony wiersz.
Należy zauważyć, że to metoda [doValider] fragmentu [AgendaFragment] obsługuje linki. Wygląda ona następująco:
// kliknięcie na link [Ajouter / Supprimer]
public void doValider(int numCréneau, String texte) {
// czy operacja jest w toku?
if (numberOfRunningTasks != 0) {
Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
return;
}
// zapisujemy pozycję przewijania, aby do niej powrócić
// przeczytaj [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// pozycja pierwszego elementu – całkowicie widoczna lub nie
firstPosition = lstCreneaux.getFirstVisiblePosition();
// przesunięcie osi Y tego elementu względem górnej krawędzi ListView
// mierzy wysokość ewentualnie ukrytej części
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// odnotowuje się również numer klikniętego pola
this.numCréneau = numCréneau;
// w zależności od treści linku, nie wykonuje się tej samej czynności
if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
doAjouter();
} else {
doSupprimer();
}
}
- metoda [doValider] otrzymuje dwie informacje:
- numer przedziału, który został kliknięty;
- tekst (Dodaj / Usuń) linku, który został kliknięty;
- wiersze 4–7: kliknięcie linków [Supprimer / Ajouter] jest zablokowane, jeśli trwają zadania asynchroniczne. Jest to wybór ułatwiający pisanie kodu. Można go poddać dyskusji;
- wiersze 11–15: zapisujemy informacje (firstPosition, top) dotyczące ListView przedziałów czasowych w polach fragmentu, aby prywatna metoda [updateAgenda] mogła je odtworzyć z tą samą pozycją przewijania;
- wiersz 17: zapisuje się numer klikniętego przedziału;
- wiersze 19–23: w zależności od tekstu klikniętego linku dodajemy lub usuwamy element;
3.6.8.2.2. Metoda [doSupprimer]
Metoda [doSupprimer] zapewnia usunięcie spotkania z klikniętego przedziału czasowego:
// usunięcie terminu spotkania
private void doSupprimer() {
// oczekiwanie na zakończenie dwóch zadań
beginWaiting(2);
// usuwamy spotkanie w tle
rdvSupprimé = false;
// identyfikator spotkania do usunięcia
long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
// usunięcie przez zadanie asynchroniczne
executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// wykorzystanie wyniku
consumeRv(responseRv);
}
});
}
// odbiór odpowiedzi
private void consumeRv(Response<Rv> responseRv) {
// błąd?
if (responseRv.getStatus() != 0) {
// komunikat
showAlert(responseRv.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// odnotowuje się, że spotkanie zostało usunięte
rdvSupprimé = true;
// prosimy o najnowszy kalendarz
executeInBackground(
mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// odpowiadamy
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// wykorzystanie kalendarza
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// błąd?
if (responseAgendaMedecinJour.getStatus() != 0) {
// komunikat
showAlert(responseAgendaMedecinJour.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// dodajemy kalendarz do sesji
session.setAgenda(responseAgendaMedecinJour.getBody());
// aktualizujemy kalendarz w widoku
updateAgenda();
}
- wiersz 4: informujemy klasę nadrzędną, że uruchomimy dwa zadania asynchroniczne i rozpoczynamy oczekiwanie na zakończenie tych dwóch zadań;
- wiersz 8: pobierany jest identyfikator spotkania, które ma zostać usunięte. Serwer potrzebuje bowiem tej informacji;
- wiersze 9–18: wysyłamy żądanie usunięcia spotkania za pomocą zadania asynchronicznego;
- wiersz 10: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 10: proces, który ma zostać wykonany i obserwowany, jest dostarczany przez metodę [mainActivity.supprimerRv(idRv)];
- wiersze 10–17: drugim parametrem jest instancja typu [Action1<T>], gdzie T jest typem zwracanym przez obserwowany proces, w tym przypadku [Response<Rv>]
- wiersz 15: po otrzymaniu odpowiedzi przekazujemy ją do metody [consumeRv] z wiersza 21;
- wiersz 10: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersze 21–44: otrzymano odpowiedź z zadania asynchronicznego. Jest ona przetwarzana;
- wiersze 23–30: najpierw sprawdzamy, czy serwer zgłosił błąd w polu [status] odpowiedzi;
- wiersz 25: jeśli wystąpił błąd, wyświetlamy komunikaty, które serwer umieścił w polu [messages] odpowiedzi;
- wiersz 27: anulujemy wszystkie zadania;
- wiersz 29: powrót do interfejsu użytkownika;
- wiersz 32: jeśli nie wystąpił błąd, odnotowuje się, że wizyta została usunięta;
- wiersze 34–43: zamiast po prostu usunąć wizytę z kalendarza aktualnie wyświetlanego przez fragment, pobieramy nowy kalendarz lekarza. Aplikacja jest bowiem przeznaczona dla wielu użytkowników i inni użytkownicy również mogli wprowadzić zmiany w kalendarzu lekarza. Dlatego warto mieć najnowszą wersję;
- wiersze 34–43, 47–61: powtarzamy czynności wykonane wcześniej we fragmencie [AccueilFragment], tym razem wykorzystując informacje pobrane z sesji;
Metoda [beginWaiting] (wiersz 4) wygląda następująco:
// początek oczekiwania
protected void beginWaiting(int numberOfRunningTasks) {
// przygotowuje się uruchomienie zadań
beginRunningTasks(numberOfRunningTasks);
// stan przycisków i menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- wiersz 4: informujemy zadanie nadrzędne, że uruchomimy zadania [numberOfRunningTasks];
- wiersz 6: ukrywa się wszystkie opcje menu;
- wiersz 7: a następnie ujawnia opcję [Actions/Annuler];
3.6.8.2.3. Metoda [doAnnuler]
Kliknięcie opcji menu [Annuler] jest obsługiwane przez metodę [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
- wiersz 7: wysyłane jest żądanie do klasy nadrzędnej o anulowanie zadań asynchronicznych;
3.6.8.2.4. Opcja menu [Retour à la configuration]
Kliknięcie opcji menu [Retour à la configuration] jest obsługiwane w następujący sposób:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// przechodzimy do widoku konfiguracji
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- wiersz 4: przechodzimy do widoku konfiguracyjnego za pomocą akcji [NAVIGATION]. Oznacza to, że chcemy przywrócić widok konfiguracyjny do stanu, w jakim go pozostawiliśmy;
3.6.8.2.5. Opcja menu [Retour à l'accueil]
Kliknięcie opcji menu [Retour à l'accueil] jest obsługiwane w podobny sposób:
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// przechodzimy do widoku głównego
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
3.6.8.3. Zarządzanie cyklem życia fragmentu
Fragment ma następujący stan: [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 {
// tytuł widoku
private String titre;
// ListView
private int firstPosition;
private int top;
// konstruktorów
public AgendaFragmentState() {
}
public AgendaFragmentState(String titre) {
this.titre = titre;
}
// metody pobierające i ustawiające
...
}
- wiersz 10: tytuł wyświetlany u góry widoku;
- wiersze 12–13: umożliwiają wyświetlenie scrolling z ListView dotyczących terminów wizyt u lekarza;
Cykl życia fragmentu jest zaimplementowany w następujący sposób:
// implementacja metod klasy nadrzędnej ------------------------------------------------------
@Override
public CoreState saveFragment() {
// zapisywanie stanu
AgendaFragmentState state = new AgendaFragmentState();
state.setTitre(txtTitre2.getText().toString());
// zapisywanie pozycji przewijania w celu powrotu do niej
// odczyt [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// pozycja pierwszego elementu – całkowicie widoczna lub nie
firstPosition = lstCreneaux.getFirstVisiblePosition();
// przesunięcie osi Y tego elementu względem górnej krawędzi ListView
// mierzy wysokość ewentualnie ukrytej części
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// zapisujemy to wszystko
state.setTop(top);
state.setFirstPosition(firstPosition);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AGENDA;
}
@Override
protected void initFragment(CoreState previousState) {
// pierwsza wizyta?
if (previousState != null) {
// nie jest to pierwsza wizyta
AgendaFragmentState state = (AgendaFragmentState) previousState;
// oraz informacje z ListView
firstPosition = state.getFirstPosition();
top = state.getTop();
}
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// pobieramy kalendarz
agenda = session.getAgenda();
// generujemy tytuł strony
Medecin medecin = agenda.getMedecin();
txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
medecin.getNom(), session.getJourRv()));
// stan menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// odświeżamy tytuł strony
AgendaFragmentState state = (AgendaFragmentState) previousState;
txtTitre2.setText(state.getTitre());
}
@Override
protected void notifyEndOfUpdates() {
// ponowne generowanie listy terminów
updateAgenda();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// stan menu
initMenu();
// w przypadku anulowania, ale spotkanie zostało usunięte, należy zaktualizować lokalny kalendarz
if (runningTasksHaveBeenCanceled && rdvSupprimé) {
// usuwamy spotkanie z lokalnego kalendarza (nie udało się uzyskać dostępu do kalendarza globalnego)
agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
// aktualizuje się interfejs użytkownika
updateAgenda();
}
}
// metody prywatne ------------------------------------------------
private void initMenu() {
// stan menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- wiersze 2–19: na żądanie klasy nadrzędnej fragment zapisuje stan następujących elementów:
- wiersz 6: tytuł wyświetlany u góry widoku;
- wiersze 7–17: informacje (top, firstPosition), które pozwolą odtworzyć scrolling z ListView;
- wiersze 21–24: numer fragmentu to [IMainActivity.VUE_AGENDA];
- wiersze 26–35: wykonywane, gdy fragment jest generowany po raz pierwszy (previousState == null) lub ponownie generowany w kolejnych przypadkach (previousState != null);
- wiersze 30–34: jeśli nie jest to pierwsza wizyta fragmentu, pobierane są informacje (top, firstPosition), które pozwolą odtworzyć scrolling z ListView;
- wiersze 38–40: wykonywane, gdy widok powiązany z fragmentem jest tworzony po raz pierwszy (previousState == null) lub odtwarzany przy kolejnych wywołaniach (previousState != null). Nie ma tu nic do zrobienia, ponieważ ListView dla przedziałów czasowych zostanie wygenerowany przez prywatną metodę [updateAgenda] (wiersze 61–65);
- wiersze 42–52: wykonywane po przejściu do fragmentu za pomocą operacji [SUBMIT]. Przechodzi się wówczas z widoku [ACCUEIL];
- wiersz 45: pobierany jest kalendarz uruchomiony przez metodę [AccueilFragment];
- wiersze 47–49: generowany jest tytuł widoku;
- operacja ListView dotycząca przedziałów czasowych zostanie wygenerowana przez metodę prywatną [updateAgenda] (wiersze 61–65);
- wiersze 54–59: wykonywane po przejściu do fragmentu w wyniku operacji [NAVIGATION] lub [RESTORE];
- wiersze 57–58: odświeża się tytuł widoku;
- ListView dla przedziałów czasowych zostanie wygenerowany przez metodę prywatną [updateAgenda] (wiersze 61–65);
- wiersze 72–74: wykonywane po zakończeniu wszystkich poprzednich aktualizacji. Aktualizujemy ListView dotyczący przedziałów czasowych, ponieważ aktualizacja ta jest konieczna niezależnie od sposobu, w jaki dotarliśmy do fragmentu;
- wiersze 67–77: wykonywane po zakończeniu wszystkich zadań asynchronicznych;
- wiersz 70: przywracamy menu do stanu domyślnego (wiersze 82–86);
- wiersz 72: były dwa zadania asynchroniczne. Sprawdzamy, czy pierwsze (usunięcie spotkania) zakończyło się powodzeniem, pomimo anulowania;
- wiersz 74: jeśli tak, usuwa się spotkanie z lokalnego kalendarza
- wiersz 75: i aktualizujemy jego wyświetlanie;
3.6.9. Obsługa widoku dodawania spotkania
3.6.9.1. Widok
Widok dodawania spotkania wygląda następująco:

Elementy interfejsu wizualnego są następujące:
3.6.9.2. Fragment
Ekran dodawania spotkania jest obsługiwany przez następujący fragment [AjoutRvFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {
// elementy interfejsu graficznego
@ViewById(R.id.spinnerClients)
protected Spinner spinnerClients;
@ViewById(R.id.txt_titre2_ajoutRv)
protected TextView txtTitre2;
// klienci
private List<Client> clients;
// dane lokalne
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
...
}
...
// implementacja metod klasy nadrzędnej ----------------------------------
...
}
- wiersz 26: fragment jest powiązany z następującym menu [menu_ajout_rv]:
![]() |
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToAccueil"
android:title="@string/navigationToAccueil"/>
<item
android:id="@+id/navigationToAgenda"
android:title="@string/navigationToAgenda"/>
</menu>
</item>
</menu>
- wiersze 30–33: elementy interfejsu graficznego;
- wiersz 36: lista klientów;
- wiersz 43: źródło danych dla spinnera klientów;
Kliknięcie linku [Valider] jest obsługiwane przez następującą metodę [doValider]:
// klienci
private List<Client> clients;
// dane lokalne
private Creneau creneau;
private Medecin medecin;
private boolean rdvAjouté;
private Rv rv;
private String[] spinnerClientsDataSource;
...
// walidacja strony
@OptionsItem(R.id.actionValider)
protected void doValider() {
// pobieranie wybranego klienta
Client client = clients.get(spinnerClients.getSelectedItemPosition());
// rozpoczęcie oczekiwania na 2 zadania asynchroniczne
beginWaiting(2);
// dodajemy RV
rdvAjouté = false;
executeInBackground(
mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// odbieranie odpowiedzi
consumeRv(responseRv);
}
});
}
// przetwarzanie obiektu Response<Rv>
void consumeRv(Response<Rv> responseRv) {
// błąd?
if (responseRv.getStatus() != 0) {
// komunikat
showAlert(responseRv.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// odnotowuje się, że spotkanie zostało dodane
rdvAjouté = true;
// zapisano spotkanie
this.rv = responseRv.getBody();
// żądanie nowego kalendarza
executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// odbieramy odpowiedź
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// przetwarzanie obiektu Response<AgendaMedecinJour>
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// błąd?
if (responseAgendaMedecinJour.getStatus() != 0) {
// komunikat
showAlert(responseAgendaMedecinJour.getMessages());
// anulowanie
doAnnuler();
// powrót do UI
return;
}
// dodajemy kalendarz do sesji
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- wiersz 13: w momencie uruchomienia metody [doValider] pola 2, 5, 6 i 9 zostały zainicjowane w trakcie cyklu życia fragmentu. Zobaczymy, w jaki sposób;
- wiersz 15: pobieramy encję [Client] odpowiadającą elementowi wybranemu w spinnerze klientów;
- wiersz 17: informujemy klasę nadrzędną, że zamierzamy uruchomić dwa zadania asynchroniczne i przygotowujemy się do oczekiwania;
- wiersz 19: na początku wizyta nie została jeszcze dodana do kalendarza lekarza;
- wiersze 20–30: wysyłamy do serwera żądanie dodania spotkania;
- wiersz 20: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 20: proces, który ma zostać uruchomiony i obserwowany, jest dostarczany przez metodę [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())];
- wiersze 22–29: drugim parametrem jest instancja typu [Action1<T>], gdzie T jest typem zwracanym przez obserwowany proces, w tym przypadku [Response<Rv>]
- wiersz 27: po otrzymaniu odpowiedzi przekazujemy ją do metody [consumeRV] z wiersza 33;
- wiersz 20: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersze 33–56: otrzymano odpowiedź z serwera. Jest ona przetwarzana;
- wiersze 35–42: najpierw sprawdzamy, czy serwer zgłosił błąd w polu [status] odpowiedzi;
- wiersz 37: jeśli wystąpił błąd, wyświetlane są komunikaty, które serwer umieścił w polu [messages] odpowiedzi;
- wiersz 39: anulujemy wszystkie zadania;
- wiersz 41 : powrót do interfejsu użytkownika;
- wiersz 44: jeśli nie wystąpił błąd, odnotowuje się, że spotkanie zostało dodane;
- wiersz 46: zapisujemy dodane spotkanie w polu fragmentu;
- wiersze 47–55: podobnie jak w przypadku usuwania terminu, po dodaniu terminu wysyłamy zapytanie do serwera o najnowszy kalendarz lekarza;
- wiersze 47–56, 59–71: mamy tu kod, który pojawiał się już kilkakrotnie;
Metoda [beginWaiting] (wiersz 17) wygląda następująco:
// początek oczekiwania
protected void beginWaiting(int numberOfRunningTasks) {
// przygotowuje się uruchomienie zadań
beginRunningTasks(numberOfRunningTasks);
// stan przycisków i menu
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});
}
- wiersz 4: informujemy zadanie nadrzędne, że uruchomimy zadania [numberOfRunningTasks];
- wiersz 6: ukrywa się wszystkie opcje menu;
- wiersz 7: a następnie ujawnia opcję [Actions/Annuler];
Kliknięcie opcji menu [Annuler] jest obsługiwane przez metodę [doAnnuler]:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
- wiersz 7: prosimy klasę nadrzędną o anulowanie zadań asynchronicznych;
Nawigacja wsteczna jest realizowana przez trzy następujące metody:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// przechodzimy do widoku konfiguracji
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToAccueil() {
// przechodzimy do widoku konfiguracji
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAgenda)
protected void navigationToAgenda() {
// przechodzimy do widoku kalendarza
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}
3.6.9.3. Zarządzanie cyklem życia fragmentu
Fragment ma następujący stan: [AjoutRvFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
// stan fragmentu AjoutRvFragment
public class AjoutRvFragmentState extends CoreState {
// pozycja wybranego klienta
private int selectedClientPosition;
// tytuł widoku
private String titre;
// źródło danych dla listy rozwijanej klientów
private String[] spinnerClientsDataSource;
// metody pobierające i ustawiające
...
}
Cykl życia fragmentu jest zaimplementowany w następujący sposób:
// implementacja metod klasy nadrzędnej ----------------------------------
@Override
public CoreState saveFragment() {
// zapis widoku
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) {
// pobieranie klientów z sesji
clients = session.getClients();
// Pierwsza wizyta?
if (previousState == null) {
// tworzymy tablicę wyświetlaną przez spinner
spinnerClientsDataSource = new String[clients.size()];
int i = 0;
for (Client client : clients) {
spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
i++;
}
} else {
// nie jest to pierwsza wizyta
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
spinnerClientsDataSource = state.getSpinnerClientsDataSource();
}
}
@Override
protected void initView(CoreState previousState) {
// powiązanie wskaźnika postępu z jego źródłem danych
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);
// Pierwsza wizyta?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// pobieramy numer terminu do rezerwacji w ramach sesji
int position = session.getPosition();
// pobieramy kalendarz lekarza w sesji
AgendaMedecinJour agenda = session.getAgenda();
// pobieramy lekarza i termin, na który umówimy wizytę
medecin = agenda.getMedecin();
creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
// tworzy się nagłówek 2 strony
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()));
// wybór klienta
spinnerClients.setSelection(0);
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// przywrócenie poprzedniego stanu
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
// nagłówek
txtTitre2.setText(state.getTitre());
// ikona ładowania
spinnerClients.setSelection(state.getSelectedClientPosition());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// stan menu
initMenu();
// następny widok?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
// nastąpiło anulowanie – czy spotkanie zostało już dodane?
if (rdvAjouté) {
// modyfikujemy lokalny kalendarz (nie otrzymaliśmy kalendarza globalnego)
AgendaMedecinJour agenda = session.getAgenda();
agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
// wyświetla się kalendarz
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
}
// metody prywatne -------------------
private void initMenu() {
// stan menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- wiersze 2–10: na żądanie klasy nadrzędnej fragment zapisuje stan następujących elementów:
- wiersz 6: tytuł u góry widoku;
- wiersz 7: pozycja wybranego elementu w spinnerze klientów;
- wiersz 8: źródło danych listy rozwijanej klientów;
- wiersze 12–15: numer fragmentu to [IMainActivity.VUE_AJOUT_RV];
- wiersze 17–35: wykonywane podczas pierwszego wygenerowania fragmentu (previousState == null) lub ponownego wygenerowania w kolejnych przypadkach (previousState != null);
- wiersz 20: pobierana jest lista klientów z sesji w celu umieszczenia jej w polu fragmentu;
- wiersze 22–30: w przypadku pierwszej wizyty tworzone jest źródło danych dla listy klientów;
- wiersze 32–33: w przypadku kolejnych wizyt źródło danych dla listy klientów jest pobierane z poprzedniego stanu fragmentu;
- wiersze 37–49: wykonywane, gdy widok powiązany z fragmentem jest tworzony po raz pierwszy (previousState==null) lub odtwarzany przy kolejnych wywołaniach (previousState !=null);
- wiersze 40–43: w każdym przypadku spinner klientów jest powiązany ze swoim źródłem danych;
- wiersze 45–48: przy pierwszej wizycie wyświetlane jest menu bez akcji [Annuler] (wiersze 107–111);
- wiersze 51–70: wykonywane po przejściu do fragmentu za pomocą operacji [SUBMIT]. Przechodzi się wówczas z widoku [AGENDA];
- wiersz 54: pobierany jest numer przedziału czasowego, w którym zostanie umówione spotkanie;
- wiersze 56–59: pobierane są obiekty [Medecin] i [Creneau] niezbędne do dodania tego spotkania i umieszczane są one w polach fragmentu;
- wiersze 61–65: na podstawie tych informacji można utworzyć tytuł widoku;
- wiersz 67: suwak wyboru klientów jest ustawiony na pierwszy element;
- wiersz 69: menu jest przywracane do stanu początkowego (bez opcji [Annuler]);
- wiersze 72–80: wykonywane po przejściu do fragmentu za pomocą operacji [NAVIGATION] lub [RESTORE];
- wiersz 77: odświeża się tytuł widoku;
- wiersz 79: spinner klientów jest ponownie ustawiany na ostatniego wybranego klienta;
- wiersze 82–84: wykonywane po zakończeniu wszystkich poprzednich aktualizacji. W tym przypadku nie ma już nic więcej do zrobienia;
- wiersze 86–104: wykonywane po zakończeniu wszystkich zadań asynchronicznych;
- wiersz 89: przywracamy menu do stanu domyślnego;
- wiersze 91–94: jeśli zadania zakończyły się pomyślnie, następuje powrót do widoku [AGENDA] poprzez [SUBMIT] (w tym przypadku mogłaby to być również akcja typu NAVIGATION);
- wiersze 96–103: jeśli zadania zakończyły się anulowaniem, to i tak sprawdzamy, czy spotkanie zostało dodane (oznaczałoby to, że to pobranie nowego kalendarza zakończyło się niepowodzeniem);
- wiersze 98–99: jeśli spotkanie zostało dodane;
- wiersze 98–99: spotkanie zwrócone przez serwer jest dodawane do bieżącego kalendarza, czyli tego, który jest aktywny w sesji;
- wiersz 101: powrót do widoku [AGENDA] za pomocą [SUBMIT] (w tym przypadku mogłaby to być również akcja typu NAVIGATION);
3.7. Exécution
Przeprowadź następujące testy:
- użyj aplikacji w normalnych warunkach i sprawdź, czy działa;
- przekręć urządzenie dla każdego z widoków i sprawdź, czy każdy z nich jest poprawnie przywracany;
- ustawić czas oczekiwania na kilka sekund w [IMainActivity];
- następnie anuluj zadania i sprawdź, czy uzyskany wynik jest zgodny z oczekiwaniami;
- obróć urządzenie podczas okresów oczekiwania i sprawdź, czy zadania zostały prawidłowo anulowane i czy nie doszło do awarii;
- zmienić kolejność fragmentów w [IMainActivity] i sprawdzić, czy aplikacja nadal działa;












































