Revenons à l'architecture la plus générale des clients Android présentés jusqu'à maintenant, celle de l'exemple 2 :
La couche [Actions] est facultative dans tous les cas . On peut déporter la logique qu'on y trouve dans la vue. La vue [Vue] et l'action [Action] sont en effet tous les deux des boss [IBoss]. A ce titre, la vue peut employer des travailleurs [IWorker] quelconques dont les tâches asynchrones [Task]. Nous allons étudier un client Android plus complexe que les précédents avec cette architecture simplifiée :
Nous appelerons cette architecture AVT (Activité-Vues-Tâches).
X-A. Le projet▲
Dans le document " Introduction aux frameworks JSF2, Primefaces et Primefaces mobile " [http://tahe.developpez.com/java/primefaces/], on étudie une application de prise de rendez-vous pour des médecins. On y développe trois types d'interfaces :
- une interface web JSF2 ;
- une interface web Primefaces ;
- une interface web mobile pour smartphones avec Primefaces Mobile ;
Par la suite nous référencerons ce document [pfm] (Primefaces Mobile). Le projet sera ici de créer, sur tablette Android, une interface analogue aux précédentes.
L'interface JSF2 était la suivante :
La page d'accueil
A partir de cette première page, l'utilisateur (Secrétariat, Médecin) va engager un certain nombre d'actions. Nous les présentons ci-dessous. La vue de gauche présente la vue à partir de laquelle l'utilisateur fait une demande, la vue de droite la réponse envoyée par le serveur.
Enfin, on peut également obtenir une page d'erreurs :
L'application Android va présenter des écran analogues. Nous ne gérerons pas l'internationalisation de l'application mais nous faciliterons celle-ci en déportant les textes affichés dans des fichiers.
Il y aura cinq écrans :
Ecran de configuration
Ecran de choix du médecin du rendez-vous
Ecran de choix du créneau horaire du rendez-vous
Ecran de choix du client du rendez-vous
Après la prise de rendez-vous :
X-B. L'architecture du projet▲
On aura une architecture client / serveur analogue à celle de l'exemple 2 de ce document :
Le serveur
- nous allons réutiliser le travail fait dans [pfm] : les couches [métier] et [DAO], les entités JPA, la base de données MySQL ;
- notre travail consistera à exposer au monde web, via un service REST, l'interface de la couche [métier].
Le client Android
L'architecture de celui-ci a déjà été présentée :
Pour développer ce client, nous nous appuierons sur ce qui a été fait dans l'exemple 2 de ce document.
X-C. La base de données▲
Elle ne joue pas un rôle fondamental dans ce document. Nous la donnons à titre d'information.
On l'appellera [brdvmedecins2]. C'est une base de données MySQL5 avec quatre tables :
X-C-1. La table [MEDECINS]▲
Elle contient des informations sur les médecins gérés par l'application [RdvMedecins].
- ID : n° identifiant le médecin - clé primaire de la table
- VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
- NOM : le nom du médecin
- PRENOM : son prénom
- TITRE : son titre (Melle, Mme, Mr)
X-C-2. La table [CLIENTS]▲
Les clients des différents médecins sont enregistrés dans la table [CLIENTS] :
- ID : n° identifiant le client - clé primaire de la table
- VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
- NOM : le nom du client
- PRENOM : son prénom
- TITRE : son titre (Melle, Mme, Mr)
X-C-3. La table [CRENEAUX]▲
Elle liste les créneaux horaires où les RV sont possibles :
- ID : n° identifiant le créneau horaire - clé primaire de la table (ligne 8)
- VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
- ID_MEDECIN : n° identifiant le médecin auquel appartient ce créneau - clé étrangère sur la colonne MEDECINS(ID).
- HDEBUT : heure début créneau
- MDEBUT : minutes début créneau
- HFIN : heure fin créneau
- MFIN : minutes fin créneau
La seconde ligne de la table [CRENEAUX] (cf [1] ci-dessus) indique, par exemple, que le créneau n° 2 commence à 8 h 20 et se termine à 8 h 40 et appartient au médecin n° 1 (Mme Marie PELISSIER).
X-C-4. La table [RV]▲
Elle liste les RV pris pour chaque médecin :
- ID : n° identifiant le RV de façon unique - clé primaire
- JOUR : jour du RV
- ID_CRENEAU : créneau horaire du RV - clé étrangère sur le champ [ID] de la table [CRENEAUX] - fixe à la fois le créneau horaire et le médecin concerné.
- ID_CLIENT : n° du client pour qui est faite la réservation - clé étrangère sur le champ [ID] de la table [CLIENTS]
Cette table a une sur les valeurs des colonnes jointes (JOUR, ID_CRENEAU) :
ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);
Si une ligne de la table[RV] a la valeur (JOUR1, ID_CRENEAU1) pour les colonnes (JOUR, ID_CRENEAU), cette valeur ne peut se retrouver nulle part ailleurs. Sinon, cela signifierait que deux RV ont été pris au même moment pour le même médecin. D'un point de vue programmation Java, le pilote JDBC de la base lance une SQLException lorsque ce cas se produit.
La ligne d'id égal à 3 (cf [1] ci-dessus) signifie qu'un RV a été pris pour le créneau n° 20 et le client n° 4 le 23/08/2006. La table [CRENEAUX] nous apprend que le créneau n° 20 correspond au créneau horaire 16 h 20 - 16 h 40 et appartient au médecin n° 1 (Mme Marie PELISSIER). La table [CLIENTS] nous apprend que le client n° 4 est Melle Brigitte BISTROU.
X-C-5. Génération de la base▲
Pour créer les tables et les remplir on pourra utiliser le script [dbrdvmedecins2.sql] qu'on trouvera sur le site des exemples.
Avec [WampServer] (cf paragraphe , page ), on pourra procéder comme suit :
- en [1], on clique sur l'icône de [WampServer] et on choisit l'option [PhpMyAdmin] [2],
- en [3], dans la fenêtre sui s'est ouverte, on sélectionne le lien [Bases de données],
- en [2], on crée une base de données dont on a donné le nom [4] et l'encodage [5],
- en [7], la base a été créée. On clique sur son lien,
- en [8], on importe un fichier SQL,
- qu'on désigne dans le système de fichiers avec le bouton [9],
- en [11], on sélectionne le script SQL et en [12] on l'exécute,
- en [13], les quatre tables de la base ont été créées. On suit l'un des liens,
- en [14], le contenu de la table.
Par la suite, nous ne reviendrons plus sur cette base. Mais le lecteur est invité à suivre son évolution au fil des tests surtout lorsque ça ne marche pas.
X-D. Les projets Eclipse de l'application▲
L'application regroupe plusieurs projets Eclipse, tous des projets Maven :
- [android-avat] : le modèle AVAT. Fait ;
- [server-rdvmedecins-entities] : les entités JPA du serveur. Fait. Elles sont tirées du document [pfm] ;
- [server-rdvmedecins-dao] : la couche [DAO] du serveur. Fait. C'est celle du document [pfm] ;
- [server-rdvmedecins-metier] : la couche [métier] du serveur. Fait. C'est celle du document [pfm] ;
- [server-rdvmedecins-jsf] : la couche [JSF2] du serveur. Fait. C'est celle du document [pfm]. Elle sert de point de repère pour notre application. On essaie de reproduire son fonctionnement ;
- [server-rdvmedecins-rest] : la couche [REST] du serveur. A faire;
- [android-rdvmedecins-entities] : les entités du client. Ce sont celles du serveur débarrassées de leurs annotations JPA ;
- [android-rdvmedecins-dao] : la couche [DAO] du client Android. Ce sera celle de l'exemple 2 ;
- [android-rdvmedecins-metier] : la couche [métier] du client Android. A faire ;
- [android-rdvmedecins-ui] : la couche [présentation] du client Android. A faire ;
X-E. La couche [métier] du serveur▲
Revenons à l'architecture du serveur :
Nous devons écrire la couche [REST] qui expose au monde web la couche [métier]. Celle-ci présente l'interface [IMetier] suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
package
server.rdvmedecins.metier.service;
import
java.util.Date;
import
java.util.List;
import
server.rdvmedecins.entities.jpa.Client;
import
server.rdvmedecins.entities.jpa.Creneau;
import
server.rdvmedecins.entities.jpa.Medecin;
import
server.rdvmedecins.entities.jpa.Rv;
import
server.rdvmedecins.entities.metier.AgendaMedecinJour;
public
interface
IMetier {
public
List<
Client>
getAllClients
(
);
public
List<
Medecin>
getAllMedecins
(
);
public
List<
Creneau>
getAllCreneaux
(
Medecin medecin);
public
List<
Rv>
getRvMedecinJour
(
Medecin medecin, Date jour);
public
Client getClientById
(
Long id);
public
Medecin getMedecinById
(
Long id);
public
Rv getRvById
(
Long id);
public
Creneau getCreneauById
(
Long id);
public
Rv ajouterRv
(
Date jour, Creneau creneau, Client client);
public
void
supprimerRv
(
Rv rv);
public
AgendaMedecinJour getAgendaMedecinJour
(
Medecin medecin, Date jour);
}
- les méthodes des lignes 16 à 43 délèguent leur exécution à la couche [DAO]. Ce sont des méthodes d'accès aux données du SGBD ;
- ligne 46 : la seule méthode appartenant véritablement à la couche [métier].
Nous allons exposer ces méthodes via un service REST en changeant parfois leur signature afin de faciliter les échanges avec les clients. On suivra la règle suivante : plutôt que d'échanger un objet JPA avec une clé primaire, on échangera simplement la clé primaire et on ira chercher l'objet en base.
:
La classe d'exception [RdvMedecinsException] : elle est lancée par les méthodes de la couche [métier]
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
public
class
RdvMedecinsException extends
RuntimeException implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
int
code =
0
;
public
RdvMedecinsException
(
) {
super
(
);
}
public
RdvMedecinsException
(
String message) {
super
(
message);
}
public
RdvMedecinsException
(
String message, Throwable cause) {
super
(
message, cause);
}
public
RdvMedecinsException
(
Throwable cause) {
super
(
cause);
}
public
RdvMedecinsException
(
String message, int
code) {
super
(
message);
setCode
(
code);
}
public
RdvMedecinsException
(
Throwable cause, int
code) {
super
(
cause);
setCode
(
code);
}
public
RdvMedecinsException
(
String message, Throwable cause, int
code) {
super
(
message, cause);
setCode
(
code);
}
...
}
L'entité JPA [Personne] : représente un médecin ou un client. On ne reproduit pas les annotations JPA afin d'alléger le code.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public
class
Personne implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Long id;
private
String titre;
private
String nom;
private
int
version;
private
String prenom;
....
@
Override
public
String toString
(
) {
return
String.format
(
"
[%s,%s,%s,%s,%s]
"
, id, version, titre, prenom, nom);
}
}
La classe [Medecin] : représente un médecin
Sélectionnez 1.
2.
3.
4.
5.
public
class
Medecin extends
Personne implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
...
}
La classe [Client] : représente un client
Sélectionnez 1.
2.
3.
4.
5.
public
class
Client extends
Personne implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
...
}
La classe [Creneau] : représente un créneau horaire.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
public
class
Creneau implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Long id;
private
int
mdebut;
private
int
hfin;
private
int
hdebut;
private
int
mfin;
private
int
version;
private
Medecin medecin;
...
@
Override
public
String toString
(
) {
return
String.format
(
"
Creneau
[%s,
%s,
%s:%s,
%s:%s,%s]
"
, id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
La classe [Rv] : représente un rendez-vous.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public
class
Rv implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Long id;
private
Date jour;
private
Creneau creneau;
private
Client client;
...
@
Override
public
String toString
(
) {
return
String.format
(
"
Rv[%s,
%s,
%s]
"
, id, creneau, client);
}
}
La classe [AgendaMedecinJour] : l'agenda d'un médecin pour un jour donné.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public
class
AgendaMedecinJour implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Medecin medecin;
private
Date jour;
private
CreneauMedecinJour[] creneauxMedecinJour;
...
public
String toString
(
) {
StringBuffer str =
new
StringBuffer
(
"
"
);
for
(
CreneauMedecinJour cr : creneauxMedecinJour) {
str.append
(
"
"
);
str.append
(
cr.toString
(
));
}
return
String.format
(
"
Agenda[%s,%s,%s]
"
, medecin, new
SimpleDateFormat
(
"
dd/MM/yyyy
"
).format
(
jour), str.toString
(
));
}
}
La classe [CreneauMedecinJour] : représente l'occupation d'un créneau horaire.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public
class
CreneauMedecinJour implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Creneau creneau;
private
Rv rv;
...
@
Override
public
String toString
(
) {
return
String.format
(
"
[%s
%s]
"
, creneau,rv);
}
}
X-F. Le serveur REST▲
Le service REST est implémenté par SpringMVC. Un service REST (RepresEntational State Transfer) est un service HTTP répondant aux demandes GET, POST, PUT, DELETE d'un client HTTP. Sa définition formelle indique pour chacune de ces méthodes, les objets que le client doit envoyer et celui qu'il reçoit. Dans les exemples de ce document, nous n'utilisons que la méthode GET alors que dans certains cas, la définition formelle de REST indique qu'on devrait utiliser une méthode PUT ou DELETE. Nous appelons REST notre service parce qu'il est implémenté par un service de Spring qu'on a l'habitude d'appeler service REST et non parce qu'il respecte la définition formelle des services REST.
X-F-1. Le projet Eclipse▲
Le projet Eclipse du serveur REST est le suivant :
- en [1], le projet dans son ensemble. C'est un projet Maven de type web ;
- en [2], les dépendances Maven du projet. Elles sont très nombreuses. Le gros des archives est fourni par Hibernate qui assure la persistance des données en base et Spring qui assure l'intégration des couches ainsi que la gestion des transactions ;
- en [3], le projet présente des dépendances sur les couches [DAO] et [métier] du serveur ainsi que sur ses entités ;
- en [4], la couche REST est assurée par une classe.
X-F-2. Les dépendances Maven▲
Le fichier [pom.xml] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
<
project
xmlns
=
"
http://maven.apache.org/POM/4.0.0
"
xmlns
:
xsi
=
"
http://www.w3.org/2001/XMLSchema-instance
"
xsi
:
schemaLocation
=
"
http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd
"
>
<
modelVersion
>
4.0.0<
/
modelVersion
>
<
groupId
>
server.rdvmedecins<
/
groupId
>
<
artifactId
>
server-rdvmedecins-rest<
/
artifactId
>
<
version
>
1.0-SNAPSHOT<
/
version
>
<
packaging
>
war<
/
packaging
>
<
name
>
server-rdvmedecins-rest<
/
name
>
<
properties
>
<
endorsed
.
dir
>
${project.build.directory}/endorsed<
/
endorsed
.
dir
>
<
project
.
build
.
sourceEncoding
>
UTF-8<
/
project
.
build
.
sourceEncoding
>
<
releaseCandidate
>
1<
/
releaseCandidate
>
<
spring
.
version
>
3.1.1.RELEASE<
/
spring
.
version
>
<
jackson
.
mapper
.
version
>
1.5.6<
/
jackson
.
mapper
.
version
>
<
/
properties
>
<
repositories
>
<
repository
>
<
id
>
com.springsource.repository.bundles.release<
/
id
>
<
name
>
SpringSource Enterprise Bundle Repository - Release<
/
name
>
<
url
>
http://repository.springsource.com/maven/bundles/release<
/
url
>
<
/
repository
>
<
/
repositories
>
<
dependencies
>
<
dependency
>
<
groupId
>
org.springframework<
/
groupId
>
<
artifactId
>
spring-core<
/
artifactId
>
<
version
>
${spring.version}<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
org.springframework<
/
groupId
>
<
artifactId
>
spring-beans<
/
artifactId
>
<
version
>
${spring.version}<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
org.springframework<
/
groupId
>
<
artifactId
>
spring-web<
/
artifactId
>
<
version
>
${spring.version}<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
org.springframework<
/
groupId
>
<
artifactId
>
spring-webmvc<
/
artifactId
>
<
version
>
${spring.version}<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
org.codehaus.jackson<
/
groupId
>
<
artifactId
>
jackson-mapper-asl<
/
artifactId
>
<
version
>
${jackson.mapper.version}<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
javax.servlet<
/
groupId
>
<
artifactId
>
servlet-api<
/
artifactId
>
<
version
>
2.4<
/
version
>
<
/
dependency
>
<
dependency
>
<
groupId
>
server.rdvmedecins<
/
groupId
>
<
artifactId
>
server-rdvmedecins-metier<
/
artifactId
>
<
version
>
1.0-SNAPSHOT<
/
version
>
<
/
dependency
>
<
/
dependencies
>
<
build
>
<
plugins
>
...
<
/
plugins
>
<
/
build
>
<
/
project
>
- lignes 29-48 : les dépendances sur Spring ;
- lignes 49-53 : la dépendance sur la bibliothèque JSON Jackson ;
- lignes 54-58 : la dépendance sur l'API servlet ;
- lignes 59-63 : la dépendance sur la couche [métier] du serveur.
X-F-3. Configuration de la couche [REST]▲
Le service REST est configuré de façon analogue à celui de l'exemple 2.
X-F-3-a. Le fichier [web.xml]▲
Le fichier [web.xml] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<?
xml
version="1.0"
encoding="UTF-8"?
>
<
web-app
xmlns
:
xsi
=
"
http://www.w3.org/2001/XMLSchema-instance
"
xmlns
=
"
http://java.sun.com/xml/ns/javaee
"
xmlns
:
web
=
"
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd
"
xsi
:
schemaLocation
=
"
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd
"
id
=
"
WebApp_ID
"
version
=
"
2.5
"
>
<
display-name
>
REST for Medecins<
/
display-name
>
<
servlet
>
<
servlet-name
>
restservices<
/
servlet-name
>
<
servlet-class
>
org.springframework.web.servlet.DispatcherServlet<
/
servlet-class
>
<
init-param
>
<
param-name
>
contextConfigLocation<
/
param-name
>
<
param-value
>
/WEB-INF/rest-services-config.xml<
/
param-value
>
<
/
init-param
>
<
load-on-startup
>
1<
/
load-on-startup
>
<
/
servlet
>
<
servlet-mapping
>
<
servlet-name
>
restservices<
/
servlet-name
>
<
url-pattern
>
/<
/
url-pattern
>
<
/
servlet-mapping
>
<
/
web-app
>
Il est identique à celui de l'exemple 2 (page , paragraphe ).
X-F-3-b. Le fichier [rest-services-config.xml]▲
Le fichier [rest-services-config.xml] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
<
beans
xmlns
=
"
http://www.springframework.org/schema/beans
"
xmlns
:
xsi
=
"
http://www.w3.org/2001/XMLSchema-instance
"
xmlns
:
oxm
=
"
http://www.springframework.org/schema/oxm
"
xmlns
:
util
=
"
http://www.springframework.org/schema/util
"
xmlns
:
mvc
=
"
http://www.springframework.org/schema/mvc
"
xmlns
:
context
=
"
http://www.springframework.org/schema/context
"
xmlns
:
tx
=
"
http://www.springframework.org/schema/tx
"
xsi
:
schemaLocation
=
"
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/oxm
http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
"
>
<
bean
id
=
"
dao
"
class
=
"
server.rdvmedecins.dao.DaoJpa
"
/
>
<
bean
id
=
"
metier
"
class
=
"
server.rdvmedecins.metier.service.Metier
"
>
<
property
name
=
"
dao
"
ref
=
"
dao
"
/
>
<
/
bean
>
<
bean
id
=
"
entityManagerFactory
"
class
=
"
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
"
>
<
property
name
=
"
dataSource
"
ref
=
"
dataSource
"
/
>
<
property
name
=
"
jpaVendorAdapter
"
>
<
bean
class
=
"
org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter
"
>
<
property
name
=
"
databasePlatform
"
value
=
"
org.hibernate.dialect.MySQL5InnoDBDialect
"
/
>
<
/
bean
>
<
/
property
>
<
/
bean
>
<
bean
id
=
"
dataSource
"
class
=
"
org.apache.commons.dbcp.BasicDataSource
"
destroy-method
=
"
close
"
>
<
property
name
=
"
driverClassName
"
value
=
"
com.mysql.jdbc.Driver
"
/
>
<
property
name
=
"
url
"
value
=
"
jdbc:mysql://localhost:3306/dbrdvmedecins2
"
/
>
<
property
name
=
"
username
"
value
=
"
root
"
/
>
<
property
name
=
"
password
"
value
=
"
"
/
>
<
/
bean
>
<
tx
:
annotation-driven
transaction-manager
=
"
txManager
"
/
>
<
bean
id
=
"
txManager
"
class
=
"
org.springframework.orm.jpa.JpaTransactionManager
"
>
<
property
name
=
"
entityManagerFactory
"
ref
=
"
entityManagerFactory
"
/
>
<
/
bean
>
<
bean
class
=
"
org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor
"
/
>
<
bean
class
=
"
org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor
"
/
>
<
context
:
component-scan
base-package
=
"
server.rdvmedecins.rest
"
/
>
<
bean
class
=
"
org.springframework.web.servlet.view.json.MappingJacksonJsonView
"
>
<
property
name
=
"
contentType
"
value
=
"
text/plain
"
/
>
<
/
bean
>
<
bean
id
=
"
jsonMessageConverter
"
class
=
"
org.springframework.http.converter.json.MappingJacksonHttpMessageConverter
"
/
>
<
bean
class
=
"
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
"
>
<
property
name
=
"
messageConverters
"
>
<
util
:
list
id
=
"
beanList
"
>
<
ref
bean
=
"
jsonMessageConverter
"
/
>
<
/
util
:
list
>
<
/
property
>
<
/
bean
>
<
/
beans
>
- lignes 16-56 : configurent les couches [métier], [DAO], [JPA] du serveur. Cette configuration est expliquée dans le document [pfm] ;
- ligne 36-42 : définissent la source de données, la base MySQL. Pour ses propres tests, le lecteur sera peut être amené à changer ces informations ;
- lignes 58-82 : configurent le service REST. La configuration est analogue à ce qu'elle était dans l'exemple 2 (page , paragraphe ).
X-F-4. Implémentation de la couche [REST]▲
La couche REST est implémentée par la classe [Rest] suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
package
server.rdvmedecins.rest;
...
@
Controller
public
class
Rest {
@
Autowired
private
IMetier metier;
@
Autowired
private
View view;
@
RequestMapping
(
value =
"
/getAllClients
"
, method =
RequestMethod.GET)
public
ModelAndView getAllClients
(
) {
...
}
@
RequestMapping
(
value =
"
/getAllMedecins
"
, method =
RequestMethod.GET)
public
ModelAndView getAllMedecins
(
) {
...
}
@
RequestMapping
(
value =
"
/getAllCreneaux/{idMedecin}
"
, method =
RequestMethod.GET)
public
ModelAndView getAllCreneaux
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin) {
...
}
@
RequestMapping
(
value =
"
/getRvMedecinJour/{idMedecin}/{jour}
"
, method =
RequestMethod.GET)
public
ModelAndView getRvMedecinJour
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin, @
PathVariable
(
"
jour
"
) String jour) {
...
}
@
RequestMapping
(
value =
"
/getClientById/{idClient}
"
, method =
RequestMethod.GET)
public
ModelAndView getClientById
(
@
PathVariable
(
"
idClient
"
) long
idClient) {
...
}
@
RequestMapping
(
value =
"
/getMedecinById/{idMedecin}
"
, method =
RequestMethod.GET)
public
ModelAndView getMedecinById
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin) {
...
}
@
RequestMapping
(
value =
"
/getRvById/{idRv}
"
, method =
RequestMethod.GET)
public
ModelAndView getRvById
(
@
PathVariable
(
"
idRv
"
) long
idRv) {
...
}
@
RequestMapping
(
value =
"
/getCreneauById/{idCreneau}
"
, method =
RequestMethod.GET)
public
ModelAndView getCreneauById
(
@
PathVariable
(
"
idCreneau
"
) long
idCreneau) {
...
}
@
RequestMapping
(
value =
"
/ajouterRv/{jour}/{idCreneau}/{idClient}
"
, method =
RequestMethod.GET)
public
ModelAndView ajouterRv
(
@
PathVariable
(
"
jour
"
) String jour, @
PathVariable
(
"
idCreneau
"
) long
idCreneau,
@
PathVariable
(
"
idClient
"
) long
idClient) {
...
}
@
RequestMapping
(
value =
"
/supprimerRv/{idRv}
"
, method =
RequestMethod.GET)
public
ModelAndView supprimerRv
(
@
PathVariable
(
"
idRv
"
) long
idRv) {
...
}
@
RequestMapping
(
value =
"
/getAgendaMedecinJour/{idMedecin}/{jour}
"
, method =
RequestMethod.GET)
public
ModelAndView getAgendaMedecinJour
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin, @
PathVariable
(
"
jour
"
) String jour) {
...
}
private
ModelAndView createResponseError
(
String numero, String message) {
...
}
}
La classe suit les principes utilisés dans l'exemple 2 (page , paragraphe ).
- ligne 15 : l'URL permettant d'obtenir la listes des clients ;
- ligne 21 : l'URL permettant d'obtenir la listes des médecins ;
- ligne 27 : l'URL permettant d'obtenir la listes des créneaux horaires d'un médecin. Le paramètre est l'identifiant du médecin ;
- ligne 33 : l'URL permettant d'obtenir la liste des rendez-vous d'un médecin pour un jour donné. Les paramètres sont :
- l'identifiant du médecin,
- le jour ;
- ligne 39 : l'URL permettant d'obtenir un client identifié par son numéro ;
- ligne 46 : idem pour un médecin ;
- ligne 52 : idem pour un rendez-vous ;
- ligne 58 : idem pour un créneau horaire ;
- ligne 63 : l'URL pour ajouter un rendez-vous. Les paramètres passés dans l'URL sont :
- le jour du rendez-vous,
- le n° du créneau horaire du rendez-vous,
- le n° du client pour qui le rendez-vous est pris ;
- ligne 70 : l'URL pour supprimer un rendez-vous identifié par son n° ;
- ligne 76 : l'URL pour obtenir l'agenda d'un médecin pour un jour donné. Les paramètres de l'URL sont :
- le n° du médecin,
- le jour ;
- ligne 82 : une méthode privée qui génère une réponse en cas d'exception.
Voyons maintenant le code de ces méthodes.
Note : pour obtenir les copies d'écran ci-dessous, le SGBD doit être lancé.
X-F-4-a. Liste des clients▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@
RequestMapping
(
value =
"
/getAllClients
"
, method =
RequestMethod.GET)
public
ModelAndView getAllClients
(
) {
try
{
List<
Client>
clients =
metier.getAllClients
(
);
return
new
ModelAndView
(
view, "
data
"
, clients);
}
catch
(
Exception e) {
return
createResponseError
(
"
103
"
, e.getMessage
(
));
}
}
- ligne 2 : l'URL n'a pas de paramètres ;
- ligne 7 : la liste des clients est demandée à la couche [métier] ;
- ligne 9 : la réponse renvoyée au client. Le paramétrage du serveur REST étant le même que celui de l'exemple 2, le client recevra une réponse JSON contenant la liste des clients (voir copie d'écran ci-dessous) ;
- ligne 12 : la réponse envoyée en cas d'exception.
La méthode [createResponseError] est la méthode chargée d'envoyer la réponse au client en cas d'exception. Elle a deux paramètres :
- le n° de l'erreur,
- le message de l'erreur ;
Son code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
private
ModelAndView createResponseError
(
String numero, String message) {
Map<
String, Object>
modèle =
new
HashMap<
String, Object>
(
);
modèle.put
(
"
error
"
, numero);
modèle.put
(
"
message
"
, message);
return
new
ModelAndView
(
view, modèle);
}
Le client recevra une chaîne JSON de la forme :
Sélectionnez 1.
{
"
error
"
:"
54
"
,"
message:
"
..."
}
Voici un exemple d'exécution :
X-F-4-b. Liste des médecins▲
Le code est analogue :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@
RequestMapping
(
value =
"
/getAllMedecins
"
, method =
RequestMethod.GET)
public
ModelAndView getAllMedecins
(
) {
try
{
List<
Medecin>
medecins =
metier.getAllMedecins
(
);
return
new
ModelAndView
(
view, "
data
"
, medecins);
}
catch
(
Exception e) {
return
createResponseError
(
"
104
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-c. Liste des créneaux d'un médecin▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
@
RequestMapping
(
value =
"
/getAllCreneaux/{idMedecin}
"
, method =
RequestMethod.GET)
public
ModelAndView getAllCreneaux
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin) {
try
{
Medecin medecin =
metier.getMedecinById
(
idMedecin);
List<
Creneau>
creneaux =
metier.getAllCreneaux
(
medecin);
return
new
ModelAndView
(
view, "
data
"
, creneaux);
}
catch
(
Exception e) {
return
createResponseError
(
"
105
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution (vue partielle) :
X-F-4-d. Liste des rendez-vous d'un médecin▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
@
RequestMapping
(
value =
"
/getRvMedecinJour/{idMedecin}/{jour}
"
, method =
RequestMethod.GET)
public
ModelAndView getRvMedecinJour
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin, @
PathVariable
(
"
jour
"
) String jour) {
try
{
Date jourRv =
new
SimpleDateFormat
(
"
dd-MM-yy
"
).parse
(
jour);
Medecin medecin =
metier.getMedecinById
(
idMedecin);
List<
Rv>
rv =
metier.getRvMedecinJour
(
medecin, jourRv);
return
new
ModelAndView
(
view, "
data
"
, rv);
}
catch
(
Exception e) {
return
createResponseError
(
"
106
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-e. Obtenir un client identifié par son n°▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
@
RequestMapping
(
value =
"
/getClientById/{idClient}
"
, method =
RequestMethod.GET)
public
ModelAndView getClientById
(
@
PathVariable
(
"
idClient
"
) long
idClient) {
try
{
return
new
ModelAndView
(
view, "
data
"
, metier.getClientById
(
idClient));
}
catch
(
Exception e) {
return
createResponseError
(
"
107
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-f. Obtenir un médecin identifié par son n°▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
@
RequestMapping
(
value =
"
/getMedecinById/{idMedecin}
"
, method =
RequestMethod.GET)
public
ModelAndView getMedecinById
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin) {
try
{
return
new
ModelAndView
(
view, "
data
"
, metier.getMedecinById
(
idMedecin));
}
catch
(
Exception e) {
return
createResponseError
(
"
108
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-g. Obtenir un rendez-vous identifié par son n°▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
@
RequestMapping
(
value =
"
/getRvById/{idRv}
"
, method =
RequestMethod.GET)
public
ModelAndView getRvById
(
@
PathVariable
(
"
idRv
"
) long
idRv) {
try
{
return
new
ModelAndView
(
view, "
data
"
, metier.getRvById
(
idRv));
}
catch
(
Exception e) {
return
createResponseError
(
"
109
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-h. Obtenir un créneau horaire identifié par son n°▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
@
RequestMapping
(
value =
"
/getCreneauById/{idCreneau}
"
, method =
RequestMethod.GET)
public
ModelAndView getCreneauById
(
@
PathVariable
(
"
idCreneau
"
) long
idCreneau) {
try
{
return
new
ModelAndView
(
view, "
data
"
, metier.getCreneauById
(
idCreneau));
}
catch
(
Exception e) {
return
createResponseError
(
"
110
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-i. Ajouter un rendez-vous▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
@
RequestMapping
(
value =
"
/ajouterRv/{jour}/{idCreneau}/{idClient}
"
, method =
RequestMethod.GET)
public
ModelAndView ajouterRv
(
@
PathVariable
(
"
jour
"
) String jour, @
PathVariable
(
"
idCreneau
"
) long
idCreneau,
@
PathVariable
(
"
idClient
"
) long
idClient) {
try
{
Date jourRv =
new
SimpleDateFormat
(
"
dd-MM-yyyy
"
).parse
(
jour);
Client client =
metier.getClientById
(
idClient);
Creneau creneau =
metier.getCreneauById
(
idCreneau);
Rv rv =
metier.ajouterRv
(
jourRv, creneau, client);
return
new
ModelAndView
(
view, "
data
"
, rv);
}
catch
(
Exception e) {
return
createResponseError
(
"
111
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
On notera :
- le n° (2) du rendez-vous créé ;
- le format JSON du jour. Cela nous posera ultérieurement des problèmes car le client Gson ne s'attend pas à recevoir une date sous cette forme. Il doit être possible de fixer la forme JSON d'une date. Cela n'a pas été fait ici.
X-F-4-j. Supprimer un rendez-vous▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
@
RequestMapping
(
value =
"
/supprimerRv/{idRv}
"
, method =
RequestMethod.GET)
public
ModelAndView supprimerRv
(
@
PathVariable
(
"
idRv
"
) long
idRv) {
try
{
Rv rv =
metier.getRvById
(
idRv);
metier.supprimerRv
(
rv);
return
new
ModelAndView
(
view, "
data
"
, "
OK
"
);
}
catch
(
Exception e) {
return
createResponseError
(
"
112
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
X-F-4-k. Obtenir l'agenda d'un médecin▲
Le code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@
RequestMapping
(
value =
"
/getAgendaMedecinJour/{idMedecin}/{jour}
"
, method =
RequestMethod.GET)
public
ModelAndView getAgendaMedecinJour
(
@
PathVariable
(
"
idMedecin
"
) long
idMedecin, @
PathVariable
(
"
jour
"
) String jour) {
try
{
Date jourRv =
new
SimpleDateFormat
(
"
dd-MM-yyyy
"
).parse
(
jour);
Medecin medecin =
metier.getMedecinById
(
idMedecin);
AgendaMedecinJour agenda =
metier.getAgendaMedecinJour
(
medecin, jourRv);
System.out.println
(
String.format
(
"
jourRv=%s,
jour=%s
"
,jourRv, agenda.getJour
(
)));
return
new
ModelAndView
(
view, "
data
"
, agenda);
}
catch
(
Exception e) {
return
createResponseError
(
"
113
"
, e.getMessage
(
));
}
}
Voici un exemple d'exécution :
- en [1], le format JSON du jour de l'agenda ;
- en [2], le format JSON du jour d'un rendez-vous. Ce ne sont pas les mêmes. Il faudra gérer ces différences côté client.
X-G. La couche [DAO] du client Android▲
X-G-1. Le projet Eclipse▲
Le projet Eclipse de la couche [DAO] du client Android est le suivant :
- en [1], le projet Maven de la couche [DAO] a une dépendance sur le projet [android-rdvmedecins-entities]. Ce projet rassemble les entités décrites page , paragraphe ;
- en [2], la couche [DAO] du client Android.
L'interface [IDao] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
package
android.rdvmedecins.dao;
import
java.util.Map;
public
interface
IDao {
public
String executeRestService
(
String method, String urlService, Object request, Map<
String, String>
paramètres);
public
void
setTimeout
(
int
millis);
}
Cette interface et son implémentation sont identiques à ce qu'elles étaient dans la couche [DAO] de l'exemple 2 page , paragraphe .
X-H. La couche [métier] du client Android▲
X-H-1. Le projet Eclipse▲
Le projet Eclipse de la couche [métier] du client Android est le suivant :
- en [1], le projet Maven de la couche [métier] a une unique dépendance, celle sur la couche [DAO] du client. Les autres apparaissent par cascade ;
- en [2], la couche [métier] du client Android.
X-H-2. Implémentation▲
L'interface [IMetier] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
package
android.rdvmedecins.metier;
import
java.util.List;
import
android.rdvmedecins.dao.IDao;
import
android.rdvmedecins.entities.dao.Client;
import
android.rdvmedecins.entities.dao.Creneau;
import
android.rdvmedecins.entities.dao.Medecin;
import
android.rdvmedecins.entities.dao.Rv;
import
android.rdvmedecins.entities.metier.AgendaMedecinJour;
public
interface
IMetier {
public
abstract
List<
Client>
getAllClients
(
);
public
abstract
List<
Medecin>
getAllMedecins
(
);
public
abstract
List<
Creneau>
getAllCreneaux
(
Long idMedecin);
public
abstract
List<
Rv>
getRvMedecinJour
(
Long idMedecin, String jour);
public
abstract
Client getClientById
(
Long idClient);
public
abstract
Medecin getMedecinById
(
Long idMedecin);
public
abstract
Rv getRvById
(
Long idRv);
public
abstract
Creneau getCreneauById
(
Long idCreneau);
public
abstract
Rv ajouterRv
(
String jour, Long idCreneau, Long idClient);
public
abstract
void
supprimerRv
(
Long idRv);
public
abstract
AgendaMedecinJour getAgendaMedecinJour
(
Long idMedecin, String jour);
public
abstract
void
setDao
(
IDao dao);
public
abstract
void
setUrlServiceRest
(
String urlServiceRest);
}
- l'interface [IMetier] du client Android correspond aux méthodes exposées par le service REST du serveur. On rappelle ainsi pour chaque méthode, l'URL du service REST visée.
L'implémentation [Metier] de cette interface est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
package
android.rdvmedecins.metier;
...
public
class
Metier implements
IMetier {
private
IDao dao;
private
Gson gson =
new
Gson
(
);
private
String urlServiceRest;
public
List<
Client>
getAllClients
(
) {
String urlService =
String.format
(
"
http://%s/getAllClients
"
, urlServiceRest);
String réponse =
dao.executeRestService
(
"
get
"
, urlService, null
, new
HashMap<
String, String>
(
));
try
{
return
gson.fromJson
(
réponse, new
TypeToken<
List<
Client>
>
(
) {
}
.getType
(
));
}
catch
(
Exception ex) {
throw
new
RdvMedecinsException
(
String.format
(
"
Réponse
incorrecte
du
serveur:
%s
"
, réponse), ex, 20
);
}
}
public
List<
Medecin>
getAllMedecins
(
) {
...
}
...
...
}
- ligne 8 : une référence sur la couche [DAO]. Sera instanciée par la fabrique du modèle AVT ;
- ligne 10 : la bibliothèque JSON utilisée côté client Android est la bibliothèque Gson de Google ;
- ligne 12 : l'URL du service REST interrogé par le client Android. Est initialisée lorsque l'utilisateur donne cette information ;
- ligne 15 : la méthode d'obtention de la liste des clients ;
- ligne 17 : construction de l'URL complète de la méthode REST à appeler ;
- ligne 19 : on demande à la couche [DAO] d'appeler cette méthode. On rappelle les quatre paramètres de la méthode [executeRestService] de la couche [DAO] :
- la méthode " get " ou " post " de la requête HTTP à émettre,
- l'URL complète de la méthode REST à exécuter,
- un dictionnaire des données transmises par une opération HTTP POST. Donc null ici, puisqu'on fait une opération HTTP GET,
- sous la forme d'un dictionnaire, les valeurs des variables de l'URL. Ici il n'y a pas de variables dans l'URL. On fournit alors un dictionnaire vide. Il ne peut être null ;
On ne gère pas d'exception. La couche [DAO] lance des exceptions non contrôlées. Elles vont remonter toutes seules jusqu'à la couche [Présentation] assurée par le modèle AVAT ;
- ligne 22 : on reçoit de la couche [DAO] une chaîne JSON représentant une liste de clients. Cette chaîne est alors utilisée pour créer un objet de type List<Client> (ligne 15) ;
- ligne 25 : au cas où la chaîne n'a pu être transformée en un objet de type List<Client> une exception est lancée.
Toutes les méthodes de la couche [métier] suivent la même logique :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public
T méthode
(
T1 param1, T2 param2,...) {
String urlService =
String.format
(
"
http://%s/unCheminAvecOuPasDeVariables
"
, urlServiceRest);
Map<
String, String>
paramètres =
new
HashMap<
String, String>
(
);
paramètres.put
(
"
clé1
"
, objet1);
paramètres.put
(
"
clé2
"
, objet2);
String réponse =
dao.executeRestService
(
"
get
"
, urlService, null
, paramètres);
try
{
return
gson.fromJson
(
réponse, new
TypeToken<
T>
(
) {
}
.getType
(
));
}
catch
(
Exception ex) {
throw
new
RdvMedecinsException
(
String.format
(
"
Réponse
incorrecte
du
serveur:
%s
"
, réponse), ex, 20
);
}
}
Prenons par exemple la méthode [getAllCreneaux]
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
public
List<
Creneau>
getAllCreneaux
(
Long idMedecin) {
String urlService =
String.format
(
"
http://%s/getAllCreneaux/{idMedecin}
"
, urlServiceRest);
Map<
String, String>
paramètres =
new
HashMap<
String, String>
(
);
paramètres.put
(
"
idMedecin
"
, String.valueOf
(
idMedecin));
String réponse =
dao.executeRestService
(
"
get
"
, urlService, null
, paramètres);
try
{
return
gson.fromJson
(
réponse, new
TypeToken<
List<
Creneau>
>
(
) {
}
.getType
(
));
}
catch
(
Exception ex) {
throw
new
RdvMedecinsException
(
String.format
(
"
Réponse
incorrecte
du
serveur:
%s
"
, réponse), ex, 22
);
}
}
- ligne 1 : l'URL interrogée. Elle a une variable, le n° [idMedecin] du médecin ;
- ligne 5 : l'URL complète est construite ;
- lignes 7-8 : un dictionnaire est construit pour affecter des valeurs aux variables de l'URL, ici une seule variable ;
- ligne 10 : la requête au service REST est faite par la couche [DAO] ;
- ligne 13 : on rend la réponse, ici un objet de type List<Creneau> ;
- ligne 16 : si la chaîne JSON n'a pu être convertie en un objet de type List<Creneau> une exception est lancée.
Nous n'allons pas décrire la totalité des méthodes. Le lecteur intéressé les trouvera dans les codes source qui accompagnent ce document. Certaines méthodes ont été plus difficiles à écrire que d'autres à cause de la représentation JSON de l'objet de type [java.util.Date] envoyée par la bibliothèque Jackson du serveur. Elle n'a pas été reconnue par la bibliothèque Gson du client Android et on avait alors une exception. Dans ces cas, il a fallu procéder en trois temps :
- désérialiser la chaîne JSON reçue en un objet Map<String,Object> ;
- remplacer dans ce dictionnaire les jours par le bon format ;
- resérialiser le nouveau dictionnaire en l'objet T que doit rendre la méthode.
C'est assez lourd et présente peu d'intérêt mais c'est un bon exercice de sérialisation / désérialisation d'un objet JSON.
X-I. La couche [AVT]▲
On aborde ici la couche AVT (Activité - Vues - Tâches) du client Android.
X-I-1. Le projet Eclipse▲
Le projet Eclipse est le suivant :
- en [1], un projet Maven de type Android ;
- en [2] les dépendances Maven. Il y en a deux :
- celle sur le modèle [AVT] ;
- celle sur la couche [métier] que nous venons d'écrire ;
Les autres dépendances découlent des deux précédentes.
- en [3], les codes source :
- en [3] : l'activité, la fabrique, la configuration et la session ;
- en [4] : les tâches asynchrones ;
- en [5] : les vues.
X-I-2. L'activité Android▲
La classe [MainActivity] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
package
android.rdvmedecins.ui.activity;
...
public
class
MainActivity extends
FragmentActivity {
private
Factory factory;
private
Vue configVue;
@
Override
protected
void
onCreate
(
Bundle savedInstanceState) {
super
.onCreate
(
savedInstanceState);
requestWindowFeature
(
Window.FEATURE_INDETERMINATE_PROGRESS);
setProgressBarIndeterminateVisibility
(
false
);
setContentView
(
R.layout.main);
factory =
new
Factory
(
this
, new
Config
(
));
configVue =
(
Vue) factory.getObject
(
Factory.CONFIG_VUE, null
, "
ConfigVue
"
);
showVue
(
configVue);
}
public
void
showVue
(
Vue vue) {
FragmentTransaction fragmentTransaction =
getFragmentManager
(
).beginTransaction
(
);
fragmentTransaction.replace
(
R.id.container, vue);
fragmentTransaction.commit
(
);
}
}
Elle a les fonctionnalités suivantes :
- ligne 21 : créer la fabrique d'objets en lui passant la classe de configuration de l'application ;
- lignes 23-25 : afficher la première vue. Celle-ci est la suivante :
X-I-3. La classe de configuration▲
La classe [Config] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
package
android.rdvmedecins.ui.activity;
public
class
Config {
private
boolean
verbose =
true
;
private
int
timeout=
1000
;
...
}
Elle est identique à ce qu'elle était dans l'exemple 2.
X-I-4. Le patron des vues▲
Toutes les vues auront le format précédent imposé par le fichier [main.xml] de l'activité :
Le fichier [main.xml] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
<
LinearLayout
xmlns
:
android
=
"
http://schemas.android.com/apk/res/android
"
xmlns
:
tools
=
"
http://schemas.android.com/tools
"
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
match_parent
"
android
:
gravity
=
"
center
"
android
:
orientation
=
"
vertical
"
>
<
LinearLayout
android
:
id
=
"
@+id/header
"
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
100dp
"
android
:
layout_weight
=
"
0.1
"
android
:
background
=
"
@color/lavenderblushh2
"
>
<
TextView
android
:
id
=
"
@+id/textViewHeader
"
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
wrap_content
"
android
:
layout_gravity
=
"
center
"
android
:
gravity
=
"
center_horizontal
"
android
:
text
=
"
@string/txt_header
"
android
:
textAppearance
=
"
?android:attr/textAppearanceLarge
"
android
:
textColor
=
"
@color/blue
"
/
>
<
/
LinearLayout
>
<
LinearLayout
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
fill_parent
"
android
:
layout_weight
=
"
0.8
"
android
:
orientation
=
"
horizontal
"
>
<
LinearLayout
android
:
id
=
"
@+id/left
"
android
:
layout_width
=
"
100dp
"
android
:
layout_height
=
"
match_parent
"
android
:
background
=
"
@color/lightcyan2
"
android
:
orientation
=
"
vertical
"
>
<
TextView
android
:
id
=
"
@+id/lnk_Config
"
android
:
layout_width
=
"
fill_parent
"
android
:
layout_height
=
"
40dp
"
android
:
layout_marginTop
=
"
100dp
"
android
:
gravity
=
"
center_vertical|center_horizontal
"
android
:
text
=
"
@string/lnk_config
"
android
:
textColor
=
"
@color/blue
"
/
>
<
TextView
android
:
id
=
"
@+id/lnk_Accueil
"
android
:
layout_width
=
"
fill_parent
"
android
:
layout_height
=
"
40dp
"
android
:
gravity
=
"
center_vertical|center_horizontal
"
android
:
text
=
"
@string/lnk_accueil
"
android
:
textColor
=
"
@color/blue
"
/
>
<
TextView
android
:
id
=
"
@+id/lnk_Agenda
"
android
:
layout_width
=
"
fill_parent
"
android
:
layout_height
=
"
40dp
"
android
:
gravity
=
"
center_vertical|center_horizontal
"
android
:
text
=
"
@string/lnk_agenda
"
android
:
textColor
=
"
@color/blue
"
/
>
<
TextView
android
:
id
=
"
@+id/lnk_Ajout
"
android
:
layout_width
=
"
fill_parent
"
android
:
layout_height
=
"
40dp
"
android
:
gravity
=
"
center_vertical|center_horizontal
"
android
:
text
=
"
@string/lnk_ajout
"
android
:
textColor
=
"
@color/blue
"
/
>
<
/
LinearLayout
>
<
FrameLayout
android
:
id
=
"
@+id/container
"
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
match_parent
"
android
:
layout_marginLeft
=
"
20dp
"
android
:
background
=
"
@color/floral_white
"
tools
:
context
=
"
.MainActivity
"
tools
:
ignore
=
"
MergeRootFrame
"
>
<
/
FrameLayout
>
<
/
LinearLayout
>
<
LinearLayout
android
:
id
=
"
@+id/bottom
"
android
:
layout_width
=
"
match_parent
"
android
:
layout_height
=
"
100dp
"
android
:
layout_weight
=
"
0.1
"
android
:
background
=
"
@color/wheat1
"
>
<
TextView
android
:
id
=
"
@+id/textViewBottom
"
android
:
layout_width
=
"
fill_parent
"
android
:
layout_height
=
"
fill_parent
"
android
:
gravity
=
"
center_vertical|center_horizontal
"
android
:
text
=
"
@string/txt_bottom
"
android
:
textColor
=
"
@color/blue
"
/
>
<
/
LinearLayout
>
<
/
LinearLayout
>
et correspond à la vue suivante :
X-I-5. La vue [Config]▲
C'est la première vue affichée. Elle est composée des éléments suivants :
N°
|
Id
|
Type
|
Rôle
|
1
|
edtUrlServiceRest
|
EditText
|
URL du service REST
|
2
|
btnValider
|
Button
|
tente une connexion au serveur REST
|
2
|
btnAnnuler
|
Button
|
annule la connexion
|
Le code de la vue est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
package
android.rdvmedecins.ui.vues;
...
public
class
ConfigVue extends
LocalVue {
private
Button btnValider;
private
Button btnAnnuler;
private
EditText edtUrlServiceRest;
private
TextView txtErrorUrlServiceRest;
private
List<
Object>
results =
new
ArrayList<
Object>
(
);
private
String urlServiceRest;
@
Override
public
View onCreateView
(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return
inflater.inflate
(
R.layout.config, container, false
);
}
@
Override
public
void
onActivityCreated
(
Bundle savedInstanceState) {
...
btnAnnuler =
(
Button) activity.findViewById
(
R.id.btn_Annuler);
btnAnnuler.setOnClickListener
(
new
OnClickListener
(
) {
@
Override
public
void
onClick
(
View arg0) {
cancelAll
(
);
cancelWaiting
(
);
}
}
);
btnValider.setVisibility
(
View.VISIBLE);
btnAnnuler.setVisibility
(
View.INVISIBLE);
}
@
Override
public
void
onResume
(
) {
super
.onResume
(
);
activateLinks
(
0
);
}
protected
void
doValider
(
) {
...
ITask getAllMedecinsTask =
(
ITask) factory.getObject
(
Factory.GETALLMEDECINS_TASK, this
, "
GetAllMedecinsTask
"
);
getAllMedecinsTask.doWork
(
edtUrlServiceRest.getText
(
).toString
(
));
ITask getAllClientsTask =
(
ITask) factory.getObject
(
Factory.GETALLCLIENTS_TASK, this
, "
GetAllClientsTask
"
);
getAllClientsTask.doWork
(
edtUrlServiceRest.getText
(
).toString
(
));
beginWaiting
(
);
beginMonitoring
(
);
}
private
boolean
isPageValid
(
) {
...
}
@
Override
public
void
notifyEndOfTasks
(
) {
cancelWaiting
(
);
Toast.makeText
(
activity, "
Travail
terminé
"
, Toast.LENGTH_LONG).show
(
);
boolean
erreur =
false
;
for
(
Object result : results) {
if
(
result instanceof
Map<
?, ?>
) {
@
SuppressWarnings
(
"
unchecked
"
)
Map<
String, Object>
map =
(
Map<
String, Object>
) result;
String clé =
map.keySet
(
).iterator
(
).next
(
);
session.add
(
clé, map.get
(
clé));
}
else
if
(
result instanceof
Exception) {
showException
(
(
Exception) result);
erreur =
true
;
break
;
}
}
if
(
!
erreur) {
Vue accueilVue =
(
Vue) factory.getObject
(
Factory.ACCUEIL_VUE, null
, "
AccueilVue
"
);
mainActivity.showVue
(
accueilVue);
}
}
@
Override
public
void
notifyEvent
(
IWorker worker, int
eventType, Object event) {
super
.notifyEvent
(
worker, eventType, event);
if
(
eventType =
=
IBoss.WORK_INFO) {
results.add
(
event);
}
}
private
void
beginWaiting
(
) {
...
}
protected
void
cancelWaiting
(
) {
...
}
}
- ligne 5 : la vue dérive de la vue [LocalVue]. Cette vue sera le parent de toutes les vues de l'application. On y rassemble tout ce qui peut être factorisé entre vues ;
- lignes 30-38 : la gestion du clic sur le bouton [Annuler]. C'est celui désormais classique de tous les exemples vus dans ce document. Nous n'y reviendrons plus ;
- lignes 46-51 : la méthode [onResume] est exécutée juste avant l'affichage de la vue. On l'utilisera dans chaque vue pour deux choses :
- récupérer des informations dans la session afin de les afficher ;
- activer certains liens du bandeau gauche.
- ligne 50 : la méthode activateLinks(int i) appartient à la classe parent [LocalVue]. Elle fait en sorte que les liens du bandeau gauche de n° <i soient cliquables et de couleur bleue, les autres étant désactivés et de couleur noire. Cela permet à l'utilisateur de revenir en arrière dans l'assistant de prise de rendez-vous. activateLinks(0) va ici désactiver tous les liens.
- ligne 57 : on instancie la tâche qui va demander la liste des médecins au serveur REST. Cette liste restera en session ;
- ligne 61 : la tâche est lancée ;
- lignes 60-63 : on lance une seconde tâche, cette fois pour obtenir la liste des clients qui sera mémorisée elle aussi en session ;
- ligne 65 : début de l'attente ;
- ligne 67 : on surveille la fin de ces deux tâches ;
- ligne 110 : la vue [Config] va voir passer les notifications des deux tâches lancées ;
- lignes 114-116 : dans le cas d'une notification [WORK_INFO], on mémorise l'information dans la liste d'objets de la ligne 14. Pour chacune des deux tâches, l'information enregistrée est un dictionnaire :
- avec la clé " client " et une valeur de type List<Client> pour la liste des clients,
- avec la clé " medecin " et une valeur de type List<Medecin> pour la liste des médecins ;
- ligne 75 : on est prévenu de la fin des deux tâches ;
- ligne 77 : on annule l'attente ;
- ligne 81 : on exploite les deux informations stockées dans la liste des résultats ;
- ligne 83 : on regarde si le résultat obtenu est bien un dictionnaire. Si oui, il est mis dans la session (lignes 84-89). Sinon on regarde si le résultat est une exception (ligne 90). Si oui, elle est affichée (ligne 92) par une méthode de la classe [LocalVue] ;
- ligne 99 : s'il n'y a pas eu d'erreurs, la liste des clients et celle des médecins sont maintenant en session. C'est là qu'iront les chercher les vues qui en ont besoin ;
- lignes 101-103 : s'il n'y a pas eu d'erreur, la vue suivante de l'assistant est affichée ;
On notera les points suivants :
- on trouve désormais un peu de logique dans la vue (lignes 56-63). La vue doit " savoir " qu'elle doit appeler deux tâches asynchrones et dans quel ordre. Auparavant, cette connaissance était dans l'action. On s'écarte un peu de la notion de séparation des tâches (separation of concern) mais peu. Il y aura toujours très peu de logique dans la vue, celle-ci étant concentrée dans la couche [métier] ;
- lignes 56-63, on appelle deux tâches asynchrones qui vont s'exécuter en parallèle. On aurait pu écrire une méthode dans la couche [métier] pour récupérer les deux listes, médecins et clients. Une tâche asynchrone aurait alors suffi pour l'exécuter et on aurait mieux atteint l'objectif de séparation des tâches. Mais dans ce cas les deux listes auraient été obtenues l'une après l'autre et non pas en parallèle.
X-I-6. La vue [LocalVue]▲
La vue [LocalVue] est parente de toutes les vues de l'application. On y a rassemblé tout ce qui pouvait être factorisé entre vues. Son code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
package
android.rdvmedecins.ui.vues;
...
public
abstract
class
LocalVue extends
Vue {
protected
ISession session;
protected
MainActivity mainActivity;
protected
TextView lnkConfig;
protected
TextView lnkAccueil;
protected
TextView lnkAgenda;
protected
TextView lnkAjout;
protected
TextView[] links;
public
LocalVue
(
) {
}
@
Override
public
void
onActivityCreated
(
Bundle savedInstanceState) {
super
.onActivityCreated
(
savedInstanceState);
session =
(
ISession) factory.getObject
(
Factory.SESSION, (
Object[]) null
);
mainActivity =
(
MainActivity) activity;
lnkConfig =
(
TextView) activity.findViewById
(
R.id.lnk_Config);
lnkAccueil =
(
TextView) activity.findViewById
(
R.id.lnk_Accueil);
lnkAgenda =
(
TextView) activity.findViewById
(
R.id.lnk_Agenda);
lnkAjout =
(
TextView) activity.findViewById
(
R.id.lnk_Ajout);
links =
new
TextView[] {
lnkConfig, lnkAccueil, lnkAgenda, lnkAjout }
;
}
protected
void
navigateToView
(
View v) {
...
}
protected
void
activateLinks
(
int
idLink) {
...
}
protected
void
showException
(
Exception ex) {
DialogFragment dialog =
new
ExceptionVue
(
ex);
dialog.show
(
getFragmentManager
(
), "
exception
"
);
}
protected
void
showHourGlass
(
) {
activity.setProgressBarIndeterminateVisibility
(
true
);
}
protected
void
hideHourGlass
(
) {
activity.setProgressBarIndeterminateVisibility
(
false
);
}
@
Override
abstract
public
void
notifyEndOfTasks
(
);
}
- ligne 5 : [LocalVue] étend la vue abstraite [Vue] qui implémente l'interface [IVue] ;
- ligne 65 : la méthode abstraite [notifyEndOfTasks] est implémentée par les classes filles ;
- ligne 56 : pour afficher le sablier ;
- ligne 60 : pour le cacher ;
- ligne 49 : la méthode d'affichage des exceptions qui remontent jusqu'aux vues. On utilise une boîte de dialogue telle que la suivante :
- ligne 51 : on utilise la classe [ExceptionVue] suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
package
android.rdvmedecins.ui.vues;
...
public
class
ExceptionVue extends
DialogFragment {
private
Exception ex;
public
ExceptionVue
(
) {
}
public
ExceptionVue
(
Exception ex) {
this
.ex =
ex;
}
@
Override
public
Dialog onCreateDialog
(
Bundle savedInstanceState) {
String message =
ex.getMessage
(
);
Throwable cause =
ex.getCause
(
);
while
(
cause !
=
null
) {
message +
=
String.format
(
"
\n[%s,
%s]
"
, cause.getClass
(
).getCanonicalName
(
), cause.getMessage
(
));
cause =
cause.getCause
(
);
}
AlertDialog.Builder builder =
new
AlertDialog.Builder
(
getActivity
(
));
builder.setTitle
(
R.string.dialog_show_exception).setMessage
(
message)
.setPositiveButton
(
R.string.dialog_show_exception_close, new
DialogInterface.OnClickListener
(
) {
public
void
onClick
(
DialogInterface dialog, int
id) {
}
}
);
return
builder.create
(
);
}
}
- ligne 6 : la classe étend [DialogFragment] qui permet d'afficher des boîtes de dialogue ;
- lignes 16-18 : le constructeur reçoit en paramètre l'exception à afficher ;
- ligne 21 : la méthode [onCreateDialog] est exécutée à la création du dialogue avant son affichage ;
- lignes 23-28 : on construit le message que l'on va afficher dans la boîte de dialogue. C'est la concaténation des messages d'erreur de toutes les exceptions contenues dans l'exception passée au constructeur ;
- ligne 30 : on construit une boîte de dialogue prédéfinie [AlertDialog] ;
- ligne 31 :
- setTitle : fixe le titre de la boîte de dialogue [1] ;
- setMessage : fixe le message que la boîte de dialogue va afficher [2] ;
- setPositiveButton : fixe la nature du bouton qui va permettre de fermer la boîte de dialogue [3] ;
- ligne 38 : on doit rendre la boîte de dialogue qui vient d'être créée.
Revenons au code de [showException] :
Sélectionnez 1.
2.
3.
4.
5.
6.
protected
void
showException
(
Exception ex) {
DialogFragment dialog =
new
ExceptionVue
(
ex);
dialog.show
(
getFragmentManager
(
), "
exception
"
);
}
- ligne 4 : la boîte de dialogue est instanciée mais pas créée ;
- ligne 5 : la boîte est créée et affichée.
Revenons au code de [LocalVue] :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
public
abstract
class
LocalVue extends
Vue {
protected
ISession session;
protected
MainActivity mainActivity;
protected
TextView lnkConfig;
protected
TextView lnkAccueil;
protected
TextView lnkAgenda;
protected
TextView lnkAjout;
protected
TextView[] links;
public
LocalVue
(
) {
}
@
Override
public
void
onActivityCreated
(
Bundle savedInstanceState) {
super
.onActivityCreated
(
savedInstanceState);
session =
(
ISession) factory.getObject
(
Factory.SESSION, (
Object[]) null
);
mainActivity =
(
MainActivity) activity;
lnkConfig =
(
TextView) activity.findViewById
(
R.id.lnk_Config);
lnkAccueil =
(
TextView) activity.findViewById
(
R.id.lnk_Accueil);
lnkAgenda =
(
TextView) activity.findViewById
(
R.id.lnk_Agenda);
lnkAjout =
(
TextView) activity.findViewById
(
R.id.lnk_Ajout);
links =
new
TextView[] {
lnkConfig, lnkAccueil, lnkAgenda, lnkAjout }
;
}
- ligne 3 : la session qui sert aux échanges d'informations entre vues ;
- ligne 5 : une référence sur l'activité Android qui sous-tend les vues ;
- lignes 7-10 : les quatre liens du bandeau gauche des vues :
- ligne 11 : un tableau contenant ces quatre liens ;
- ligne 22 : récupère une référence sur la session (singleton) ;
- ligne 24 : récupère la nature exacte de l'activité. activity est une référence injectée dans la classe abstraite [Vue] dont dérive [LocalVue] et est de type [Activity]. Afin d'éviter des transtypages dans le code des vues, on crée un champ mainActivity avec le bon type ;
- lignes 26-29 : on récupère les références des quatre liens ;
- ligne 31 : on les stocke dans le tableau.
L'activation / désactivation des liens se fait avec la méthode suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
protected
void
activateLinks
(
int
idLink) {
int
i =
0
;
while
(
i <
idLink) {
links[i].setTextColor
(
getResources
(
).getColor
(
R.color.blue));
links[i].setOnClickListener
(
new
OnClickListener
(
) {
@
Override
public
void
onClick
(
View v) {
navigateToView
(
v);
}
}
);
i+
+
;
}
while
(
i <
links.length) {
links[i].setTextColor
(
getResources
(
).getColor
(
R.color.black));
links[i].setOnClickListener
(
null
);
i+
+
;
}
}
- ligne 2 : le paramètre de la méthode est un n° de lien. La méthode active tous les liens qui ont un n° <idLink et désactivent tous les autres ;
- ligne 6 : le lien activé sera de couleur bleur ;
- lignes 7-14 : le clic sur ce lien est géré ;
- ligne 19 : un lien désactivé sera de couleur noire ;
- ligne 20 : le clic sur le lien n'est pas géré ;
- ligne 11 : un clic sur un lien actif va provoquer l'exécution de la méthode [navigateToView] qui va afficher la vue correspondant au lien ;
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
protected
void
navigateToView
(
View v) {
String libellé =
(
(
TextView) v).getText
(
).toString
(
);
if
(
libellé.equals
(
getResources
(
).getString
(
R.string.lnk_config))) {
mainActivity.showVue
(
(
Vue) factory.getObject
(
Factory.CONFIG_VUE, (
Object[]) null
));
}
if
(
libellé.equals
(
getResources
(
).getString
(
R.string.lnk_accueil))) {
mainActivity.showVue
(
(
Vue) factory.getObject
(
Factory.ACCUEIL_VUE, (
Object[]) null
));
}
if
(
libellé.equals
(
getResources
(
).getString
(
R.string.lnk_agenda))) {
mainActivity.showVue
(
(
Vue) factory.getObject
(
Factory.AGENDA_VUE, (
Object[]) null
));
}
if
(
libellé.equals
(
getResources
(
).getString
(
R.string.lnk_ajout))) {
mainActivity.showVue
(
(
Vue) factory.getObject
(
Factory.AJOUT_RV_VUE, (
Object[]) null
));
}
}
- ligne 2 : le paramètre de la méthode est un lien de type [TextView] ;
- ligne 4 : on récupère le libellé de ce lien ;
- lignes 6-17 : on compare ce libellé à ceux des quatres liens ;
- ligne 7 : la classe [MainActivity] a une classe [showVue] permettant d'afficher un objet [Vue]. Cet objet est demandé à la fabrique (singleton).
X-I-7. La fabrique▲
La fabrique crée tous les objets du client Android :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
public
class
Factory implements
IFactory {
public
static
final
int
CONFIG =
-
1
;
public
static
final
int
SESSION =
-
2
;
public
static
final
int
METIER =
-
3
;
public
static
final
int
CONFIG_VUE =
10
;
public
static
final
int
ACCUEIL_VUE =
11
;
public
static
final
int
AGENDA_VUE =
12
;
public
static
final
int
AJOUT_RV_VUE =
13
;
public
static
final
int
GETALLMEDECINS_TASK =
30
;
public
static
final
int
GETALLCLIENTS_TASK =
31
;
public
static
final
int
GETAGENDAMEDECIN_TASK =
32
;
public
static
final
int
AJOUTER_RV_TASK =
33
;
public
static
final
int
SUPPRIMER_RV_TASK =
34
;
private
Config config;
private
boolean
verbose;
private
int
timeout;
private
IMetier metier;
private
ISession session;
private
Vue configVue;
private
Vue accueilVue;
private
Vue agendaVue;
private
Vue ajoutRvVue;
private
Activity activity;
public
Factory
(
Activity activity, Config config) {
this
.activity =
activity;
this
.config =
config;
verbose =
config.isVerbose
(
);
timeout =
config.getTimeout
(
);
}
@
Override
public
Object getObject
(
int
id, Object... params) {
switch
(
id) {
case
CONFIG:
return
config;
case
SESSION:
return
getSession
(
);
case
METIER:
return
getMetier
(
params);
case
CONFIG_VUE:
return
getConfigVue
(
params);
case
ACCUEIL_VUE:
return
getAccueilVue
(
params);
case
AGENDA_VUE:
return
getAgendaVue
(
params);
case
AJOUT_RV_VUE:
return
getAjoutRvVue
(
params);
case
GETALLMEDECINS_TASK:
return
getAllMedecinsTask
(
params);
case
GETALLCLIENTS_TASK:
return
getAllClientsTask
(
params);
case
GETAGENDAMEDECIN_TASK:
return
getAgendaMedecinTask
(
params);
case
AJOUTER_RV_TASK:
return
getAjouterRvTask
(
params);
case
SUPPRIMER_RV_TASK:
return
getSupprimerRvTask
(
params);
}
return
null
;
}
....
Nous donnons ce code seulement pour lister les objets gérés par la fabrique. Leur code de fabrication est analogue à celui des fabriques déjà présentées.
X-I-8. La tâche [GetAllClientsTask]▲
Cette tâche fournit la liste des médecins :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
package
android.rdvmedecins.ui.tasks;
...
public
class
GetAllClientsTask extends
Task {
private
Object info;
@
Override
protected
void
onPreExecute
(
) {
boss.notifyEvent
(
this
, IBoss.WORK_STARTED, null
);
}
@
Override
protected
Void doInBackground
(
Object... params) {
IMetier metier =
(
IMetier) factory.getObject
(
Factory.METIER, params[0
]);
try
{
List<
Client>
clients =
metier.getAllClients
(
);
Map<
String,Object>
result=
new
HashMap<
String,Object>
(
);
result.put
(
"
clients
"
,clients);
info=
result;
}
catch
(
Exception ex) {
info =
ex;
}
return
null
;
}
@
Override
protected
void
onPostExecute
(
Void result) {
boss.notifyEvent
(
this
, IBoss.WORK_INFO, info);
boss.notifyEvent
(
this
, IBoss.WORK_TERMINATED, null
);
}
}
Toutes les tâches du client Android sont construites sur le même modèle :
- lignes 12-15 : la méthode [onPreExecute] est utilisée pour envoyer au boss (la vue) la notification [WORK_STARTED] ;
- lignes 38-42 : la méthode [onPostExecute] est utilisée pour envoyer au boss (la vue) la notification [WORK_INFO] avec le résultat de la tâche et la notification [WORK_TERMINATED] ;
- ligne 20 : on demande à la fabrique une référence sur la couche [métier] ;
- lignes 21-31 : on exécute la méthode adéquate de la couche [métier] ;
- lignes 25-27 : l'information produite par la tâche est un dictionnaire avec une unique entrée de clé "clients" et de valeur la liste des clients de type List<Client>.
X-I-9. La tâche [GetAllMedecinsTask]▲
Cette tâche fournit la liste des médecins sous la forme d'un dictionnaire avec une unique entrée de clé "medecins" et de valeur la liste des clients de type List<Medecin>, ceci de façon similaire à [GetAllClientsTask].
X-I-10. La vue [AccueilVue]▲
C'est la vue suivante :
N°
|
Id
|
Type
|
Rôle
|
1
|
spinnerMedecins
|
Spinner
|
liste déroulante des médecins
|
2
|
edtJourRv
|
TextView
|
la date du rendez-vous au format JJ-MM-AAAA
|
3
|
btnValider
|
Button
|
affiche l'agenda du médecin choisi
|
3
|
btnAnnuler
|
Button
|
annule la demande
|
Le code de la vue est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
public
void
onResume
(
) {
super
.onResume
(
);
medecins =
(
List<
Medecin>
) session.get
(
"
medecins
"
);
String[] arrayMedecins =
new
String[medecins.size
(
)];
int
i =
0
;
for
(
Medecin medecin : medecins) {
arrayMedecins[i] =
String.format
(
"
%s
%s
%s
"
, medecin.getTitre
(
), medecin.getPrenom
(
), medecin.getNom
(
));
i+
+
;
}
ArrayAdapter<
String>
dataAdapter =
new
ArrayAdapter<
String>
(
activity, android.R.layout.simple_spinner_item,
arrayMedecins);
dataAdapter.setDropDownViewResource
(
android.R.layout.simple_spinner_dropdown_item);
spinnerMedecins.setAdapter
(
dataAdapter);
activateLinks
(
1
);
}
- ligne 1 : avant de s'afficher la vue doit mettre à jour son modèle avec des informations trouvées dans la session ;
- ligne 5 : la liste des médecins est récupérée en session. La session est injectée dans les vues et les tâches par la fabrique lors de leur création ;
- lignes 8-12 : un tableau [arrayMedecin] de chaînes de la forme [Mr Paul Marand] est construit ;
- lignes 14-17 : ce tableau sera le contenu de la liste déroulante [spinnerMedecins] ;
- ligne 19 : le 1er lien du bandeau gauche est activé. Il permet à l'utilisateur de revenir à la vue [ConfigVue] précédente.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
protected
void
doValider
(
) {
if
(
!
isPageValid
(
)) {
return
;
}
results.clear
(
);
ITask getAgendaMedecinTask =
(
ITask) factory
.getObject
(
Factory.GETAGENDAMEDECIN_TASK, this
, "
AgendaMedecinJourTask
"
);
getAgendaMedecinTask.doWork
(
idMedecin, jourRv);
beginWaiting
(
);
beginMonitoring
(
);
}
- ligne 2 : la méthode exécutée lors d'un clic sur le bouton [Valider] ;
- ligne 8 : les résultats des tâches asynchrones sont stockées dans une liste d'objets [results]. Avant chaque nouvelle exécution, on nettoie cette liste ;
- lignes 10-13 : avec les informations saisies (médecin et jour), on lance la tâche asynchrone [GetAgendaMedecinTask] qui crée l'agenda du médecin ;
- ligne 15 : début attente visuelle ;
- ligne 17 : début attente de la fin des tâches.
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@
Override
public
void
notifyEvent
(
IWorker worker, int
eventType, Object event) {
super
.notifyEvent
(
worker, eventType, event);
if
(
eventType =
=
IBoss.WORK_INFO) {
results.add
(
event);
}
}
- ligne 3 : on traite les notifications envoyées par la tâche lancée ;
- ligne 5 : la notification est d'abord passée au parent ;
- lignes 7-8 : l'information tarnsportée par la notification [WORK_INFO] est mémorisée ;
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
@
Override
public
void
notifyEndOfTasks
(
) {
cancelWaiting
(
);
boolean
erreur =
false
;
for
(
Object result : results) {
if
(
result instanceof
AgendaMedecinJour) {
session.add
(
"
agenda
"
, result);
}
else
if
(
result instanceof
Exception) {
showException
(
(
Exception) result);
erreur =
true
;
break
;
}
}
if
(
!
erreur) {
Vue agendaVue =
(
Vue) factory.getObject
(
Factory.AGENDA_VUE, null
, "
AgendaVue
"
);
mainActivity.showVue
(
agendaVue);
}
}
- ligne 2 : la méthode exécutée à la fin de la tâche ;
- ligne 4 : fin de l'attente visuelle ;
- ligne 7 : on scanne la liste des résultats. On n'en attend qu'un donc on peut se passer de la boucle ;
- lignes 9-11 : si le résultat est l'agenda attendu, il est mémorisé dans la session ;
- lignes 12-14 : si le résultat est une exception, celle-ci est affichée ;
- lignes 21-24 : s'il n'y pas eu d'erreur, on affiche la vue suivante [AgendaVue].
X-I-11. La tâche [GetAgendaMedecinTask]▲
La tâche rend l'agenda du médecin. Son code est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
@
Override
protected
Void doInBackground
(
Object... params) {
IMetier metier =
(
IMetier) factory.getObject
(
Factory.METIER, (
Object[])null
);
try
{
Long idMedecin =
(
Long) params[0
];
String jourRv =
(
String) params[1
];
AgendaMedecinJour agenda =
metier.getAgendaMedecinJour
(
idMedecin, jourRv);
info =
agenda;
}
catch
(
Exception ex) {
info =
ex;
}
return
null
;
}
- ligne 3 : la vue a passé les paramètres (N° du médecin, Jour du rendez-vous) ;
- ligne 12 : l'information rendue est de type [AgendaMedecinJour].
X-I-12. La vue [AgendaVue] - 1▲
C'est la vue suivante :
N°
|
Id
|
Type
|
Rôle
|
1
|
lstCreneaux
|
ListView
|
liste des créneaux horarires des médecins
|
2
|
txtTitre2
|
TextView
|
une ligne d'information
|
3
|
|
TextView
|
lien pour ajouter un rendez-vous
|
4
|
|
TextView
|
lien our supprimer un rendez-vous
|
Le code de la vue est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
public
class
AgendaVue extends
LocalVue {
private
TextView txtTitre2;
private
ListView lstCreneaux;
private
Button btnAnnuler;
private
AgendaMedecinJour agenda;
private
List<
Object>
results =
new
ArrayList<
Object>
(
);
private
int
firstPosition;
private
int
top;
...
@
Override
public
void
onResume
(
) {
super
.onResume
(
);
agenda =
(
AgendaMedecinJour) session.get
(
"
agenda
"
);
Medecin medecin =
agenda.getMedecin
(
);
String text =
String.format
(
"
Rendez-vous
de
%s
%s
%s
le
%s
"
, medecin.getTitre
(
), medecin.getPrenom
(
),
medecin.getNom
(
), new
SimpleDateFormat
(
"
dd-MM-yyyy
"
, Locale.FRANCE).format
(
agenda.getJour
(
)));
txtTitre2.setText
(
text);
ArrayAdapter<
CreneauMedecinJour>
adapter =
new
ListCreneauxAdapter
(
mainActivity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour
(
), this
);
lstCreneaux.setAdapter
(
adapter);
lstCreneaux.setSelectionFromTop
(
firstPosition, top);
activateLinks
(
2
);
btnAnnuler.setVisibility
(
View.INVISIBLE);
}
...
- ligne 19 : méthode exécutée juste avant l'affichage de la vue ;
- ligne 23 : on récupère l'agenda du médecin en session ;
- lignes 25-28 : on génère la ligne d'information [2] ;
- lignes 30-32 : on initialise le ListView [1] via un adaptateur propriétaire [ListCreneauxAdapter] auquel on passe les paramètres suivants :
- ligne 34 : on positionne le [ListView] au bon endroit. Lorsqu'on supprime un rendez-vous, le [ListView] est régénéré totalement à partir de la base de données afin de prendre en compte les éventuelles modifications apportées en base par d'autres utilisateurs. On veut alors réafficher le [ListView] dans la position où il était lorsque l'utilisateur a cliqué le lien [Supprimer]. Nous expliquerons comment faire ;
- ligne 36 : les deux premiers liens du bandeau gauche sont activés permettant à l'utilisateur de revenir à l'une des deux premières vues de l'assistant.
- l'activité Android en cours,
- le fichier XML définissant le contenu de chaque élément du [ListView],
- le tableau des créneaux horaires du médecin,
- la vue elle-même ;
Pour comprendre le reste de la vue, il nous faut d'abord expliquer le fonctionnement du [ListView] [1] ;
X-I-13. L'adaptateur [ListCreneauxAdapter]▲
La classe [ListCreneauxAdapter] sert à définir une ligne du [ListView] :
On voit ci-dessus, que selon le créneau a un rendez-vous ou non, l'affichage n'est pas le même. Le code de la classe [ListCreneauxAdapter] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
package
android.rdvmedecins.ui.vues;
...
public
class
ListCreneauxAdapter extends
ArrayAdapter<
CreneauMedecinJour>
{
private
CreneauMedecinJour[] creneauxMedecinJour;
private
Context context;
private
int
layoutResourceId;
private
AgendaVue vue;
public
ListCreneauxAdapter
(
Context context, int
layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaVue vue) {
super
(
context, layoutResourceId, creneauxMedecinJour);
this
.creneauxMedecinJour =
creneauxMedecinJour;
this
.context =
context;
this
.layoutResourceId =
layoutResourceId;
this
.vue =
vue;
Arrays.sort
(
creneauxMedecinJour, new
MyComparator
(
));
}
@
Override
public
View getView
(
final
int
position, View convertView, ViewGroup parent) {
...
}
class
MyComparator implements
Comparator<
CreneauMedecinJour>
{
...
}
}
- ligne 5 : la classe [ListCreneauxAdapter] doit étendre un adaptateur prédéfini pour les [ListView], ici la classe [ArrayAdapter] qui comme son nom l'indique alimente le [ListView] avec un tableau d'objets, ici de type [CreneauMedecinJour]. Rappelons le code de cette entité :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
public
class
CreneauMedecinJour implements
Serializable {
private
static
final
long
serialVersionUID =
1L
;
private
Creneau creneau;
private
Rv rv;
...
}
- la classe [CreneauMedecinJour] contient un créneau horaire (ligne 5) et un éventuel rendez-vous (ligne 6) ou null si pas de rendez-vous ;
Retour au code de la classe [ListCreneauxAdapter] :
- ligne 17 : le constructeur reçoit quatre paramètres :
- ligne 26 : le tableau des créneaux horaires est trié dans l'ordre croissant des horaires ;
- l'activité Android en cours,
- le fichier XML définissant le contenu de chaque élément du [ListView],
- le tableau des créneaux horaires du médecin,
- la vue elle-même ;
La méthode [getView] est chargée de générer la vue correspondant à une ligne du [ListView]. Celle-ci comprend trois éléments :
N°
|
Id
|
Type
|
Rôle
|
1
|
txtCreneau
|
TextView
|
créneau horaire
|
2
|
txtClient
|
TextView
|
le client
|
3
|
btnValider
|
TextView
|
lien pour ajouter / supprimer un rendez-vous
|
Le code de la méthode [getView] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
@
Override
public
View getView
(
final
int
position, View convertView, ViewGroup parent) {
CreneauMedecinJour creneauMedecin =
creneauxMedecinJour[position];
View row =
(
(
Activity) context).getLayoutInflater
(
).inflate
(
layoutResourceId, parent, false
);
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
(
)));
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);
final
TextView btnValider =
(
TextView) row.findViewById
(
R.id.btn_Valider);
if
(
creneauMedecin.getRv
(
) =
=
null
) {
btnValider.setText
(
R.string.btn_ajouter);
btnValider.setTextColor
(
context.getResources
(
).getColor
(
R.color.blue));
}
else
{
btnValider.setText
(
R.string.btn_supprimer);
btnValider.setTextColor
(
context.getResources
(
).getColor
(
R.color.red));
}
btnValider.setOnClickListener
(
new
OnClickListener
(
) {
@
Override
public
void
onClick
(
View v) {
vue.doValider
(
position, btnValider.getText
(
).toString
(
));
}
}
);
return
row;
}
- ligne 1 : position est le n° de ligne qu'on va générer dans le [ListView]. C'est également le n° du créneau dans le tableau [creneauxMedecinJour]. On ignore les deux autres paramètres ;
- ligne 4 : on récupère le créneau horaire à afficher dans la ligne du [ListView] ;
- ligne 6 : la ligne est construite à partir de sa définition XML
Le code de [creneau_medecin.xml] est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
<?
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
>
- lignes 8-10 : le créneau horaire [1] est construit ;
- lignes 12-20 : l'identité du client [2] est construite ;
- ligne 23 : si le créneau n'a pas de rendez-vous ;
- lignes 25-26 : on construit le lien [Ajouter] de couleur bleue ;
- lignes 29-30 : sinon on construit le lien [Annuler] de couleur rouge ;
- lignes 33-40 : quelque soit la nature lien [Ajouter / Supprimer] c'est la méthode [doValider] de la vue qui gèrera le clic sur le lien. La méthode recevra deux arguments :
- ligne 42 : on rend la ligne qu'on vient de construire.
- le n° du créneau qui a été cliqué,
- le libellé du lien qui a été cliqué ;
On notera que c'est la méthode [doValider] de la vue qui gère les liens. On y vient.
X-I-14. La vue [AgendaVue] - 2▲
La méthode [doValider] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
protected
void
doValider
(
int
position, String texte) {
session.add
(
"
position
"
, position);
firstPosition =
lstCreneaux.getFirstVisiblePosition
(
);
View v =
lstCreneaux.getChildAt
(
0
);
top =
(
v =
=
null
) ? 0
: v.getTop
(
);
if
(
texte.equals
(
getResources
(
).getString
(
R.string.btn_ajouter))) {
doAjouter
(
position);
}
else
{
doSupprimer
(
position);
}
}
- ligne 2 : on reçoit le n° du créneau dans lequel l'utilisateur a cliqué un lien ainsi que le libellé de ce lien ;
- ligne 4 : le n° du créneau est mis en session ;
- lignes 5-12 : on note des informations sur la position du créneau cliqué afin de pouvoir le réafficher ultérieurement à l'endroit où il était lorsqu'il a été cliqué. On note deux informations (firstPosition, top). Ces deux informations sont utilisées dans la méthode [onResume] :
Sélectionnez 1.
2.
lstCreneaux.setSelectionFromTop
(
firstPosition, top);
L'explication du mécanisme mis en œuvre est assez complexe. Il faudrait faire un dessin. En ligne 6, on trouve l'URL qui donne cette explication ;
- lignes 14-18 : selon le libellé du lien cliqué on ajoute (ligne 15) ou on supprime (ligne 17) un rendez-vous.
La méthode [doAjouter] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
private
void
doAjouter
(
int
position) {
Vue ajoutRvVue =
(
Vue) factory.getObject
(
Factory.AJOUT_RV_VUE, null
, "
AjoutRvVue
"
);
mainActivity.showVue
(
ajoutRvVue);
}
- lignes 4-5 : on fait afficher la vue suivante [AjoutRvVue].
La méthode [doSupprimer] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
private
void
doSupprimer
(
int
position) {
results.clear
(
);
ITask supprimerRvTask =
(
ITask) factory.getObject
(
Factory.SUPPRIMER_RV_TASK, this
, "
SupprimerRvTask
"
);
supprimerRvTask.doWork
(
agenda.getCreneauxMedecinJour
(
)[position].getRv
(
).getId
(
));
beginWaiting
(
);
beginMonitoring
(
);
}
- lignes 5-7 : on lance une tâche asynchrone pour supprimer le rendez-vous en base. Parce qu'on a lancé une tâche asynchrone, il faut gérer ses notifications.
La méthode [notifyEvent] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@
Override
public
void
notifyEvent
(
IWorker worker, int
eventType, Object event) {
super
.notifyEvent
(
worker, eventType, event);
if
(
eventType =
=
IBoss.WORK_INFO) {
results.add
(
event);
}
}
- lignes 7-9 : on gère la notification [WORK_INFO]. On se contente de mettre dans une liste les informations reçues. Ici, il n'y en aura qu'une, la chaîne " OK " ou une exception.
La méthode [notifyEndOfTasks] est appelée lorsque la tâche est terminée ;
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
@
Override
public
void
notifyEndOfTasks
(
) {
cancelWaiting
(
);
Object result =
results.get
(
0
);
if
(
result instanceof
String) {
results.clear
(
);
ITask getAgendaMedecinTask =
(
ITask) factory.getObject
(
Factory.GETAGENDAMEDECIN_TASK, this
,
"
AgendaMedecinJourTask
"
);
getAgendaMedecinTask.doWork
(
agenda.getMedecin
(
).getId
(
),
new
SimpleDateFormat
(
"
dd-MM-yyyy
"
, Locale.FRANCE).format
(
agenda.getJour
(
)));
beginWaiting
(
);
beginMonitoring
(
);
}
else
{
if
(
result instanceof
AgendaMedecinJour) {
session.add
(
"
agenda
"
, result);
AgendaMedecinJour agenda =
(
AgendaMedecinJour) result;
ArrayAdapter<
CreneauMedecinJour>
adapter =
new
ListCreneauxAdapter
(
mainActivity, R.layout.creneau_medecin,
agenda.getCreneauxMedecinJour
(
), this
);
lstCreneaux.setAdapter
(
adapter);
lstCreneaux.setSelectionFromTop
(
firstPosition, top);
}
else
if
(
result instanceof
Exception) {
showException
(
(
Exception) result);
}
}
}
- ligne 6 : on récupère le résultat de la suppression, la chaîne " OK " si tout s'est bien passé, une exception sinon ;
- ligne 8 : on teste le 1er cas ;
- lignes 11-16 : si la suppression s'est bien passée, on lance une nouvelle tâche asynchrone pour régénérer l'agenda du médecin à partir de la base. On a pris soin auparavant de nettoyer la liste des résultats. La nouvelle tâche va s'exécuter et produire une information de type [AgendaMedecinJour]. Puis la méthode [notifyEndOfTasks] va être de nouveau appelée avec cette nouvelle information ;
- ligne 22 : traite cette information ;
- ligne 24 : elle est mise en session ;
- lignes 26-31 : le [ListView] est régénéré à partir de ce nouvel agenda ;
- lignes 32-34 : affichent une éventuelle exception issue de ces deux tâches.
X-I-15. La tâche [SupprimerRvTask]▲
La tâche [SupprimerRvTask] supprime un rendez-vous de la façon suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
@
Override
protected
Void doInBackground
(
Object... params) {
Long idRv =
(
Long) params[0
];
IMetier metier =
(
IMetier) factory.getObject
(
Factory.METIER, (
Object[]) null
);
try
{
metier.supprimerRv
(
idRv);
info =
"
OK
"
;
}
catch
(
Exception ex) {
info =
ex;
}
return
null
;
}
X-I-16. La vue [AjoutRvVue]▲
C'est la vue suivante :
N°
|
Id
|
Type
|
Rôle
|
1
|
spinnerClients
|
Spinner
|
liste déroulante des clients
|
2
|
txtTitre2
|
TextView
|
une ligne d'information
|
3
|
btnValider
|
Button
|
bouton pour valider le rendez-vous
|
3
|
btnAnnuler
|
Button
|
bouton pour annuler la demande de validation
|
Le code de la vue est le suivant :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
@
SuppressWarnings
(
"
unchecked
"
)
@
Override
public
void
onResume
(
) {
super
.onResume
(
);
clients =
(
List<
Client>
) session.get
(
"
clients
"
);
String[] arrayClients =
new
String[clients.size
(
)];
int
i =
0
;
for
(
Client client : clients) {
arrayClients[i] =
String.format
(
"
%s
%s
%s
"
, client.getTitre
(
), client.getPrenom
(
), client.getNom
(
));
i+
+
;
}
ArrayAdapter<
String>
dataAdapter =
new
ArrayAdapter<
String>
(
activity, android.R.layout.simple_spinner_item,
arrayClients);
dataAdapter.setDropDownViewResource
(
android.R.layout.simple_spinner_dropdown_item);
spinnerClients.setAdapter
(
dataAdapter);
int
position =
(
Integer) session.get
(
"
position
"
);
AgendaMedecinJour agenda =
(
AgendaMedecinJour) session.get
(
"
agenda
"
);
medecin =
agenda.getMedecin
(
);
Creneau creneau =
agenda.getCreneauxMedecinJour
(
)[position].getCreneau
(
);
idCreneau =
creneau.getId
(
);
jour =
new
SimpleDateFormat
(
"
dd-MM-yyyy
"
, Locale.FRANCE).format
(
agenda.getJour
(
));
String titre2 =
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
(
));
txtTitre2.setText
(
titre2);
activateLinks
(
3
);
}
- ligne 1 : la méthode [onResume] est appelée juste avant l'affichage de la vue. Elle va aller chercher en session le modèle qu'elle doit afficher ;
- ligne 7 : la liste des clients est cherchée en session ;
- lignes 8-19 : le contenu de la liste déroulante des clients est construit avec des lignes de la forme [Melle Brigitte Bistrou] ;
- lignes 20-33 : la ligne d'information [2] est construite à partir d'informations trouvées également en session ;
- ligne 35 : active les trois premiers liens du bandeau gauche. Permet à l'utilisateur de revenir sur les trois précédentes vues.
La méthode exécutée lors du clic sur le bouton [Valider] est la suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
protected
void
doValider
(
) {
results.clear
(
);
Long idClient =
clients.get
(
spinnerClients.getSelectedItemPosition
(
)).getId
(
);
ITask ajouterRvTask =
(
ITask) factory.getObject
(
Factory.AJOUTER_RV_TASK, this
, "
AjouterRvTask
"
);
ajouterRvTask.doWork
(
jour, idCreneau, idClient);
beginWaiting
(
);
beginMonitoring
(
);
}
- lignes 5-10 : exécutent la tâche asynchrone [AjouterRvTask] qui va ajouter le rendez-vous en base.
Parce qu'elle a lancé une tâche asynchrone, la vue voit passer des notifications :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@
Override
public
void
notifyEvent
(
IWorker worker, int
eventType, Object event) {
super
.notifyEvent
(
worker, eventType, event);
if
(
eventType =
=
IBoss.WORK_INFO) {
results.add
(
event);
}
}
- lignes 7-9 : on se contente d'emmagasiner les résultats (ici un seul) dans une liste.
La vue va être prévenue de la fin des tâches (une seule ici) :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
@
Override
public
void
notifyEndOfTasks
(
) {
cancelWaiting
(
);
Object result =
results.get
(
0
);
if
(
result instanceof
AgendaMedecinJour) {
session.add
(
"
agenda
"
, result);
Vue agendaVue =
(
Vue) factory.getObject
(
Factory.AGENDA_VUE, null
, "
AgendaVue
"
);
mainActivity.showVue
(
agendaVue);
}
else
if
(
result instanceof
Rv) {
results.clear
(
);
ITask getAgendaMedecinTask =
(
ITask) factory.getObject
(
Factory.GETAGENDAMEDECIN_TASK, this
,
"
AgendaMedecinJourTask
"
);
getAgendaMedecinTask.doWork
(
medecin.getId
(
), jour);
beginWaiting
(
);
beginMonitoring
(
);
}
else
if
(
result instanceof
Exception) {
showException
(
(
Exception) result);
}
}
La tâche [AjouterRvTask] rend une information de type [Rv], le rendez-vous ajouté. Ce cas est traité ligne 13 :
- ligne 13 : résultat de la tâche [AjouterRvTask] ;
- lignes 16-20 : on lance la tâche qui régénère l'agenda du médecin à partir des informations en base. Cette tâche va rendre une information de type [AgendaMedecinJour]. Ce cas est traité ligne 7 ;
- ligne 7 : on a reçu l'agenda du médecin ;
- ligne 9 : on le met dans la session pour la vue suivante ;
- lignes 11-12 : on affiche la vue [AgendaVue] ;
- lignes 25-27 : pour le cas où l'une des deux tâches précédentes renvoie une exception.
X-I-17. La tâche [AjouterRvTask]▲
La tâche [AjouterRvTask] ajoute un rendez-vous de la façon suivante :
Sélectionnez 1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
@
Override
protected
Void doInBackground
(
Object... params) {
String jour =
(
String) params[0
];
Long idCreneau =
(
Long) params[1
];
Long idClient =
(
Long) params[2
];
IMetier metier =
(
IMetier) factory.getObject
(
Factory.METIER, (
Object[]) null
);
try
{
Rv rv =
metier.ajouterRv
(
jour, idCreneau, idClient);
info =
rv;
}
catch
(
Exception ex) {
info =
ex;
}
return
null
;
}
Ceci termine notre revue de l'exemple 7.
X-J. Conclusion de l'exemple 7▲
Rappelons l'architecture client / serveur de cet exemple :
Le serveur J2EE
Le client Android
Dans le document [pfm], une application web mobile avait été construite avec les mêmes fonctionnalités que celle qui vient d'être construite. Pour l'instant l'application web mobile l'emporte sur l'application native Android. En effet, elle fonctionne sur n'importe quel smartphone ce qui n'est pas le cas de notre application Android. Pour rendre celle-ci plus attractive, on pourrait utiliser des éléments du smartphone / tablette (caméra, sonnerie, micro…) que l'application web mobile ne pourrait utiliser. Mais ça peut relever du gadget pour un client de gestion par exemple. Plus intéressant, on peut déporter la couche [métier] du serveur sur le client Android. L'architecture devient alors la suivante :
Le serveur J2EE
Le service REST expose désormais l'interface de la couche [DAO].
Le client Android
La nouvelle couche [métier] du client Android va désormais avoir deux fonctionnalités :
- s'interfacer avec le service REST de la couche [DAO] du serveur ;
- embarquer la couche [métier] du serveur.
Le métier est désormais déporté sur les clients. On allège ainsi le serveur et on gagne probablement en performances. C'est plus difficile à atteindre avec une application web mobile.