Cours android

Introduction à la programmation d’application Android tablette


Télécharger Introduction à la programmation d’application Android tablette

★★★★★★★★★★3.5 étoiles sur 5 basé sur 1 votes.
Votez ce document:

Télécharger aussi :


Introduction à la programmation

de tablettes Android

par l'exemple

Serge Tahé, IstiA - université d'Angers janvier 2014

Table des matières

APPRENTISSAGE DE LA PROGRAMMATION ANDROID  4  1.1 INTRODUCTION ..4

1.2  LATABLETTE ANDROID .4

1.3  TESTSDESPROJETS ANDROID .6

EXEMPLE-01   : VUE ET ÉVÈNEMENTS 8

2.1  CRÉATIONDUPROJET .8  2.2 L'ENCODAGEDESFICHIERSDUPROJET .9

2.3  LEMANIFESTEDEL'APPLICATION 9

2.4  L'ACTIVITÉPRINCIPALE ..11

2.5  EXÉCUTIONDEL'APPLICATION 12

2.6  CONSTRUIREUNEVUE .13

2.7  GESTIONDESÉVÉNEMENTS 18  2.8 MODIFICATIONDUMANIFESTE 20

EXEMPLE-02   : NAVIGATION ENTRE VUES .                                                                                                         22

3.1  CRÉATIONDUPROJET ..22  3.2 AJOUTD'UNESECONDEACTIVITÉ 22

3.3  NAVIGATIONDELAVUEN° 1 ÀLAVUEN° 2 .25

3.4  CONFIGURATIONDEL'ENVIRONNEMENTD'EXÉCUTION ..26

3.5  CONSTRUCTIONDELAVUEN° 2 .26

3.6  EXPLOITATIONDESINFORMATIONSDEL'INTENTDEL'ACTIVITÉ 28  3.7 NAVIGATIONDELAVUEN° 2 VERSLAVUEN° 1 ..29

EXEMPLE-03   : CONSTRUIRE UN PROJET MAVEN / ANDROID .. 31

EXEMPLE-04   : MAVENISER UN PROJET ANDROID EXISTANT . 34

EXEMPLE-05   : NAVIGATION PAR ONGLETS  . 35

6.1  LEPROJET ANDROID 35

6.2  LESVUES 35

6.3  L'ACTIVITÉ 36  6.4 UNNOUVEAUFRAGMENT 40

 6.5 DÉSACTIVERLE SWIPEOU BALAYAGE .43

EXEMPLE-06   : LA NAVIGATION ENTRE VUES REVISITÉE  .. 47  7.1 LEPROJET .47

7.2  MISEENPLACEDELANAVIGATION ..50

7.3  CONCLUSION .51

  8 EXEMPLE-07   : UNE ARCHITECTURE À DEUX COUCHES . 52  8.1.1 LE PROJET ANDROID 52

 8.1.2 LA VUE [VUE_01] 52  8.1.3 LA VUE [ACTIVITY_MAIN] .55  8.1.4 LA COUCHE [MÉTIER] .56  8.1.5 L'ACTIVITÉ [MAINACTIVITY] ..57  8.1.6 LE FRAGMENT DE LA VUE [VUE_01] 59  8.1.7 EXÉCUTION .62  8.1.8 MAVENISATION DU PROJET 62

9      EXEMPLE-08 : ARCHITECTURE CLIENT / SERVEUR  . 65  9.1 SPRING MVC ..65

 9.1.1 LE PROJET ECLIPSE .65  9.1.2 ANATOMIE D'UN PROJET SPRING MVC 67

9.2  LESERVEUR REST ..73

 9.2.1 LA COUCHE [MÉTIER] .74  9.2.2 LE SERVICE REST ..76  9.2.3 EXÉCUTION DU SERVEUR REST 80

9.3  LECLIENT ANDROIDDUSERVEUR REST ..81

 9.3.1 LE PROJET ANDROID 81  9.3.2 LE MANIFESTE DE L'APPLICATION ANDROID ..82  9.3.3 LA COUCHE [METIER] .83  9.3.4 LA COUCHE [DAO] .85  9.3.5 LA COUCHE [ANDROID] .88  9.3.6 EXÉCUTION DU CLIENT REST 90

10  EXEMPLE-09   : UN CLIENT REST ASYNCHRONE  . 93

10.1  LACLASSE [ASYNCTASK] 93

10.2  LEPROJET ANDROID .94

10.3  LESÉLÉMENTSDEL'INTERFACEASYNCHRONE .94

10.4  L'APPELDELATÂCHEDANSLAVUE ..96

10.5  MODIFICATIONDEL'ACTIVITÉ .99

10.6  EXÉCUTIONDUCLIENT REST ASYNCHRONE .99

11  EXEMPLE-10   : ANNULATION D'UNE TÂCHE ASYNCHRONE . 100

11.1  L'ACTIVITÉ [MAINACTIVITY] .100

11.2  LAVUE XML [VUE_01] .101

11.3  LEFRAGMENT [VUE_01] ..102  12  EXEMPLE 11   : GESTION DE PLUSIEURS TÂCHES ASYNCHRONES . 105  12.1 LESERVEUR [REST] ..105

 12.1.1 LA COUCHE [MÉTIER] 105  12.1.2 LE CONTRÔLEUR SPRING MVC 107

 12.2 LECLIENT ANDROIDDUSERVEUR REST .108

 12.2.1 LA COUCHE [DAO] 109  12.2.2 LA COUCHE [MÉTIER] 109  12.2.3 LA TÂCHE ASYNCHRONE ..111  12.2.4 LE FRAGMENT [VUE_01] .112  12.2.5 L'ACTIVITÉ [MAINACTIVITY] .114  12.2.6 EXÉCUTION 114

13  EXEMPLE-12   : COMPOSANTS DE SAISIE DE DONNÉES .               115  13.1 LEPROJET ANDROID 115

13.2  LAVUE XML DUFORMULAIRE ..115

13.3  LESCHAÎNESDECARACTÈRESDUFORMULAIRE 121

13.4  LEFRAGMENTDUFORMULAIRE 121  13.5 L'ACTIVITÉ [MAINACTIVITY] 125

14  EXEMPLE-13   : UTILISATION D'UN PATRON DE VUES  . 127

15  EXEMPLE-14   : LE COMPOSANT [LISTVIEW]  .. 131

15.1  LEPROJET ECLIPSE .131

15.2  LAVUE [VUE1] INITIALE ..132

15.3  LAVUERÉPÉTÉEPARLE [LISTVIEW] .133

15.4  LEFRAGMENT [VUE1FRAGMENT] 134

15.5  L'ADAPTATEUR [LISTADAPTER] DU [LISTVIEW] ..136

15.6  RETIRERUNÉLÉMENTDELALISTE .138

15.7  LAVUE XML [VUE2] .138

15.8  LEFRAGMENT [VUE2FRAGMENT] 139

15.9  EXÉCUTION ..141

15.10  AMÉLIORATION 141

16  EXEMPLE-16   : UTILISER UN MENU . 143

16.1  LADÉFINITION XML DUMENU .143

16.2  LAGESTIONDUMENUDANSL'ACTIVITÉ [MAINACTIVITY] .144

16.3  LAGESTIONDUMENUDANSLEFRAGMENT [VUE1FRAGMENT] .144

16.4  LAGESTIONDUMENUDANSLEFRAGMENT [VUE2FRAGMENT] .145

16.5  EXÉCUTION ..146

17  EXERCICE D'APPLICATION                                                                                                          147

17.1  INTRODUCTION ..147

17.2  INSTALLATIONETTESTDUSERVEUR REST .147

17.3  LESVUESDUCLIENT ANDROID ..151

17.4  TRAVAILÀFAIRE 153

18  CONCLUSION .. 157

 1 Apprentissage de la programmation Android

 1.1

Introduction

Ce document présente certains concepts d'Android au travers de 16 exemples :

Nature

Vues et événements

Navigation entre vues

Construire un projet Maven / Android

Maveniser un projet Android existant

Navigation par onglets

Navigation entre vues revisitée

Architecture à deux couches

Architecture client / serveur

Un client REST asynchrone

Annulation d'une tâche asynchrone

Gestion de plusieurs tâches asynchrones

Composants de saisie de données

Utilisation d'un patron de vues

Le composant ListView

Utiliser un menu

Exercice d'application

Ce document est utilisé en dernière année de l'école d'ingénieurs IstiA de l'université d'Angers [] comme document préparatoire à un TP présenté dans []. Cela explique le ton parfois un peu particulier du texte. Ce document ne présente que les concepts nécessaires à ce TP. Aussi n'est-il qu'un document de formation partielle à la programmation Android. Il cible essentiellement les débutants.

Les outils nécessaires pour développer une application Android sont décrits dans les annexes du document ], paragraphe 11. L'outil de développement utilisé est STS (SpringToolSuite), un IDE basé sur Eclipse.

Le site de référence pour la programmation Android est à l'URL []. C'est là qu'il faut aller, pour avoir une vue d'ensemble de la programmation Android.

Les projets Eclipse des 16 exemples sont disponibles à l'URL [].

 1.2

La tablette Android

Vous aurez besoin par la suite de connecter votre tablette à un réseau wifi et de connaître son adresse IP sur ce réseau. Voici comment procéder :

•   allumez votre tablette ;

•   cherchez dans les applications disponibles sur la tablette (en haut à droite) celle qui s'appelle [paramètres] avec une icône de roue dentée ;

•   dans la section à gauche, activez le wifi ;

•   dans la section à droite, sélectionnez un réseau wifi ;

•   une fois connecté au réseau, faites une frappe courte sur le réseau sélectionné. L'adresse IP de la tablette sera affichée. Notez-la. Vous allez en avoir besoin ;

Toujours dans l'application [Paramètres],

•   sélectionnez à gauche l'option [Options de développement] (tout en bas des options) ;

•   vérifiez qu'à droite l'option [Débogage USB] est cochée.

Pour revenir au menu, tapez dans la barre d'état en bas, l'icône du milieu, celle d'une maison. Toujours dans la barre d'état en bas,

•   l'icône la plus à gauche est celle du retour en arrière : vous revenez à la vue précédente ;

•   l'icône la plus à droite est celle de la gestion des tâches. Vous pouvez voir et gérer toutes les tâches exécutées à un moment donné par votre tablette ;

Reliez votre tablette à votre PC avec le câble USB qui l'accompagne.

Installez la clé wifi sur l'un des ports USB du PC puis connectez-vous sur le même réseau wifi que la tablette. Ceci fait, dans une fenêtre DOS, tapez la commande [ipconfig] :

1.

dos>ipconfig

2. 3.

Configuration IP de Windows

4. 5.

Carte Ethernet Connexion au réseau local :

6. 7.

   Suffixe DNS propre à la connexion. . . :

8.

   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4

9.

   Adresse IPv4. . . . . . . . . . . . . .: 192.168.2.1

10.

   Masque de sous-réseau. . . . . . . . . : 255.255.255.0

11.

   Passerelle par défaut. . . . . . . . . :

12. 13.

Carte réseau sans fil Wi-Fi :

14. 15.

   Suffixe DNS propre à la connexion. . . :

16.

   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2

17.

   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25

18.

   Masque de sous-réseau. . . . . . . . . : 255.255.255.0

19.

Passerelle par défaut. . . . . . . . . : 192.168.1.1

Votre PC a deux cartes réseau et donc deux adresses IP :

•   celle de la ligne 9 qui est celle du PC sur le réseau filaire ;

•   celle de la ligne 17 qui est celle du PC sur le réseau wifi ;

Notez ces deux informations. Vous en aurez besoin. Vous êtes désormais dans la configuration suivante :

La tablette aura à se connecter à votre PC. Celui est normalement protégé par un pare-feu qui empêche tout élément extérieur d'ouvrir une connexion avec le PC. Il vous faut donc inhiber le pare-feu. Faites-le avec l'option [Panneau de configuration\Système et sécurité\Pare-feu Windows]. Parfois il faut de plus inhiber le pare-feu mis en place par l'antivirus. Cela dépend de votre antivirus.

Maintenant, sur votre PC, vérifiez la connexion réseau avec la tablette avec une commande [ping 192.168.1.y] où [192.168.1.y] est l'adresse IP de la tablette. Vous devez obtenir quelque chose qui ressemble à ceci :

1.

dos>ping 192.168.1.26

2. 3.

Envoi d'une requête 'Ping' 192.168.1.26 avec 32 octets de données :

4.

Réponse de 192.168.1.26 : octets=32 temps=244 ms TTL=64

5.

Réponse de 192.168.1.26 : octets=32 temps=199 ms TTL=64

6.

Réponse de 192.168.1.26 : octets=32 temps=28 ms TTL=64

7.

Réponse de 192.168.1.26 : octets=32 temps=88 ms TTL=64

8. 9.

Statistiques Ping pour 192.168.1.26:

10.

    Paquets : envoyés = 4, reçus = 4, perdus = 0 (perte 0%),

11.

Durée approximative des boucles en millisecondes :

12.

Minimum = 28ms, Maximum = 244ms, Moyenne = 139ms

Les lignes 4-7 indiquent que la tablette d'adresse IP [192.168.1.y] a répondu à la commande [ping].

 1.3

Tests des projets Android

Pour tester vos projets Android utilisez prioritairement la tablette. Elle est nettement plus rapide que l'émulateur. Celui-ci peut être utile lorsque le projet nécessite une connexion réseau entre la tablette et le PC et que cette connexion n'existe pas (absence de réseau wifi). Dans ce cas, vous êtes obligés d'utiliser l'émulateur.

Lorsque votre projet Android s'exécute, un certain nombre de logs sont émis sur la fenêtre [LogCat] [Window / ShowView / Other / Android / Logcat]. Ces logs sont très nombreux. Positionnez le filtre des logs à [Error] pour diminuer leur nombre.

Si le projet plante, l'exception qui s'est produite sera affichée dans cette fenêtre. Dans la recherche d'une erreur, vous pouvez faire des impressions sans votre code [Android] :

.println( ) ;

Ces affichages se retrouveront en vert dans la fenêtre [LogCat].

Vous pouvez également exécuter votre projet en mode débogage :

•      en [1], exécutez votre projet en mode débogage ;

•      en [2], on met un point d'arrêt sur une ligne de code en double-cliquant à gauche de son n° ; •       en [3], au point d'arrêt faire :

•      [F6], pour exécuter la ligne sans entrer dans les méthodes si la ligne contient des appels de méthodes,

•      [F5], pour exécuter la ligne en entrant dans les méthodes si la ligne contient des appels de méthodes,

•      [F8], pour continuer jusqu'au prochain point d'arrêt ;

Une fois le débogage terminé, quittez la perspective [Debug] pour revenir à une perspective [Java] [1] :

Parfois, votre projet sera erroné à cause de problèmes de configuration notamment Maven. Dans ce cas, consultez la fenêtre [Problems] [Window / Show View / Other / General / Problems] pour connaître la nature exacte du ou des problèmes rencontrés [2]. Parfois des solutions sont proposées pour résoudre le problème.

 2 Exemple-01 : vue et évènements

Créons avec STS un premier projet Android :

 2.1

Création du projet

•   en [1-2], on crée un projet de type [Android Application Project] ;

•   en [3], on remplit les champs d'identité de l'application ;

•   en [4], on garde les valeurs proposées par défaut sauf pour le champ [Minimum Required SDK] qui fixe la version la plus ancienne d'Android sur laquelle l'application peut être exécutée. On utilisera dans ce document la version minimale 11. Ce n'est qu'à partir de cette version que certaines des classes que nous allons utiliser ont été disponibles ;

•   on valide les valeurs par défaut de l'assistant jusqu'à la dernière page [5] ;

Le projet créé est le suivant :

 2.2

L'encodage des fichiers du projet

Dans les applications client / serveur que nous allons créer, le serveur enverra des chaînes de caractères pouvant contenir des caractères accentués. Il faut que le client et le serveur soient d'accord sur le type d'encodage utilisé pour les caractères dans les chaînes de caractères échangées. Nous utiliserons l'encodage UTF-8. Pour cela, vos projets doivent être encodés en UTF-8. Pour vous en assurer, procédez de la façon suivante : [clic droit sur le projet / Properties / Resource] :

•   en [1], choisir [UTF-8] ;

 2.3

Le manifeste de l'application

Le fichier [] [1] fixe les caractéristiques de l'application Android. Son contenu est ici le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <manifest xmlns:android=";

3.    package=".android"

4.    android:versionCode="1" 5.    android:versionName="1.0" > 6.

7.    <uses-sdk

8.    android:minSdkVersion="11" 9.        android:targetSdkVersion="16" /> 10.

11.   <application

12.   android:allowBackup="true"

13.   android:icon="@drawable/ic_launcher"

14.   android:label="@string/app_name"

15.   android:theme="@style/AppTheme" >

16.   <activity

17.   android:name=".android.MainActivity"

18.   android:label="@string/app_name" >

19.   <intent-filter>

20.   <action android:name="" /> 21.

22.   <category android:name="android.intent.category.LAUNCHER" />

23.   </intent-filter>

24.   </activity> 25.    </application> 26.

27.</manifest>

•   ligne 3 : le paquetage du projet Android. Un certain nombre de classes seront automatiquement générées dans ce paquetage [2] ;

•   ligne 8 : la version minimale d'Android pouvant exécuter l'application. Ici la version 11, une version récente est nécessaire pour disposer des onglets, des fragments [Fragment] et d'une activité de type [FragmentActivity] ;

•   ligne 9 : la version maximale d'Android. Mettre la dernière version de cet OS ;

•   ligne 13 : l'icône [3] de l'application. Elle peut être changée ;

•   ligne 14 : le libellé de l'aplication. Il se trouve dans le fichier [] [4] :

1.

<?xml version="1.0" encoding="utf-8"?>

2.

<resources>

3.

    <string name="app_name">exemple-01</string>

4.

    <string name="action_settings">Settings</string>

5.

    <string name="hello_world">Hello world!</string>

6.

</resources>

Le fichier [] contient les chaînes de caractères utilisées par l'application.

•   ligne 15 : le style de l'interface visuelle. Elle est définie dans le fichier [] [4] :

1.

<resources>

2.

    <style name="AppBaseTheme" parent="android:Theme.Light">

3.

    </style>

4. 5.

    <!-- Application theme. -->

6.

    <style name="AppTheme" parent="AppBaseTheme">

7.

    </style>

8. 9.

</resources>

•   ligne 16 : une balise d'activité. Une application Android peut avoir plusieurs activités ;

•   ligne 17 : le nom complet de la classe de l'activité ;

•   ligne 18 : son libellé ;

•   ligne 20 : l'activité est désignée comme étant l'activité principale ;

•   ligne 22 : et elle doit apparaître dans la liste des applications qu'il est possible de lancer sur l'appareil Android.

 2.4

L'activité principale

Une application Android repose sur une ou plusieurs activités. Ici une activité [1] a été générée : [MainActivity]. Une activité peut afficher une ou plusieurs vues selon son type exact. La classe [MainActivity] générée est la suivante :

1. package .android; 2.

3.    import .Bundle;

4.    import .Activity; 5. import ; 6.

7. public class MainActivity extends Activity { 8.

9.    @Override

10.   protected void onCreate(Bundle savedInstanceState) {

11.   super.onCreate(savedInstanceState);

12.   setContentView(R.layout.activity_main);

13.   }

14.

15.   @Override

16.   public boolean onCreateOptionsMenu(Menu menu) {

17.   // Inflate the menu; this adds items to the action bar if it is present.

18.   getMenuInflater().inflate(R.menu.main, menu);

19.   return true;

20.   }

21.

22.}

•   ligne 7 : la classe [MainActivity] étend la classe [Activity]. C'et toujours le cas.

•   ligne 10 : la méthode [onCreate] est exécutée lorsque l'activité est créée. C'est avant l'affichage de la vue associée à

l'activité ;

•   ligne 11 : la méthode [onCreate] de la classe parente est appelée. il faut toujours le faire ;

•   ligne 12 : le fichier [] [2] est la vue associée à l'activité. La définition XML de cette vue est la suivante :

a)    <RelativeLayout xmlns:android=";

b)    xmlns:tools=";

c)    android:layout_width="match_parent"

d)    android:layout_height="match_parent"

e)    android:paddingBottom="@dimen/activity_vertical_margin"

f)    android:paddingLeft="@dimen/activity_horizontal_margin"

g)    android:paddingRight="@dimen/activity_horizontal_margin"

h)    android:paddingTop="@dimen/activity_vertical_margin"

i)    tools:context=".MainActivity" >

j)

k)    <TextView

l)    android:layout_width="wrap_content"

m)    android:layout_height="wrap_content"

n)    android:text="@string/hello_world" /> o)

p) </RelativeLayout>

•   lignes a-p : le gestionnaire de mise en forme. Celui qui a été choisi par défaut est le type [RelativeLayout]. Dans ce type de conteneur, les composants sont placés les uns par rapport aux autres. C'est le conteneur conseillé ; • lignes k-n : un composant de type [TextView] qui sert à afficher du texte ; • ligne n : le texte affiché. Il est tiré du fichier [] [3] :

a) <?xml version="1.0" encoding="utf-8"?>

b) <resources>

c)

d)    <string name="app_name">exemple-01</string>

e)    <string name="action_settings">Settings</string>

f)    <string name="hello_world">Hello world!</string> g)

h) </resources>

Le texte affiché sera donc [Hello world!]. Où sera-t-il affiché ? Comme rien n'est indiqué, il va être affiché en haut et à gauche du conteneur.

 2.5

Exécution de l'application

Pour exécuter une application Android, il nous faut créer une configuration d'exécution :

•   en [1], sélectionnez l'icône [Run as ] ;

•   en [2], sélectionnez l'option [Run Configurations ] ;

•   en [3], sélectionnez le type [Android Application] puis l'icône [New launch configuration] ;

•   en [4], indiquez le projet qui sera exécuté par cette configuration ;

•   en [5], donnez un nom à cette configuration. Peut être quelconque ;

•   dans l'onglet [Target] [6], sélectionnez l'option [7]. Elle permet de choisir le mode d'exécution : en mode émulation avec une tablette logicielle ou en mode réel avec une tablette Android ;

•   en [8], validez cette configuration ; •  en [9], exécutez-la ;

•   en [8], un émulateur de tablette. Si aucun émulateur n'est lancé, lancez l'émulateur appelé [Tablet] ; •     en [9], une tablette Android ;

•   sélectionnez l'émulateur de tablette et exécutez l'application ;

L'émulateur logiciel affiche au bout d'un moment la vue suivante :

Branchez maintenant une tablette Android sur un port USB du PC et exécutez l'application sur celle-ci :

•   en [1], sélectionnez la tablette Android et testez l'application.

 2.6

Construire une vue

Nous allons maintenant modifier la vue affichée avec l'éditeur graphique d'Eclipse ADT (Andoid Developer Tools).

•   en [1], créez une nouvelle vue XML [clic droit sur layout / New / Other / Android/ Android Layout XML File] ; •          en [2], nommez la vue ;

•   en [3], indiquez la balise racine de la vue. Ici, nous choisissons un conteneur [RelativeLayout]. Dans ce conteneur de composants, ceux-ci sont placés les uns par rapport aux autres : " à droite de ", " à gauche de ", " au-dessous de ", " audessus de " ;

Le fichier [] généré [4] est le suivant :

1.

<?xml version="1.0" encoding="utf-8"?>

2.

<RelativeLayout xmlns:android=";

3.

    android:layout_width="match_parent"

4.

    android:layout_height="match_parent" >

5.

</RelativeLayout>

•   ligne 2 : un conteneur [RelativeLayout] vide qui occupera toute la largeur de la tablette (ligne 3) et toute sa hauteur (ligne

4) ;

•   en [1], sélectionnez l'onglet [Graphical Layout] ;

•   en [2], mettez-vous en mode tablette ;

•   en [3], mettez-vous à l'échelle 1 de la tablette ;

•   en [1], prendre un [TextView] et le tirer sur la vue [2] ;

•   en [3], fixer le texte du composant ;

•   en [4], on veut créer une nouvelle chaîne de caractères dans le fichier [] ;

•   en [5], le texte de la chaîne de caractères créée ;

•   en [6], l'identifiant de la chaîne de caractères créée ; •      en [7], l'interface visuelle se met à jour ;

•   en [8], modifier la taille du texte ;

•   en [9], mettre une taille, ici 50 pixels ; •             en [10], la nouvelle vue ;

•   en [11] et [12], modifier l'identifiant du composant ;

Le fichier [] a évolué comme suit :

1.

<?xml version="1.0" encoding="utf-8"?>

2.

<RelativeLayout xmlns:android=";

3.

    android:layout_width="match_parent"

4.

    android:layout_height="match_parent" >

5. 6.

    <TextView

7.

        android:id="@+id/textView_titre"

8.

        android:layout_width="wrap_content"

9.

        android:layout_height="wrap_content"

10.   android:layout_alignParentLeft="true"

11.   android:layout_alignParentTop="true"

12.   android:layout_marginLeft="278dp"

13.   android:layout_marginTop="77dp"

14.   android:text="@string/vue1_titre" 15.        android:textSize="50sp" /> 16.

17.</RelativeLayout>

•   les modifications faites dans l'interface graphique sont aux lignes 7, 14 et 15. Les autres attributs du [TextView] sont des valeurs par défaut ou bien découlent du positionnement du composant dans la vue ;

•   lignes 8-9 : la taille du composant est celle du texte qu'elle contient (wrap_content) en hauteur et largeur ;

•   lignes 11, 13 : le haut du composant est aligné avec le haut de la vue (ligne 11), 77 pixels dessous (ligne 13) ;

•   lignes 10, 12 : le côté gauche du composant est aligné avec la gauche de la vue (ligne 10), 278 pixels à droite (ligne 12) ;

En général, les tailles exactes des marges gauche, droite, haute et basse seront fixées directement dans le XML.

En procédant de la même façon, créez la vue suivante [1] :

Les composants sont les suivants :

Id

Type

Rôle

1

textView_titre

TextView

Titre de la vue

2

textView_nom

TextView

un texte

3

editText_Nom

EditText

saisie d'un nom

4

button_valider

Button

pour valider la saisie

5

button_vue2

Button

pour passer à la vue n° 2

Le fichier XML est le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    android:layout_width="match_parent" 4.    android:layout_height="match_parent" > 5.

6.    <TextView

7.    android:id="@+id/textView_titre"

8.    android:layout_width="wrap_content"

9.    android:layout_height="wrap_content"

10.   android:layout_alignParentLeft="true"

11.   android:layout_alignParentTop="true"

12.   android:layout_marginLeft="88dp"

13.   android:layout_marginTop="26dp"

14.   android:text="@string/vue1_titre" 15.        android:textSize="50sp" /> 16.

17.    <TextView

18.   android:id="@+id/textView_nom"

19.   android:layout_width="wrap_content"

20.   android:layout_height="wrap_content"

21.   android:layout_alignParentLeft="true"

22.   android:layout_below="@+id/textView_titre"

23.   android:layout_marginLeft="45dp"

24.   android:layout_marginTop="35dp" 25.        android:text="@string/textView_nom" /> 26.

27.   <EditText

28.   android:id="@+id/editText_nom"

29.   android:layout_width="wrap_content"

30.   android:layout_height="wrap_content"

31.   android:layout_alignBottom="@+id/textView_nom"

32.   android:layout_marginLeft="49dp"

33.   android:layout_toRightOf="@+id/textView_nom"

34.   android:ems="10"

35.   android:inputType="text" > 36.

37.        <requestFocus /> 38. </EditText> 39.

40.   <Button

41.   android:id="@+id/button_valider"

42.   android:layout_width="wrap_content"

43.   android:layout_height="wrap_content"

44.   android:layout_alignBaseline="@+id/editText_nom"

45.   android:layout_alignBottom="@+id/editText_nom"

46.   android:layout_marginLeft="50dp"

47.   android:layout_toRightOf="@+id/editText_nom" 48. android:text="@string/btn_Valider" /> 49.

50.   <Button

51.   android:id="@+id/button_vue2"

52.   android:layout_width="wrap_content"

53.   android:layout_height="wrap_content"

54.   android:layout_alignLeft="@+id/textView_nom"

55.   android:layout_below="@+id/textView_nom"

56.   android:layout_marginTop="25dp" 57.        android:text="@string/btn_vue2" /> 58.

59.</RelativeLayout>

•   lignes 17-25 : le composant [textView_nom] est positionné sous le composant [textView_titre] (ligne 22) à une distance de 35 pixels (ligne 24) ;

•   lignes 27-38 : le composant [editText_Nom] est positionné à droite du composant [textView_nom] (ligne 33) à une distance de 49 pixels (ligne 32). Il a un attribut inputType ligne 35. Cet attribut est obtenu de la façon suivante (clic droit sur le composant / InputType) :

•   lignes 40-48 :  le composant [button_valider] est positionné à droite du composant [editText_Nom] (ligne 47) à une distance de 50 pixels (ligne 46) ;

•   ligne 54 : le côté gauche du composant [button_vue2] est aligné avec le côté gauche du composant [textView_nom] ;

•   lignes 55-56 : le composant [button_vue2] est positionné à 25 pixels au-dessous du composant [textView_Nom] ;

Tous les textes proviennent du fichier [] [6] suivant :

1. <?xml version="1.0" encoding="utf-8"?> 2. <resources> 3.

4.    <string name="app_name">exemple-01</string>

5.    <string name="action_settings">Settings</string>

6.    <string name="hello_world">Hello world!</string>

7.    <string name="vue1_titre">Vue n° 1</string>

8.    <string name="textView_nom">Quel est votre nom :</string>

9.    <string name="btn_Valider">Validez</string> 10.    <string name="btn_vue2">Vue n° 2</string> 11.

12.</resources>

Maintenant, modifions l'activité [MainActivity] pour que cette vue soit affichée au démarrage de l'application :

1.

@Override

2.

protected void onCreate(Bundle savedInstanceState) {

3.

super.onCreate(savedInstanceState);

4.

setContentView(R.layout.vue1);

5.

}

•   ligne 4 : c'est la vue [] qui est désormais affichée ;

Exécutez l'application et vérifiez que c'est bien la vue [] qui est affichée :

 2.7

Gestion des événements

Gérons maintenant le clic sur le bouton [Validez] de la vue [Vue1] :

Le code de [MainActivity] évolue comme suit :

1. package .android; 2.

3.

4.

5. public class MainActivity extends Activity { 6.

7.    // les champs de la vue

8.    private EditText edtNom;

9.    private Button btnValider; 10.    private Button btnVue2; 11.

12.       @Override

13.       protected void onCreate(Bundle savedInstanceState) {

14.       super.onCreate(savedInstanceState);

15.       setContentView(R.layout.vue1);

16.       // on récupère les composants de la vue

17.       edtNom = (EditText) findViewById(R.id.editText_nom);

18.       btnValider = (Button) findViewById(R.id.button_valider);

19.       btnVue2 = (Button) findViewById(R.id.button_vue2);

20.       // gestionnaire d'évts

21.       // bouton [Valider]

22.       btnValider.setOnClickListener(new OnClickListener() {

23.       @Override

24.       public void onClick(View arg0) {

25.       doValider();

26.       }

27.       });

28.       // bouton [Vue2]

29.       btnVue2.setOnClickListener(new OnClickListener() {

30.       @Override

31.       public void onClick(View arg0) {

32.       // on passe à la vue n° 2

33.       navigateToView2();

34.       }

35.       });

36.

37.   }

38.

39.   protected void navigateToView2() {

40.   // TODO Auto-generated method stub

41.   }

42.

43.   @Override

44.   public boolean onCreateOptionsMenu(Menu menu) {

45.   // Inflate the menu; this adds items to the action bar if it is present.

46.   getMenuInflater().inflate(R.menu.main, menu);

47.   return true;

48.   }

49.

50.   protected void doValider() {

51.   // on affiche le nom saisi

52.   Toast.makeText(this, String.format("Bonjour %s", edtNom.getText().toString()), Toast.LENGTH_LONG).show();

53.   }

54.}

•      ligne 13 : la méthode exécutée lorsque l'activité est créée. On l'utilise souvent pour récupérer des références sur les composants de la vue qui va être affichée ;

•      ligne 14 : la méthode du parent est appelée. C'est obligatoire ;

•      ligne 17 : on récupère la référence du composant d'id [editText_nom] ;

•      ligne 18 : on récupère la référence du composant d'id [button_valider] ;

•      ligne 19 : on récupère la référence du composant d'id [button_vue2] ;

•      lignes 22-27 : on définit un gestionnaire pour l'événement clic sur le bouton [Valider] ; •  ligne 50 : la méthode qui gère ce clic ; •             ligne 51 : affiche le nom saisi :

•      Toast.makeText( ).show() : affiche un texte à l'écran,

•      le 1er paramètre de makeText est l'activité,

•      le second paramètre est le texte à afficher dans la boîte qui va être affichée par makeText,

•      le troisième paramètre est la durée de vie de la boîte affichée : Toast.LENGTH_LONG ou Toast.LENGTH_SHORT ;

Les lignes 22-27 et 29-35 implémentent une interface avec une classe anonyme. On implémente une interface I avec une classe anonyme avec le code suivant :

new I(){

// implémentation des méthodes de l'interface I

}

Lignes 22-27 :

•      la méthode [setOnClickListener] admet comme paramètre un type implémentant l'interface [OnClickListener] qui a une unique méthode [onClick]. Le paramètre de cette méthode est une référence sur le composant qui a été cliqué ; • sans la méthode de la classe anonyme, on a d'autres solutions :

•      définir une classe implémentant l'interface [OnClickListener]. On peut arriver alors à la création d'un grand nombre de classes pour gérer les divers événements de l'interface ;

•      faire que l'activité [MainActivity] implémente toutes les interfaces dont on peut avoir besoin pour la gestion de ses événements. Cela limite alors la réutilisation de la classe ;

Exécutez le projet et vérifiez qu'il se passe quelque chose lorsque vous cliquez sur le bouton [Validez].

 2.8

Modification du manifeste

Lorsqu'on lance l'exécution du projet [exemple-01], la vue s'affiche dans la tablette avec le focus sur la zone de saisie du nom. Le clavier logiciel est alors automatiquement affiché. Ce n'est pas esthétique car cela cache une partie de la vue. On peut éviter cet affichage en modifiant le fichier [] :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <manifest xmlns:android=";

3.    package=".android"

4.    android:versionCode="1" 5.    android:versionName="1.0" > 6.

7.    <uses-sdk

8.    android:minSdkVersion="11" 9.        android:targetSdkVersion="16" /> 10.

11.   <application

12.   android:allowBackup="true"

13.   android:icon="@drawable/ic_launcher"

14.   android:label="@string/app_name"

15.   android:theme="@style/AppTheme" >

16.   <activity

17.   android:name=".android.MainActivity"

18.   android:label="@string/app_name"

19.   android:windowSoftInputMode="stateHidden" >

20.   <intent-filter>

21.   <action android:name="" /> 22.

23.   <category android:name="android.intent.category.LAUNCHER" />

24.   </intent-filter>

25.   </activity> 26.    </application> 27.

28.</manifest>

C'est la ligne 19 qui empêche l'affichage du clavier logiciel au démarrage de l'application. Vérifiez-le.

 3 Exemple-02 : navigation entre vues

Dans le projet précédent, le bouton [Vue n° 2] n'a pas été exploité. On se propose de l'exploiter en créant une seconde vue et en montrant comment naviguer d'une vue à l'autre. Il y a plusieurs façons de résoudre ce problème. Celle qui est proposée ici est d'associer chaque vue à une activité. Une autre méthode est d'avoir une unique activité de type [FragmentActivity] qui affiche des vues de type [Fragment]. Ce sera la méthode utilisée dans des applications à venir.

 3.1

Création du projet

Comme le nouveau projet est une extension du précédent, nous allons dupliquer le projet [exemple-01] dans le projet [exemple-02].

•   en [1], on copie le projet [exemple-01] ;

•   en [2], on le colle dans l'explorateur de projets ;

•   en [3], on lui donne un nom ;

•   en [4], et un dossier existant ou non. S'il existe, il doit être vide. S'il n'existe pas, il sera créé ;

•   en [5], le nouveau projet.

 3.2

Ajout d'une seconde activité

Pour gérer une seconde vue, nous allons créer une seconde activité. C'est elle qui gèrera la vue n° 2. On est là dans un modèle une vue = une activité. Il y a d'autres modèles.

•   créez une nouvelle activité Android [1-3] ;

•   en [4], prendre une activité vide ;

•   en [5], donner un nom à l'activité. Ce sera le nom de sa classe ; •  en [6], donner un nom à la vue associée. Ici la vue sera [] ;

•   validez par [Finish].

•   en [7], la nouvelle activité ; •                en [8], la vue qui lui a été associée ;

•   en [9], le manifeste a été modifié.

Le manifeste a enregistré la nouvelle activité :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <manifest xmlns:android=";

3.    package=".android"

4.    android:versionCode="1" 5.    android:versionName="1.0" > 6.

7.    <uses-sdk

8.    android:minSdkVersion="11" 9.        android:targetSdkVersion="16" /> 10.

11.   <application

12.   android:allowBackup="true"

13.   android:icon="@drawable/ic_launcher"

14.   android:label="@string/app_name"

15.   android:theme="@style/AppTheme" >

16.   <activity

17.   android:name=".android.MainActivity"

18.   android:label="@string/app_name"

19.   android:windowSoftInputMode="stateHidden" >

20.   <intent-filter>

21.   <action android:name="" /> 22.

23.   <category android:name="android.intent.category.LAUNCHER" />

24.   </intent-filter>

25.   </activity>

26.   <activity

27.   android:name=".android.SecondActivity"

28.   android:label="@string/title_activity_second" >

29.   </activity> 30.    </application> 31.

32.</manifest>

•   lignes 26-29 : la nouvelle activité.

La classe [SecondActivity] générée est la suivante :

1. package .android; 2.

3.    import .Bundle;

4.    import .Activity; 5. import ; 6.

7. public class SecondActivity extends Activity { 8.

9.    @Override

10.   protected void onCreate(Bundle savedInstanceState) {

11.   super.onCreate(savedInstanceState);

12.   setContentView(R.layout.vue2);

13.   }

14.

15.   @Override

16.   public boolean onCreateOptionsMenu(Menu menu) {

17.   // Inflate the menu; this adds items to the action bar if it is present.

18.   getMenuInflater().inflate(R.menu.second, menu);

19.   return true;

20.   }

21.

22.}

•   lignes 10-12 : lorsque l'activité est créée, elle affiche la vue [] (ligne 12) ;

•   lignes 15-20 : gèrent un éventuel menu. Nous n'en avons pas. Ces lignes peuvent être suprimées.

La vue [] est la suivante :

 3.3

Navigation de la vue n° 1 à la vue n° 2

Revenons au code de la classe [MainActivity] qui affiche la vue ° 1. Le passage à la vue n° 2 est pour l'instant géré de la façon suivante :

1.

@Override

2.

protected void onCreate(Bundle savedInstanceState) {

3.

super.onCreate(savedInstanceState);

4.

setContentView(R.layout.vue1);

5.

// on récupère les composants de la vue

6.

edtNom = (EditText) findViewById(R.id.editText_nom);

7.

btnValider = (Button) findViewById(R.id.button_valider);

8.

btnVue2 = (Button) findViewById(R.id.button_vue2);

9.

// gestionnaire d'évts

10.

// bouton [Valider]

11.

btnValider.setOnClickListener(new OnClickListener() {

12.

@Override

13.

public void onClick(View arg0) {

14.

doValider();

15.

}

16.

});

17.

// bouton [Vue2]

18.

btnVue2.setOnClickListener(new OnClickListener() {

19.

@Override

20.

public void onClick(View arg0) {

21.

// on passe à la vue n° 2

22.

navigateToView2();

23.

}

24.

});

25. 26.

}

27. 28.

protected void navigateToView2() {

29. 30.}

// TODO Auto-generated method stub

•   lignes 18-24 : le clic sur le bouton [Vue n° 2] est géré par la méthode [navigateToView2] de la ligne 29. C'est là que nous allons installer le code de navigation.

Le code de navigation vers la vue n° 2 sera le suivant :

1.

// naviguer vers la vue n° 2

2.

protected void navigateToView2() {

3.

// on navigue vers la vue n° 2 en lui passant le nom saisi dans la vue n° 1

4.

// on crée un Intent

5.

Intent intent = new Intent();

6.

// on associe cet Intent à une activité

7.

intent.setClass(this, SecondActivity.class);

8.

// on associe des informations à cet Intent

9.

intent.putExtra("NOM", edtNom.getText().toString().trim());

10.

// on lance l'Intent, ici une activité de type [SecondActivity]

11.

startActivity(intent);

12.

}

Les commentaires décrivent les étapes à réaliser pour le changement de vue :

1.     ligne 5 : créer un objet de type [Intent]. Cet objet va permettre de préciser et l'activité à lancer et les informations à lui passer ;

2.     ligne 7 : associer l'Intent à une activité, ici une activité de type [SecondActivity] qui sera chargée d'afficher la vue n° 2. Il faut se souvenir que l'activité [MainActivity] affiche elle la vue n° 1. Donc on a une vue = une activité. Il nous faudra définir le type [SecondActivity] ;

3.     ligne 9 : de façon facultative, mettre des informations dans l'objet [Intent]. Celles-ci sont destinées à l'activité [SecondActivity] qui va être lancée. Les paramètres de [Intent.putExtra] sont (Object clé , Object valeur). On notera que la méthode [EditText.getText()] qui rend le texte saisi dans la zone de saisie ne rend pas un type [String] mais un type [Editable]. Il faut utiliser la méthode [toString] pour avoir le texte saisi.

4.     ligne 11 : lancer l'activité définie par l'objet [Intent].

 3.4

Configuration de l'environnement d'exécution

Nous avons suffisamment de code pour un test. Créez une configuration d'exécution en suivant ce qui a été fait au paragraphe 2.5, page 12. Nommez-la [exemple-02]. Exécutez-la et vérifiez que le bouton [Vue n° 2] de la vue n°1 vous emmène bien sur la vue n° 2.

 3.5

Construction de la vue n° 2

•   en [1], nous supprimons la vue [] qui ne nous sert plus.

Nous modifions la vue [] de la façon suivante :

Les composants sont les suivants :

                 Id           Type

Rôle

1

textView_titre TextView

Titre de la vue

2

textView_bonjour TextView

un texte

5

button_vue1      Button

pour passer à la vue n° 1

Le fichier XML [] est le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    android:layout_width="match_parent" 4.    android:layout_height="match_parent" > 5.

6.    <TextView

7.    android:id="@+id/textView_titre"

8.    android:layout_width="wrap_content"

9.    android:layout_height="wrap_content"

10.   android:layout_alignParentLeft="true"

11.   android:layout_alignParentTop="true"

12.   android:layout_marginLeft="88dp"

13.   android:layout_marginTop="26dp"

14.   android:text="@string/vue2_titre" 15.        android:textSize="50sp" /> 16.

17.   <TextView

18.   android:id="@+id/textView_bonjour"

19.   android:layout_width="wrap_content"

20.   android:layout_height="wrap_content"

21.   android:layout_alignParentLeft="true"

22.   android:layout_below="@+id/textView_titre"

23.   android:layout_marginLeft="45dp"

24.   android:layout_marginTop="35dp"

25.   android:text="@string/textView_bonjour" /> 26.

27.   <Button

28.   android:id="@+id/button_vue1"

29.   android:layout_width="wrap_content"

30.   android:layout_height="wrap_content"

31.   android:layout_alignLeft="@+id/textView_bonjour"

32.   android:layout_below="@+id/textView_bonjour"

33.   android:layout_marginTop="25dp" 34.        android:text="@string/btn_vue1" /> 35.

36.</RelativeLayout>

Exécutez le projet [exemple-02] et vérifiez que vous obtenez bien la nouvelle vue.

 3.6

Exploitation des informations de l'Intent de l'activité

Dans [MainActivity], nous avons écrit le code suivant :

1.

// naviguer vers la vue n° 2

2.

protected void navigateToView2() {

3.

// on navigue vers la vue n° 2 en lui passant le nom saisi dans la vue n° 1

4.

// on crée un Intent

5.

Intent intent = new Intent();

6.

// on associe cet Intent à une activité

7.

intent.setClass(this, SecondActivity.class);

8.

// on associe des informations à cet Intent

9.

intent.putExtra("NOM", edtNom.getText().toString().trim());

10.

// on lance l'Intent, ici une activité de type [SecondActivity]

11. 12.}

startActivity(intent);

Ligne 9, nous avons mis pour [SecondActivity] des informations qui n'ont pas été exploitées. Nous les exploitons maintenant et cela se passe dans le code de [SecondActivity] :

On ajoute au code de [SecondActivity] une méthode [onResume]. Cette méthode est l'une des méthodes exécutées juste avant l'affichage de la vue. C'est donc un endroit pour préparer la vue. Le code de [SecondActivity] évolue comme suit :

1.

package .android;

2. 3.

import .Activity;

4.

import android.content.Intent;

5.

import .Bundle;

6.

import android.widget.TextView;

7. 8.

public class SecondActivity extends Activity {

9.

10.

private TextView textViewBonjour;

11. 12.

@Override

13.

protected void onCreate(Bundle savedInstanceState) {

14.

super.onCreate(savedInstanceState);

15.

setContentView(R.layout.vue2);

16.

// on récupère les composants de la vue

17.

textViewBonjour = (TextView) findViewById(R.id.textView_bonjour);

18.

}

19. 20.

@Override

21.

protected void onResume() {

22.

super.onResume();

23.

// on récupère l'intent s'il existe

24.

Intent intent = getIntent();

25.

if (intent != null) {

26.

Bundle extras = intent.getExtras();

27.

if (extras != null) {

28.

// on récupère le nom

29.

String nom = extras.getString("NOM");

30.

if (nom != null) {

31.

// on l'affiche

32.

textViewBonjour.setText(String.format("Bonjour %s !", nom));

33.

}

34.

}

35.

}

36.

37.

38.}

}

             

•   ligne 17 : on récupère une référence sur le composant [TextView] de la vue n° 2 ;

•   lignes 21-36 : la méthode [onResume] sera exécutée juste avant l'affichage de la vue n° 2 ;

•   ligne 22 : on doit appeler la méthode [onResume] de la classe parent. C'est obligatoire ;

•   ligne 24 : la classe [Activity] a une méthode [getIntent] qui rend l'objet [Intent] associé à l'activité ;

•   ligne 26 : la méthode [Intent.getExtras] rend un type [Bundle] qui est une sorte de dictionnaire contenant les informations associées à l'objet [Intent] de l'activité ;

•   ligne 29 : on récupère le nom placé dans l'objet [Intent] de l'activité ;

•   ligne 32 : on l'affiche.

Testez cette nouvelle version. Tapez un nom dans la vue n° 1 et vérifiez que la vue n° 2 l'affiche bien.

 3.7

Navigation de la vue n° 2 vers la vue n° 1

Pour naviguer de la vue n° 1 à la vue n° 2 nous allons suivre la procédure vue précédemment :

•   mettre le code de navigation dans l'activité [SecondActivity] qui affiche la vue n° 2 ;

•   écrire la méthode [onResume] dans l'activité [MainActivity] qui affiche la vue n° 1 ;

Le code de [SecondActivity] évolue comme suit :

1. package .android; 2.

3. import .Activity; 4.

5.

6. public class SecondActivity extends Activity { 7.

8.    // les champs de la vue

9.    private TextView textViewBonjour; 10.    private Button btnVue1; 11.

12.       @Override

13.       protected void onCreate(Bundle savedInstanceState) {

14.       super.onCreate(savedInstanceState);

15.       setContentView(R.layout.vue2);

16.       // on récupère les composants de la vue

17.       textViewBonjour = (TextView) findViewById(R.id.textView_bonjour);

18.       btnVue1 = (Button) findViewById(R.id.button_vue1);

19.       // gestionnaire d'évts

20.       // bouton [Vue1]

21.       btnVue1.setOnClickListener(new OnClickListener() {

22.       @Override

23.       public void onClick(View arg0) {

24.       // on passe à la vue n° 2

25.       navigateToView1();

26.       }

27.       });

28.       }

29.

30.  @Override

31.  protected void onResume() {

32 .

33.   }

34.

35.   protected void navigateToView1() {

36.

// on crée un Intent pour l'activité [MainActivity]

37.

Intent intent1 = new Intent();

38.

intent1.setClass(this, MainActivity.class);

39.

// on récupère l'Intent de l'activité courante [SecondActivity]

40.

Intent intent2 = getIntent();

41.

if (intent2 != null) {

42.

Bundle extras2 = intent2.getExtras();

43.

if (extras2 != null) {

44.

// on met le nom dans l'Intent de [MainActivity]

45.

intent1.putExtra("NOM", extras2.getString("NOM"));

46.

}

47.

// on lance [MainActivity]

48.

startActivity(intent1);

49.

}

50. 51.}

}

•   ligne 18 : on récupère une référence sur le bouton [Vue n° 1] ;

•   lignes 21-27 : on associe la méthode [navigateToView1] au clic sur ce bouton ;

•   lignes 35-49 : la méthode [navigateToView1] ;

•   ligne 37 : on crée un nouvel [Intent] ;

•   ligne 38 : associé à l'activité [MainActivity] ;

•   ligne 40 : on récupère l'Intent associé à [SecondActivity] ;

•   ligne 42 : on récupère les informations de cet Intent ;

•   ligne 45 : la clé [NOM] est récupérée dans [intent2] pour être mise dans [intent1] avec la même valeur associée ;

•   ligne 48 : l'activité [MainActivity] est lancée.

Dans le code de [MainActivity] on ajoute la méthode [onResume] suivante :

1.

@Override

2.

protected void onResume() {

3.

super.onResume();

4.

// on récupère l'intent s'il existe

5.

Intent intent = getIntent();

6.

if (intent != null) {

7.

Bundle extras = intent.getExtras();

8.

if (extras != null) {

9.

// on récupère le nom

10.

String nom = extras.getString("NOM");

11.

if (nom != null) {

12.

// on l'affiche

13.

edtNom.setText(nom);

14.

}

15.

}

16.

}

17.

}

Faites ces modifications et testez votre application. Maintenant quand on revient de la vue n° 2 à la vue n° 1, on doit retrouver le nom saisi initialement, ce qui n'était pas le cas jusqu'à maintenant.

 4 Exemple-03 : construire un projet Maven / Android

Qu'avons-nous appris jusqu'à maintenant :

•   construire des vues ;

•   gérer les événements de celles-ci ;

•   naviguer entre-elles.

C'est suffisant pour bon nombre d'applications. Pour les besoins des applications à venir, nous allons explorer de nouveaux domaines :

•   construire un projet Android avec Maven

•   construire une vue avec des onglets ;

•   construire une application architecturée en couches qui communique avec des services distants.

Explorons tout d'abord la création d'un projet Android avec Maven.

•   en [1], créez un projet Maven ;

•   en [2], fixez le dossier d'installation du nouveau projet [exemple-03] ;

•   en [3], choississez l'archétype [de.akquinet.android.archetypes / android-quickstart – version 1.0.10] ;

Si cet archétype ne vous est pas proposé, procédez comme suit :

•   en [4], ajoutez un archétype ;

•   en [5], donnez les références de l'archétype désiré :

•   en [6], donnez les caractéristiques du projet Maven ;

•   en [7], le projet Maven créé.

Le fichier [] généré pour le projet Maven est le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<project xmlns="; xmlns:xsi=";

3.

xsi:schemaLocation=" ">

4.

<modelVersion>4.0.0</modelVersion>

5.

<groupId>exemples</groupId>

6.

<artifactId>exemple-03</artifactId>

7.

<version>0.0.1-SNAPSHOT</version>

8.

<packaging>apk</packaging>

9.

<name>exemple-03</name>

10. 11.

<properties>

12.

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

13.

<platform.version> 4.1.1.4

14.

</platform.version>

15.

<android.plugin.version>3.5.3</android.plugin.version>

16.

</properties>

17. 18.

<dependencies>

19.

<dependency>

20.         <groupId>com.google.android</groupId>

21.         <artifactId>android</artifactId>

22.         <version>${platform.version}</version>

23.         <scope>provided</scope>

24.         </dependency>

25.         </dependencies>

26.         <build>

27.         <finalName>${project.artifactId}</finalName>

28.         <pluginManagement>

29.         <plugins>

30.         <plugin>

31.         <groupId>com.jayway.maven.plugins.android.generation2</groupId>

32.         <artifactId>android-maven-plugin</artifactId>

33.         <version>${android.plugin.version}</version>

34.         <extensions>true</extensions>

35.         </plugin>

36.         </plugins>

37.         </pluginManagement>

38.         <plugins>

39.         <plugin>

40.         <groupId>com.jayway.maven.plugins.android.generation2</groupId>

41.         <artifactId>android-maven-plugin</artifactId>

42.         <configuration>

43.         <sdk>

44.         <platform>16</platform>

45.         </sdk>

46.         </configuration>

47.         </plugin>

48.         </plugins>

49.         </build>

50.</project>

•   les lignes 26-49 décrivent le plugin Maven pour Android. On n'y touchera pas ;

•   lignes 19-24 : le projet a une dépendance sur l'OS Android. On notera qu'elle est de portée [provided], ç-à-d que l'OS ne sera pas embarqué dans le binaire du projet. Nous verrons qu'il faut souvent ajouter une autre dépendance aux projets Maven / Android.

Créez une configuration pour le projet [exemple-03] et exécutez-la. On obtient la vue suivante :

Travail à faire : changez le titre de la vue.

 5 Exemple-04 : maveniser un projet Android existant

Nous montrons ici comment transformer un projet Android existant en projet Maven. C'est intéressant car les projets Maven sont reconnus par tous les IDE (Netbeans, Eclise, Intellj Idea). Dupliquez le projet [exemple-02] dans [exemple-04] en suivant l'exemple du paragraphe 3.1, page 22.

•   en [1], le nouveau projet [exemple-04] ;

•   en [2], copiez le fichier [] du projet [exemple-03] dans le projet [exemple-04] ;

Puis modifiez-le de la façon suivante :

1.

<modelVersion>4.0.0</modelVersion>

2.

<groupId>exemples</groupId>

3.

<artifactId>exemple-04</artifactId>

4.

<version>0.0.1-SNAPSHOT</version>

5.

<packaging>apk</packaging>

6.

<name>exemple-04</name>

•   on met [exemple-04] aux lignes 3 et 6.

Ceci fait, transformez le projet [exemple-04] en projet Maven [Propriétés du projet / Configure / Convert to Maven Project]. Des erreurs de compilation apparaissent alors dans les codes Java de [MainActivity] et [SecondActivity] :

Corrigez-les. Il s'agit d'enlever les annotations [@Override] sur les gestionnaires d'événements.

Créez une configuration d'exécution pour le projet [exemple-04] et exécutez-la.

 6 Exemple-05 : navigation par onglets

Nous allons maintenant explorer les interfaces à onglets.

 6.1

Le projet Android

Créez un projet Android (pas Maven) appelé [exemple-05]. Suivez la démarche du projet [exemple-01] jusqu'à la dernière étape où il y a un changement :

•      en [1], donnez les informations pour le nouveau projet ;

•      en [2], précisez l'API 11 comme API minimale. Ce n'est qu'à partir de cet API que les onglets ont été gérés ;

•      en [3], dans la dernière étape de l'assistant, précisez que vous voulez une navigation à onglets.

Créez un contexte d'exécution pour le projet [exemple-05] et exécutez-la. Vous obtenez une interface avec trois onglets :

Apprenons à programmer ces onglets en en ajoutant un autre.

Le projet [Exemple-05] est composé d'une activité [1] et de deux vues [2].

 6.2

Les vues

La vue [] est la suivante :

1.

<.ViewPager

xmlns:android=";

2.

    xmlns:tools=";

3.

    android:id="@+id/pager"

4.

    android:layout_width="match_parent"

5.

    android:layout_height="match_parent"

6.

tools:context=".MainActivity" />

Cette vue est un conteneur dans lequel vont venir s'afficher des [Fragments]. On notera deux points :

•      ligne 1 : le gestionnaire de disposition porte un nom particulier ;

•      ligne 3 : l'identifiant de ce gestionnaire. Il est utilisé dans le code ;

La vue [] est la suivante :

1.    <RelativeLayout xmlns:android=";

2.    xmlns:tools=";

3.    android:layout_width="match_parent"

4.    android:layout_height="match_parent"

5.    android:paddingBottom="@dimen/activity_vertical_margin"

6.    android:paddingLeft="@dimen/activity_horizontal_margin"

7.    android:paddingRight="@dimen/activity_horizontal_margin"

8.    android:paddingTop="@dimen/activity_vertical_margin" 9. tools:context=".MainActivity$DummySectionFragment" > 10.

11.   <TextView

12.   android:id="@+id/section_label"

13.   android:layout_width="wrap_content" 14.        android:layout_height="wrap_content" /> 15.

16.</RelativeLayout>

On retrouve là des choses connues :

•      ligne 1 : un gestionnaire de disposition de type [RelativeLayout] ;

•      lignes 11-14 : un composant [TextView] nommé [@+id/section_label] ;

 6.3

L'activité

Le code généré pour l'activité est assez complexe. C'est une caractéristique de la programmation Android. Tout devient vite assez complexe.

Le code de [MainActivity] est le suivant :

1.

package .android;

2. 3.

import .Locale;

4. 5.

6. 7.

public class MainActivity extends FragmentActivity implements ActionBar.TabListener {

8. 9.

// le gestionnaire de fragments ou sections

10.

SectionsPagerAdapter mSectionsPagerAdapter;

11. 12.

// le conteneur des fragments

13.

ViewPager mViewPager;

14. 15.

@Override

16.

protected void onCreate(Bundle savedInstanceState) {

17.

// classique

18.

super.onCreate(savedInstanceState);

19.

setContentView(R.layout.activity_main);

20. 21.

// la barre d'onglets

22.

final ActionBar actionBar = getActionBar();

23. 24.

actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

25.   // instanciation de notre gestionnaire de fragments

26.   mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); 27.

28.   // on récupère la référence du conteneur de fragments

29.   mViewPager = (ViewPager) findViewById(R.id.pager);

30.   // et associé à notre gestionnaire de fragments 31.       mViewPager.setAdapter(mSectionsPagerAdapter); 32.

33.   // on crée autant d'onglets qu'il y a de fragments affichés par le conteneur

34.   for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {

35.   // actionBar est la barre d'onglets

36.   // actionBar.newTab() crée un nouvel onglet

37.   // actionBar.newTab().setText() donne un titre à cet onglet

38.   // actionBar.newTab().setText().setTabListener(this) indique que cette classe gère les évts des onglets 39.

actionBar.addTab(actionBar.newTab().setText(mSectionsPagerAdapter.getPageTitle(i)).setTabLi stener(this));

40.   }

41.   }

42.

43.

44.   @Override

45.   public void onTabSelected( tab, FragmentTransaction fragmentTransaction) {

46.   // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments

47.   mViewPager.setCurrentItem(tab.getPosition());

48.   }

49.

50.  @Override

51.  public void onTabUnselected( tab, FragmentTransaction fragmentTransaction) {

52.  }

53.

54.  @Override

55.  public void onTabReselected( tab, FragmentTransaction fragmentTransaction) {

56.  }

57.

58.  // notre gestionnaire de fragments

59.  // à redéfinir pour chaque application

60.  // doit définir les méthodes suivantes

61.  // getItem, getCount, getPageTitle

62.  public class SectionsPagerAdapter extends FragmentPagerAdapter {

63 .

64.   }

65.

66.    // un fragment est une vue affichée par un conteneur de fragments 67.    public static class DummySectionFragment extends Fragment {

68 .

69.   }

70.

71.}

•      ligne 7 : l'activité dérive de la classe [FragmentActivity]. C'est nouveau. Dans les exemples précédents, elle dérivait de la classe [Activity]. Alors que la classe [Activity] ne gérait qu'une vue, la classe [FragmentActivity] permet de gérer plusieurs vues appelées [Fragments] ;

•      ligne 13 : Android fournit un conteneur de vues de type [.ViewPager]. Il faut fournir à ce conteneur un gestionnaire de vues ou fragments. C'est le développeur qui le fournit ;

•      ligne 10 : le gestionnaire de fragments utilisé dans cet exemple. Son implémentation est aux lignes 62-64 ;

•      ligne 16 : la méthode exécutée à la création de l'activité ;

•      ligne 19 : la vue [] est associée à l'activité. Cette vue est un conteneur dans lequel vont venir s'afficher des [Fragments] :

1.

<.ViewPager

xmlns:android=";

2.

    xmlns:tools=";

3.

    android:id="@+id/pager"

4.

    android:layout_width="match_parent"

5.

    android:layout_height="match_parent"

6.

tools:context=".MainActivity" />

Les fragments vont venir s'insérer dans le conteneur d'id [pager] de la ligne 3.

•      ligne 26 : le gestionnaire de fragments est instancié. Le paramètre du constructeur est la classe Android

[.FragmentManager] ;

•      ligne 29 : on récupère dans la vue [] la référence du conteneur de fragments ;

•      ligne 31 : le gestionnaire de fragments est lié au conteneur de fragments ;

Le gestionnaire de fragments [SectionsPagerAdapter] est le suivant :

1.

// notre gestionnaire de fragments

2.

// à redéfinir pour chaque application

3.

// doit définir les méthodes suivantes

4.

// getItem, getCount, getPageTitle

5.

public class SectionsPagerAdapter extends FragmentPagerAdapter {

6. 7.

// constructeur

8.

public SectionsPagerAdapter(FragmentManager fm) {

9.

super(fm);

10.

}

11. 12.

// doit rendre le fragment n° i avec ses éventuels arguments

13.

@Override

14.

public Fragment getItem(int position) {

15.

// création du fragment et donc d'une vue

16.

Fragment fragment = new DummySectionFragment();

17.

// les arguments du fragment - ici [position+1]

18.

Bundle args = new Bundle();

19.

args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1);

20.

fragment.setArguments(args);

21.

// on rend le fragment

22.

return fragment;

23.

}

24. 25.

// rend le nombre de fragments à gérer

26.

@Override

27.

public int getCount() {

28.

// 3 fragments

29.

return 3;

30.

}

31. 32.

// rend le titre du fragment n° position

33.

@Override

34.

public CharSequence getPageTitle(int position) {

35.

Locale l = Locale.getDefault();

36.

switch (position) {

37.

case 0:

38.

return getString(R.string.title_section1).toUpperCase(l);

39.

case 1:

40.

return getString(R.string.title_section2).toUpperCase(l);

41.

case 2:

42.

return getString(R.string.title_section3).toUpperCase(l);

43.

}

44.

return null;

45.

}

46.

}

•      ligne 5 : le gestionnaire de fragments étend la classe Android [.FragmentManager]. Le constructeur nous est imposé. Nous devons définir trois méthodes :

•      int getCount() : rend le nombre de fragments à gérer,

•      Fragment getItem(i) : rend le fragment n° i,

•      CharSequence getPageTitle(i) : rend le titre du fragment n° i ;

•      lignes 27-30 : getCount rend le nombre de fragments gérés, ici trois ;

•      ligne 14 : getItem(i) rend le fragment n° i. Ici tous les fragments seront identiques de type [DummySectionFragment] ;

•      ligne 16 : le fragment est instancié. On peut lui transmettre des informations via un type [Bundle] ;

•      ligne 18 : un [Bundle] est créé. C'est une sorte de dictionnaire (clé, valeur) ;

•      ligne 19 : la valeur (i+1) est associée à la clé [DummySectionFragment.ARG_SECTION_NUMBER] ;

•      ligne 20 : le [Bundle] est passé au fragment ;

•      ligne 22 : le fragment est rendu ;

•      ligne 34 : les titres des trois fragments sont trouvés dans le fichier [] :

1.

    <string name="title_section1">Section 1</string>

2.

    <string name="title_section2">Section 2</string>

3.

<string name="title_section3">Section 3</string>

Les trois fragments créés sont du type [DummySectionFragment] suivant :

1.    // un fragment est une vue affichée par un conteneur de fragments

2.    public static class DummySectionFragment extends Fragment { 3.

4.     public static final String ARG_SECTION_NUMBER = "section_number"; 5.

6.    public DummySectionFragment() {

7.    }

8.

9.     @Override

10.      public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

11.      // le fragment est associé à la vue [fragment_main_dummy]

12.      View rootView = inflater.inflate(R.layout.fragment_main_dummy, container, false);

13.      // on récupère une référence sur le [TextView]

14.      TextView dummyTextView = (TextView) rootView.findViewById(R.id.section_label);

15.      // on lui affecte un texte

16.      dummyTextView.setText(Integer.toString(getArguments().getInt(ARG_SECTION_NUMBER)));

17.      // on retourne la vue créée

18.      return rootView;

19.      }

20.      }

Un fragment doit définir la méthode [onCreateView] de la ligne 10. Cette méthode doir rendre la vue associée au fragment.

•      ligne 12 : la vue [] est associée au fragment. Nous avons vu que cette vue n'avait qu'un seul composant, un [TextView] ;

•      ligne 14 : on récupère une référence sur ce [TextView] ;

•      ligne 16 : on récupère les arguments du fragment. On se rappelle que lorsqu'il a été créé, le fragment a reçu des arguments, un entier associé à la clé [DummySectionFragment.ARG_SECTION_NUMBER]. Le code

[Integer.toString(getArguments().getInt(ARG_SECTION_NUMBER))] récupère cet entier. Celui-ci est ensuite donné comme texte du [TextView] ;

•      ligne 18 : on rend la vue ainsi créée ;

Pour l'instant, nous n'avons pas parlé des onglets à dessein. Les notions de fragments et de conteneur de fragments sont indépendantes des onglets. On peut les utiliser avec des vues sans onglets. La gestion des onglets est faite aux lignes suivantes :

1.

// on crée autant d'onglets qu'il y a de fragments affichés par le conteneur

2.

for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {

3.

// actionBar est la barre d'onglets

4.

// actionBar.newTab() crée un nouvel onglet

5.

// actionBar.newTab().setText() donne un titre à cet onglet

6.

// actionBar.newTab().setText().setTabListener(this) indique que cette classe gère les évts des onglets

7.

actionBar.addTab(actionBar.newTab().setText(mSectionsPagerAdapter.getPageTitle(i)).setTabLi stener(this));

8.    }

9.    }

10.

11.

12.   @Override

13.   public void onTabSelected( tab, FragmentTransaction fragmentTransaction) {

14.   // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments

15.   mViewPager.setCurrentItem(tab.getPosition());

16.   }

17.

18.  @Override

19.  public void onTabUnselected( tab, FragmentTransaction fragmentTransaction) {

20.  }

21.

22.  @Override

23.  public void onTabReselected( tab, FragmentTransaction fragmentTransaction) {

24.  }

On notera ligne 15, la façon de changer de fragment dans le conteneur de fragments.

Créez un environnement de configuration pour le projet [exemple-05] et exécutez-le.

 6.4

Un nouveau fragment

Apprenons à créer un fragment et à l'afficher. Tout d'abord copions la vue [] du projet [exemple-01] dans le projet [exemple-05].

•      en [1], la vue [] présente des erreurs ;

•      en [2], ces erreurs proviennent de textes absents dans le fichier [] ;

On rajoute les textes manquants en les prenant dans le fichier [] du projet [exemple-01] :

1.

<?xml version="1.0" encoding="utf-8"?>

2.

<resources>

3. 4.

    <string name="app_name">Exemple-05</string>

5.

    <string name="action_settings">Settings</string>

6.

    <string name="title_section1">Section 1</string>

7.

    <string name="title_section2">Section 2</string>

8.    <string name="title_section3">Section 3</string>

9.    <string name="vue1_titre">Vue n° 1</string>

10.   <string name="textView_nom">Quel est votre nom :</string>

11.   <string name="btn_Valider">Validez</string> 12.    <string name="btn_vue2">Vue n° 2</string>

13.   

14.</resources>

Maintenant, nous créons la classe [Vue1Fragment] qui va être le fragment chargé d'afficher la vue [] :

Pour créer la classe, nous nous inspirons de la classe [DummySectionFragment] étudiée précédemment :

1. package .android; 2.

3.

4.

5.    // un fragment est une vue affichée par un conteneur de fragments

6.    public class Vue1Fragment extends Fragment { 7.

8.    // les champs de la vue affichée par le fragment

9.    private EditText edtNom;

10.   private Button btnValider; 11.    private Button btnVue2; 12.

13.       @Override

14.       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

15.       // le fragment est associé à la vue [vue1]

16.       View rootView = inflater.inflate(R.layout.vue1, container, false);

17.       // on récupère les composants de la vue

18.       edtNom = (EditText) rootView.findViewById(R.id.editText_nom);

19.       btnValider = (Button) rootView.findViewById(R.id.button_valider);

20.       btnVue2 = (Button) rootView.findViewById(R.id.button_vue2);

21.       // gestionnaire d'évts

22.       // bouton [Valider]

23.       btnValider.setOnClickListener(new OnClickListener() {

24.       @Override

25.       public void onClick(View arg0) {

26.       doValider();

27.       }

28.       });

29.       // bouton [Vue2]

30.       btnVue2.setOnClickListener(new OnClickListener() {

31.       @Override

32.       public void onClick(View arg0) {

33.       // on passe à la vue n° 2

34.       navigateToView2();

35.       }

36.       });

37.       // on retourne la vue créée

38.       return rootView;

39.       }

40.

41.   private void navigateToView2() {

42.      // TODO

43.

44.   }

45.

46.   protected void doValider() {

47.   // on affiche le nom saisi

48.   Toast.makeText(getActivity(), String.format("Bonjour %s", edtNom.getText().toString()), Toast.LENGTH_LONG).show();

49.   }

50.}

•   ligne 14 : la méthode [onCreateView] doit être implémentée par le fragment. C'est dans cette méthode qu'on associe une vue au fragment ;

•   ligne 16 : la vue [] est associée au fragment ;

•   lignes 18-20 : on récupère les composants de la vue [rootView], ç-à-d de [] ;

•   le reste est repris du projet [exemple-01] ;

•   lignes 41-44 : nous n'implémentons pas la navigation vers la vue [Vue n°2]. Nous allons le faire ultérieurement avec le conteneur de fragments ;

•   ligne 48 : le premier paramètre de [Toast.makeText] est de type [Activity]. La méthode [Fragment.getActivity()] permet d'avoir l'activité dans laquelle se trouve le fragment. Il s'agit de [MainActivity] puisque dans cette architecture, nous n'avons qu'une activité qui affiche différentes vues ou fragments ;

Exécutez l'environnement de configuration du projet [exemple-05]. Vous devez obtenir la vue suivante :

Si on clique sur l'onglet [VUE N° 1], on obtient la vue suivante :

Si nous tapons un nom et que nous validons, on obtient la vue suivante :

Maintenant passez sur un autre onglet et revenez sur l'onglet [VUE N° 1]. On obtient la vue suivante :

Ci-dessus, le nom saisi a été conservé alors que dans le projet [exemple-02] il disparaissait lorsqu'on naviguait vers une autre vue et qu'on revenait. En fait, le conteneur de fragments garde les fragments en mémoire et ne les régénère pas de nouveau lorsqu'ils doivent être réaffichés. On retrouve ainsi le fragment dans l'état où on l'a laissé.

 6.5

Désactiver le Swipe ou Balayage

Dans l'application précédente, lorsque vous balayez la tablette avec la main vers la gauche ou la droite, la vue courante laisse alors place à la vue de droite ou de gauche selon les cas. Dans les applications que nous allons écrire, ce comportement ne sera pas souhaitable. On voudra passer d'une vue à une autre seulement si certaines conditions sont remplies. Nous allons apprendre à désactiver le balayage des vues (swipe).

Revenons sur la vue XML principale [activity_main] :

Le code XML de la vue est le suivant :

1.

<.ViewPager

xmlns:android=";

2.

    xmlns:tools=";

3.

    android:id="@+id/pager"

4.

    android:layout_width="match_parent"

5.

    android:layout_height="match_parent"

6.

tools:context=".MainActivity" />

La ligne 1 désigne la classe qui gère les pages de l'activité. On retrouve cette classe dans l'activité [MainActivity] :

1. import .ViewPager; 2.

3.

4. public class MainActivity extends FragmentActivity implements ActionBar.TabListener { 5.

6.     // le gestionnaire de fragments ou sections 7.     SectionsPagerAdapter mSectionsPagerAdapter; 8.

9.    // le conteneur des fragments

10.ViewPager mViewPager;

Ligne 10, le gestionnaire de pages est de type [.ViewPager] (ligne 1). Pour désactiver le balayage, on est amené à dériver cette classe de la façon suivante :

1.

package .android;

2. 3.

import android.content.Context;

4.

import .ViewPager;

5.

import .AttributeSet;

6.

import .MotionEvent;

7. 8.

public class MyPager extends ViewPager {

9.

10.

// contrôle le swipe

11.

boolean isSwipeEnabled;

12. 13.

public MyPager(Context context) {

14.

super(context);

15. 16.

}

17.

public MyPager(Context context, AttributeSet attrs) {

18.

super(context, attrs);

19.

}

20. 21.

@Override

22.

public boolean onInterceptTouchEvent(MotionEvent event) {

23.

// swipe autorisé ?

24.

if (isSwipeEnabled) {

25.

return super.onInterceptTouchEvent(event);

26.

} else {

27.

return false;

28.

}

29.

}

30. 31.

@Override

32.

public boolean onTouchEvent(MotionEvent event) {

33.

// swipe autorisé ?

34.

if (isSwipeEnabled) {

35.

return super.onTouchEvent(event);

36.

} else {

37.

return false;

38.

}

39.

}

40. 41.

// setter

42.

public void setSwipeEnabled(boolean isSwipeEnabled) {

43.

this.isSwipeEnabled = isSwipeEnabled;

44.

45.

46.}

}

     

•   ligne 8 : la classe [MyPager] étend la classe [ViewPager] ;

•   sur un balayage de la main, les gestionnaires d'événements des lignes 22 et 32 peuvent être appelés. Elles rendent toutes deux un booléen. Il leur suffit de rendre le booléen [false] pour inhiber le balayage ;

•   ligne 11 : le booléen qui sert à indiquer si on accepte ou non le balayage de la main.

Ceci fait, il faut utiliser désormais notre nouveau gestionnaire de pages. Cela se fait dans la vue XML [activity_main] et dans l'activité principale [MainActivity]. Dans [activity_main] on écrit :

1.

<.android.MyPager xmlns:android=";

2.

    xmlns:tools=";

3.

    android:id="@+id/pager"

4.

    android:layout_width="match_parent"

5.

    android:layout_height="match_parent"

6.

tools:context=".MainActivity" />

Ligne 1, on utilise la nouvelle classe. Dans [MainActivity], le code évolue comme suit :

1.

public class MainActivity extends FragmentActivity implements ActionBar.TabListener {

2. 3.

// le gestionnaire de fragments ou sections

4.

SectionsPagerAdapter mSectionsPagerAdapter;

5. 6.

// le conteneur des fragments

7.

MyPager mViewPager;

8. 9.

@Override

10.

protected void onCreate(Bundle savedInstanceState) {

11.

// classique

12.

super.onCreate(savedInstanceState);

13.

setContentView(R.layout.activity_main);

14. 15.

// la barre d'onglets

16.

final ActionBar actionBar = getActionBar();

17.

actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

18. 19.

// instanciation de notre gestionnaire de fragments

20.

mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

21. 22.

// on récupère la référence du conteneur de fragments

23.

mViewPager = (MyPager) findViewById(R.id.pager);

24.

// il est associé à notre gestionnaire de fragments

25.

mViewPager.setAdapter(mSectionsPagerAdapter);

26.

// on inhibe le swipe

27.

mViewPager.setSwipeEnabled(false);

28.

29.

30 .

// on crée autant d'onglets qu'il y a de fragments affichés par le conteneur

     

•   ligne 7 : le gestionnaire de pages a le type [MyPager] ;

•   ligne 27 : on inhibe ou non le balayage de la main.

Testez cette nouvelle version. Inhibez ou non le balayage et constatez la différence de comportement des vues. Dans toutes les applications à venir, le balayage sera inhibé. Nous ne le rappellerons pas.

 7 Exemple-06 : La navigation entre vues revisitée

Dans le projet [exemple-02] nous avons introduit la navigation entre vues. Il s'agissait alors d'une navigation entre activités : 1 vue =

1 activité. Nous nous proposons ici d'avoir 1 activité avec plusieurs vues. L'activité sera de type [FragmentActivity] et la vue de type [Fragment].

 7.1

Le projet

Dupliquons le projet [exemple-05] dans [exemple-06] comme il a été fait pour le projet [exemple-02] :

•   en [1], le projet [exemple-06] ;

•   en [2], les vues de ce projet ;

•   en [3], on supprime la vue [] qui ne sert plus ; •                en [4], apparaissent des erreurs de compilation. On les ignore pour l'instant.

Nous allons ajouter la vue [VUE N° 2] comme nouveau fragment et apprendre comment naviguer de la vue n° 1 à la vue n° 2 et vice-versa.

En [1] ci-dessus, nous copions la vue [] à partir du projet [exemple-02]. Nous avons des erreurs [2]. Pour les corriger, il suffit d'ajouter au fichier [] de [exemple-06] des chaînes provenant du fichier [] de [exemple-02] :

    <string name="vue2_titre">Vue n° 2</string>

    <string name="btn_vue1">Vue n° 1</string>

    <string name="textView_bonjour">"Bonjour "</string>

Ceci fait, nous n'avons plus d'erreurs sur la vue []. Nous créons maintenant le fragment [Vue2Fragment] qui va afficher [] :

Son code sera le suivant :

1. package .android; 2.

3. import .Bundle; 4. .

5.

6.    // un fragment est une vue affichée par un conteneur de fragments

7.    public class Vue2Fragment extends Fragment { 8.

9.    // les champs de la vue

10.   private TextView textViewBonjour; 11.    private Button btnVue1; 12.

13.       @Override

14.       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

15.       // le fragment est associé à la vue [vue2]

16.       View rootView = inflater.inflate(R.layout.vue2, container, false);

17.       // on récupère les composants de la vue

18.       textViewBonjour = (TextView) rootView.findViewById(R.id.textView_bonjour);

19.       btnVue1 = (Button) rootView.findViewById(R.id.button_vue1);

20.       // gestionnaire d'évts

21.       // bouton [Vue1]

22.       btnVue1.setOnClickListener(new OnClickListener() {

23.       @Override

24.       public void onClick(View arg0) {

25.       // on passe à la vue n° 2

26.       navigateToView1();

27.       }

28.       });

29.       // on retourne la vue créée

30.       return rootView;

31.       }

32.

33.   private void navigateToView1() {

34.   // TODO

35.

36.   }

37.

38.}

L'activité [MainActivity] évolue de la façon suivante :

1.

package .android;

2. 3.

import .Locale;

4. 5.

import .ActionBar;

6.

7.

8. public class MainActivity extends FragmentActivity{ 9.

10.    // le gestionnaire de fragments ou sections 11.    SectionsPagerAdapter mSectionsPagerAdapter; 12.

13.    // le conteneur des fragments 14. MyPager mViewPager; 15.

16.   @Override

17.   protected void onCreate(Bundle savedInstanceState) {

18.   // classique

19.   super.onCreate(savedInstanceState); 20.  setContentView(R.layout.activity_main); 21.

22.   // instanciation de notre gestionnaire de fragments

23.   mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); 24.

25.   // on récupère la référence du conteneur de fragments

26.   mViewPager = (MyPager) findViewById(R.id.pager);

27.   // il est associé à notre gestionnaire de fragments

28.   mViewPager.setAdapter(mSectionsPagerAdapter);

29.   // on inhibe le swipe

30.   mViewPager.setSwipeEnabled(false);

31.   }

32.

33.

34.   // notre gestionnaire de fragments

35.   // à redéfinir pour chaque application

36.   // doit définir les méthodes suivantes

37.   // getItem, getCount, getPageTitle

38.   public class SectionsPagerAdapter extends FragmentPagerAdapter { 39.

40.   // les fragments

41.   Fragment[] fragments = { new Vue1Fragment(), new Vue2Fragment() }; 42.

43.      // constructeur

44.      public SectionsPagerAdapter(FragmentManager fm) {

45.      super(fm);

46.      }

47.

48.      // doit rendre le fragment n° i avec ses éventuels arguments

49.      @Override

50.      public Fragment getItem(int position) {

51.      // on rend le fragment

52.      return fragments[position];

53.      }

54.

55.      // rend le nombre de fragments à gérer

56.      @Override

57.      public int getCount() {

58.      // 2 fragments

59.      return 2;

60.      }

61.

62.       // rend le titre du fragment n° position

63.       @Override

64.       public CharSequence getPageTitle(int position) {

65.       Locale l = Locale.getDefault();

66.       switch (position) {

67.       case 0:

68.       return getString(R.string.vue1_titre).toUpperCase(l);

69.

case 1:

70.

return getString(R.string.vue2_titre).toUpperCase(l);

71.

}

72.

return null;

73.

}

74.

75.

76.}

}

•   il n'y a plus d'onglets donc plus de gestion d'onglets ;

•   ligne 8 : l'activité étend la classe [FragmentActivity] ;

•   lignes 23-30 : création du conteneur de fragments ;

•   ligne 38 : notre gestionnaire de fragments ;

•   lignes 57-60 : il y a deux fragments ;

•   ligne 41 : les deux fragments sont dans un tableau ;

•   lignes 50-53 : on rend le fragment n° [position] dans le tableau ;

Créez une configuration d'exécution pour le projet [exemple-06] puis exécutez-la. Le premier fragment est affiché. Pour nous c'est la vue [Vue n° 1].

 7.2

Mise en place de la navigation

C'est la classe [MainActivity] qui va assurer la navigation. L'activité unique du projet est une instance de cette classe. Cette activité est accessible à tous les fragments. Aussi l'utiliserons-nous également pour passer de l'information d'un fragment à un autre. Le code de [MainActivity] évolue comme suit :

1. package .android; 2.

3. import .Locale; 4. .

5.

6. public class MainActivity extends FragmentActivity { 7.

8.    ..

9.    // informations échangées entre les vues 10.    private String nom; 11.

12.  @Override

13.  protected void onCreate(Bundle savedInstanceState) {

14 .

15.   }

16.

17.   // navigation

18.   public void navigateToView(int i) {

19.   // on affiche le fragment n° i

20.   mViewPager.setCurrentItem(i);

21.   }

22.

23.   // getters et setters

24.   public String getNom() {

25.   return nom;

26.   }

27.

28.   public void setNom(String nom) {

29.   this.nom = nom;

30.   }

31.

32.  // notre gestionnaire de fragments

33.  public class SectionsPagerAdapter extends FragmentPagerAdapter {

34 ..

35.   }

36.

37.}

•   ligne 10 : comme l'activité est unique et partagée entre tous les fragments, elle peut servir d'entrepôt pour la communication entre fragments / vues. Ici on mémorisera le nom saisie dans la vue 1 ;

•   lignes 18-20 : la navigation sera implémentée également dans l'activité. Pour naviguer, les fragments utiliseront cette méthode de l'activité.

La navigation dans [Vue1Fragment] est alors la suivante :

1.

private void navigateToView2() {

2.

// on mémorise le nom dans l'activité

3.

activité.setNom(edtNom.getText().toString());

4.

// on navigue vers la vue 2

5.

activité.navigateToView(1);

6.

}

On a l'équivalent dans [Vue2Fragment] :

1.

private void navigateToView1() {

2.

// on navigue vers la vue 1

3.

activité.navigateToView(0);

4.

}

Lorsque la vue n° 2 s'affiche, il faut afficher le nom saisi dans la vue n° 1. Un fragment n'est créé qu'une fois. Aussi ces méthodes [onStart, onResume] ne sont-elles appelées qu'une fois, lors de la création initiale du fragment. Il nous faut un événement qui nous dise que le fragment est devenu visible. C'est l'événement suivant :

1.

@Override

2.

public void setMenuVisibility(final boolean visible) {

3.

super.setMenuVisibility(visible);

4.

if (visible) {

5.

// la vue est visible - on affiche le nom saisi dans la vue 1

6.

textViewBonjour.setText(String.format("Bonjour %s !", activité.getNom()));

7.

}

8.

}

Exécutez de nouveau le projet [exemple-06] et vérifiez son bon fonctionnement.

 7.3

Conclusion

A ce point, nous avons un début d'architecture cohérent pour une application à plusieurs vues :

•   une seule activité gère toutes les vues ;

•   cette activité gère la navigation et la transmission d'information entre vues.

Nous avons désormais les bases pour construire des architectures plus complexes que les précédentes.

 8 Exemple-07 : une architecture à deux couches

Nous allons construire une application à une vue et ayant l'architecture suivante :

 8.1.1      Le projet Android

Nous dupliquons le projet [exemple-06] dans [exemple-07] :

•   en [1], le projet [exemple-07] ;

•   en [2], nous ne gardons que l'activité [MainActivity] et supprimons les deux fragments. Cela fait apparaître des erreurs dans [MainActivity] ;

•   en [3], on supprime les deux vues associées aux deux fragments.

 8.1.2      La vue [vue_01]

Nous allons créer la vue [vue_01] qui permettra de générer des nombres aléatoires :

Ses composants sont les suivants :

           Id         Type

Rôle

1

edtNbAleas EditText

nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]

2

edtA        EditText

valeur de a

2

edtB        EditText

valeur de b

4

btnExécuter Button

lance la génération des nombres

5

ListView     lstReponses

liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

Son code XML est le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    xmlns:tools=";

4.    android:id="@+id/RelativeLayout1"

5.    android:layout_width="match_parent"

6.    android:layout_height="match_parent" 7.    android:orientation="vertical" > 8.

9.    <TextView

10.   android:id="@+id/txt_Titre2"

11.   android:layout_width="wrap_content"

12.   android:layout_height="wrap_content"

13.   android:layout_marginTop="20dp"

14.   android:text="@string/aleas"

15.   android:textAppearance="?android:attr/textAppearanceLarge" /> 16.

17.    <TextView

18.   android:id="@+id/txt_nbaleas"

19.   android:layout_width="wrap_content"

20.   android:layout_height="wrap_content"

21.   android:layout_below="@+id/txt_Titre2"

22.   android:layout_marginTop="20dp" 23.        android:text="@string/txt_nbaleas" /> 24.

25.   <EditText

26.   android:id="@+id/edt_nbaleas"

27.   android:layout_width="wrap_content"

28.   android:layout_height="wrap_content"

29.   android:layout_alignBaseline="@+id/txt_nbaleas"

30.   android:layout_marginLeft="20dp"

31.   android:layout_toRightOf="@+id/txt_nbaleas" 32. android:inputType="number" /> 33.

34.   <TextView

35.   android:id="@+id/txt_errorNbAleas"

36.   android:layout_width="wrap_content"

37.   android:layout_height="wrap_content"

38.   android:layout_alignBaseline="@+id/edt_nbaleas"

39.   android:layout_marginLeft="20dp"

40.   android:layout_toRightOf="@+id/edt_nbaleas"

41.   android:text="@string/txt_errorNbAleas" 42.        android:textColor="@color/red" /> 43.

44.   <TextView

45.   android:id="@+id/txt_a"

46.   android:layout_width="wrap_content"

47.   android:layout_height="wrap_content"

48.   android:layout_below="@+id/txt_nbaleas"

49.   android:layout_marginTop="20dp" 50.        android:text="@string/txt_a" /> 51.

52.   <EditText

53.   android:id="@+id/edt_a"

54.   android:layout_width="wrap_content"

55.   android:layout_height="wrap_content"

56.   android:layout_alignBaseline="@+id/txt_a"

57.   android:layout_marginLeft="20dp"

58.   android:layout_toRightOf="@+id/txt_a" 59.        android:inputType="number" /> 60.

61.   <TextView

62.   android:id="@+id/txt_b"

63.   android:layout_width="wrap_content"

64.   android:layout_height="wrap_content"

65.   android:layout_alignBaseline="@+id/txt_a"

66.   android:layout_marginLeft="20dp"

67.   android:layout_toRightOf="@+id/edt_a" 68.        android:text="@string/txt_b" /> 69.

70.   <EditText

71.   android:id="@+id/edt_b"

72.   android:layout_width="wrap_content"

73.   android:layout_height="wrap_content"

74.   android:layout_alignBaseline="@+id/txt_a"

75.   android:layout_marginLeft="20dp"

76.   android:layout_toRightOf="@+id/txt_b" 77.        android:inputType="number" /> 78.

79.      <TextView

80.      android:id="@+id/txt_errorIntervalle"

81.   android:layout_width="wrap_content"

82.   android:layout_height="wrap_content"

83.   android:layout_alignBaseline="@+id/edt_b"

84.   android:layout_marginLeft="20dp"

85.   android:layout_toRightOf="@+id/edt_b"

86.   android:text="@string/txt_errorIntervalle" 87. android:textColor="@color/red" /> 88.

89.

90.   <Button

91.   android:id="@+id/btn_Executer"

92.   android:layout_width="wrap_content"

93.   android:layout_height="wrap_content"

94.   android:layout_alignParentLeft="true"

95.   android:layout_below="@+id/txt_a"

96.   android:layout_marginTop="20dp" 97.        android:text="@string/btn_executer" /> 98.

99.

100.    <TextView

101.    android:id="@+id/txt_Reponses"

102.    android:layout_width="wrap_content"

103.    android:layout_height="wrap_content"

104.    android:layout_below="@+id/btn_Executer"

105.    android:layout_marginTop="30dp"

106.    android:text="@string/list_reponses"

107.    android:textAppearance="?android:attr/textAppearanceLarge" 108. android:textColor="@color/blue" /> 109.

110.    <ListView

111.    android:id="@+id/lst_reponses"

112.    android:layout_width="match_parent"

113.    android:layout_height="match_parent"

114.    android:layout_alignParentLeft="true"

115.    android:layout_below="@+id/txt_Reponses"

116.    android:layout_marginTop="40dp"

117.    android:background="@color/wheat"

118.    android:clickable="true"

119.    tools:listitem="@android:layout/simple_list_item_1" > 120.    </ListView> 121.

122. </RelativeLayout> 123.

 8.1.3      La vue [activity_main]

La vue XML [activity_main] utilise la classe [.android.activity.MyPager] [2] aussi doit-elle être écrite de la façon suivante :

1.

<.android.activity.MyPager

xmlns:android=";

2.

    xmlns:tools=";

3.

    android:id="@+id/pager"

4.

    android:layout_width="match_parent"

5.

    android:layout_height="match_parent"

6.

tools:context=".MainActivity" />

Ligne 1, il faut utiliser le package exact de la classe [MyPager] sinon une exception sera lancée.

 8.1.4      La couche [métier]

La couche [métier] présente l'interface [IMetier] suivante :

1.

package .android.metier;

2. 3.

import ;

4. 5.

public interface IMetier {

6. 7.

public List<Object> getAleas(int a, int b, int n);

8.

}

La méthode [getAleas(a,b,n)] renvoie normalement n nombres entiers aléatoires dans l'intervalle [a,b]. On a prévu également qu'elle renvoie une fois sur trois une exception, exception également insérée dans les réponses rendues par la méthode. Au final celle-ci rend une liste d'objets de type [Exception] ou [Integer].

L'implémentation [Metier] de cette interface est la suivante :

1. package .android.metier; 2.

3.    import .ArrayList;

4.    import ; 5. import .Random; 6.

7. public class Metier implements IMetier { 8.

9.    @Override

10.   public List<Object> getAleas(int a, int b, int n) {

11.   // la liste des objets

12.   List<Object> réponses = new ArrayList<Object>();

13.   // qqs vérifications

14.   if (n < 1) {

15.   ré(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));

16.   }

17.   if (a < 0) {

18.   ré(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));

19.   }

20.   if (b < 0) {

21.   ré(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));

22.   }

23.   if (a >= b) {

24.

ré(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));

25.

}

26.

// erreur ?

27.

if (ré() != 0) {

28.

return réponses;



29.

}

30.

// on génère les nombres aléatoires

31.

Random random = new Random();

32.

for (int i = 0; i < n; i++) {

33.

// on génère une exception aléatoire 1 fois / 3

34.

int nombre = random.nextInt(3);

35.

if (nombre == 0) {

36.

ré(new AleaException("Exception aléatoire"));

37.

} else {

38.

// sinon on rend un nombre aléatoire entre deux bornes [a,b]

39.

ré(Integer.valueOf(a + random.nextInt(b - a + 1)));

40.

}

41.

}

42.

// résultat

43.

return réponses;

44. 45.}

}

Le type [AleaException] utilisé par la classe [Metier] est la suivante :

1. package .android.metier; 2.

3. public class AleaException extends RuntimeException { 4.

5.   public AleaException() {

6.   }

7.

8.    public AleaException(String detailMessage) {

9.    super(detailMessage);

10.   }

11.

12.   public AleaException(Throwable throwable) {

13.   super(throwable);

14.   }

15.

16.   public AleaException(String detailMessage, Throwable throwable) {

17.   super(detailMessage, throwable);

18.   }

19.

20.}

 8.1.5      L'activité [MainActivity]

L'activité unique du projet est une instance de la classe [MainActivity] suivante :

1. 2.

package .android.activity;

3. import .android.R; 4.

5.

6. public class MainActivity extends FragmentActivity { 7.

8.     // le gestionnaire de fragments ou sections 9.     SectionsPagerAdapter mSectionsPagerAdapter; 10.

11.    // le conteneur des fragments 12. MyPager mViewPager; 13.

14.    // couche [métier] 15.    private IMetier métier; 16.

17.   @Override

18.   protected void onCreate(Bundle savedInstanceState) {

19.   // classique

20.   super.onCreate(savedInstanceState); 21.  setContentView(R.layout.activity_main); 22.

23.   // instanciation de notre gestionnaire de fragments

24.   mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); 25.

26.   // on récupère la référence du conteneur de fragments

27.   mViewPager = (MyPager) findViewById(R.id.pager);

28.   // il est associé à notre gestionnaire de fragments

29.   mViewPager.setAdapter(mSectionsPagerAdapter);

30.   // on inhibe le swipe

31.   mViewPager.setSwipeEnabled(false); 32.

33.   // instanciation couche [métier]

34.   métier = new Metier();

35.   }

36.

37.

38.   // getters et setters

39.   public IMetier getMétier() {

40.   return métier;

41.   }

42.

43.   public void setMétier(IMetier métier) {

44.   this.métier = métier;

45.   }

46.

47.   // notre gestionnaire de fragments

48.   // à redéfinir pour chaque application

49.   // doit définir les méthodes suivantes

50.   // getItem, getCount, getPageTitle

51.   public class SectionsPagerAdapter extends FragmentPagerAdapter { 52.

53.   // les fragments

54.   Fragment[] fragments = { new Vue_01() }; 55.

56.      // constructeur

57.      public SectionsPagerAdapter(FragmentManager fm) {

58.      super(fm);

59.      }

60.

61.      // doit rendre le fragment n° i avec ses éventuels arguments

62.      @Override

63.      public Fragment getItem(int position) {

64.      // on rend le fragment

65.      return fragments[position];

66.

}

67. 68.

// rend le nombre de fragments à gérer

69.

@Override

70.

public int getCount() {

71.

// 1 fragment

72.

return 1;

73.

}

74. 75.

// rend le titre du fragment n° position

76.

@Override

77.

public CharSequence getPageTitle(int position) {

78.

Locale l = Locale.getDefault();

79.

switch (position) {

80.

case 0:

81.

return getString(R.string.app_name).toUpperCase(l);

82.

}

83.

return null;

84.

}

85.

86.

87.}

}

•   ligne 6 : l'activité est de type [FragmentActivity] ;

•   ligne 15 : nous avons dit que l'activité était un bon endroit pour partager des données entre vues. On va y stocker une référence sur la couche [métier]. Celle-ci ne sera instanciée qu'une fois ;

•   ligne 34 : instanciation de la couche [métier] ;

•   ligne 51 : notre gestionnaire de fragments. Ici nous n'en avons qu'un ;

•   ligne 54 : le tableau des fragments est réduit au seul fragment [Vue_01] associé à la vue XML [vue_01] ;

•   ligne 72 : le nombre de fragments ;

 8.1.6      Le fragment de la vue [vue_01]

La classe [Vue_01] est l'unique fragment affiché par l'activité du projet. Son code est le suivant :

1.

package ;

2. 3.

import .android.R;

4.

5. 6.

public class Vue_01 extends Fragment {

7. 8.

// les éléments de l'interface visuelle

9.

private ListView listRéponses;

10.

private Button btnExecuter;

11.

private EditText edtNbAleas;

12.

private EditText edtA;

13.

private EditText edtB;

14.

private TextView txtErrorAleas;

15.

private TextView txtErrorIntervalle;

16. 17.

// les saisies

18.

private int nbAleas;

19.

private int a;

20.   private int b;

21.   // l'activité

22.   private MainActivity activité; 23.

24.   @Override

25.   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

26.   // on note l'activité

27.   activité = (MainActivity) getActivity();

28.   // on crée la vue du fragment à partir de sa définition XML

29.   View view = inflater.inflate(R.layout.vue_01, container, false);

30.   // zones de saisie

31.   edtNbAleas = (EditText) view.findViewById(R.id.edt_nbaleas);

32.   edtA = (EditText) view.findViewById(R.id.edt_a);

33.   edtB = (EditText) view.findViewById(R.id.edt_b); 34.

35.   // les messages d'erreur

36.   txtErrorAleas = (TextView) view.findViewById(R.id.txt_errorNbAleas);

37.   txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle); 38.

39.       // bouton Exécuter

40.       btnExecuter = (Button) view.findViewById(R.id.btn_Executer);

41.       btnExecuter.setOnClickListener(new OnClickListener() {

42.       @Override

43.       public void onClick(View arg0) {

44.       doExecuter();

45.       }

46.       });

47.

48.   // réponses des actions et tâches

49.   listRéponses = (ListView) view.findViewById(R.id.lst_reponses);

50.   // on retourne la vue

51.   return view;

52.   }

53.

54.   protected void doExecuter() {

55 .

56.   }

57.

58.    // on vérifie la validité des données saisies 59.    private boolean isPageValid() {

60 .

61.   }

62.

63.}

•      ligne 6 : la classe est de type [Fragment] ;

•      ligne 27 : la méthode [onCreateView] est exécutée une fois lors de l'instanciation initiale du fragment. On en profite pour définir :

•      des références sur les différents composants de la vue,

•      un gestionnaire pour le clic sur le bouton [Exécuter] ;

•      ligne 29 : on mémorise une référence sur l'activité ;

•      ligne 31 : le fragment est associé à la vue XML [vue_01] ;

•      lignes 32-39 : on récupère les références des objets de la vue ;

•      lignes 41-48 : on définit un gestionnaire d'événements pour le bouton [Exécuter] ;

•      ligne 49 : une référence sur l'objet [ListView] qui va afficher la liste d'objets renvoyée par la couche [métier] ;

•      ligne 51 : on rend la vue créée ;

La méthode [doExecuter] est la suivante :

1.

protected void doExecuter() {

2.

// on efface les éventuels msg d'erreur précédents

3.

txtErrorAleas.setText("");

4.      txtErrorIntervalle.setText("");

5.      // on teste la validité des saisies

6.      if (!isPageValid()) {

7.      return;

8.      }

9.      // on efface les réponses précédentes

10.       listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, new String[]{}));

11.       // on appelle la couche métier pour obtenir les nombres aléatoires

12.       List<Object> data = activité.getMétier().getAleas(a, b, nbAleas);

13.       // on crée une liste de String à partir de ces données

14.       List<String> strings = new ArrayList<String>();

15.       for (Object o : data) {

16.       if (o instanceof Exception) {

17.       (((Exception) o).getMessage());

18.       } else {

19.       (o.toString());

20.       }

21.       }

22.       // on affiche les réponses

23.       listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, strings));

24.       }

•   lignes 6-8 : avant d'exécuter l'action demandée, on vérifie que les valeurs saisies sont correctes ;

•   ligne 12 : la liste des nombres aléatoires est demandée à la couche [métier]. Une référence de celle-ci a été stockée dans l'activité. On obtient une liste d'objets où chaque objet est de type [Integer] ou [AleaException] ;

•   lignes 14-21 : à partir de la liste d'objets obtenue, on crée la liste de [String] qui va être affichée par le composant de type [ListView] de la vue ;

•   ligne 23 : le composant [listRéponses] de type [ListView] va afficher la liste de [String] que nous venons de construire.

La signature de [ListView.setAdapter] est la suivante :

public void setAdapter (ListAdapteradapter)

[ListAdapter] est une interface. La classe [ArrayAdapter] est une classe implémentant cette interface. Le constructeur utilisé ici est le suivant :

public ArrayAdapter (Contextcontext, int resource, int textViewResourceId,List<T> objects)

•   [context] est l'activité qui affiche le [ListView] ;

•   [resource] est l'entier identifiant la vue utilisée pour afficher un élément du [ListView]. Cette vue peut avoir une complexité quelconque. C'est le développeur qui la construit en fonction de ses besoins ;

•   [textViewResourceId] est l'entier identifiant un composant [TextView] dans la vue [resource]. La chaîne affichée le sera par ce composant ;

•   [objects] : la liste d'objets affichés par le [ListView]. La méthode [toString] des objets est utilisée pour afficher l'objet dans le [TextView] identifié par [textViewResourceId] dans la vue identifiée par [resource].

Le travail du développeur est de créer la vue [resource] qui va afficher chaque élément du [ListView]. Pour le cas simple où on ne désire afficher qu'une simple chaîne de caractères comme ici, Android fournit la vue identifiée par

[android.R.layout.simple_list_item_1]. Celle-ci contient un composant [TextView] identifié par [.text1]. C'est la méthode utillisée ligne 23 pour afficher la liste [strings].

La validité des valeurs saisies est vérifiée par la méthode [isPageValid] suivante :

1.

// on vérifie la validité des données saisies

2.

private boolean isPageValid() {

3.

// saisie du nombre de nombres aléatoires

4.

nbAleas = 0;

5.

Boolean erreur = false;

6.

int nbErreurs = 0;

7.

try {

8.

nbAleas = Integer.parseInt(edtNbAleas.getText().toString());

9.

erreur = (nbAleas < 1);

10.

} catch (Exception ex) {

11.

erreur = true;

12.

}

13.

// erreur ?

14.

if (erreur) {

15.

nbErreurs++;

16.

txtErrorAleas.setText(R.string.txt_errorNbAleas);

17.

}

18.

// saisie de a

19.

a = 0;

20.

erreur = false;

21.

try {

22.

a = Integer.parseInt(edtA.getText().toString());

23.

} catch (Exception ex) {

24.

erreur = true;

25.

}

26.

// erreur ?

27.

if (erreur) {

28.

nbErreurs++;

29.

txtErrorIntervalle.setText(R.string.txt_errorIntervalle);

30.

}

31.

// saisie de b

32.

b = 0;

33.

erreur = false;

34.

try {

35.

b = Integer.parseInt(edtB.getText().toString());

36.

erreur = b < a;

37.

} catch (Exception ex) {

38.

erreur = true;

39.

}

40.

// erreur ?

41.

if (erreur) {

42.

nbErreurs++;

43.

txtErrorIntervalle.setText(R.string.txt_errorIntervalle);

44.

}

45.

// retour

46.

return (nbErreurs == 0);

47.

}

       

Ci-dessus on a vérifié simplement que les valeurs saisies étaient des nombres entiers. La couche [métier] est plus exigeante. Si vous entrez un nombre a supérieur au nombre b, la couche [métier] vous renverra une exception.

 8.1.7      Exécution

Créez un contexte d'exécution pour ce projet et exécutez-le.

 8.1.8      Mavenisation du projet

Mavenisez le projet [exemple-07] en suivant la méthode suivie pour le projet [exemple-04]. Modifiez dans [] les caractéristiques du projet de la façon suivante :

<modelVersion>4.0.0</modelVersion>

<groupId>exemples</groupId>

<artifactId>exemple-07</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>apk</packaging>

<name>exemple-07</name>

Lorsque votre projet ne présente plus d'erreurs de syntaxe exécutez le. Vous obtenez l'erreur suivante :

On trouve les logs d'Android dans la fenêtre [Logcat] d'Eclipse :

Les logs indiquent que l'activité [.android.activity.MainActivity] n'a pas été trouvée dans le binaire []. Le problème est complexe car les logs ne nous aident en rien.

Examinons le [Build Path] [1] du projet :

• en [2], la bibliothèque [android-support-v4] est utilisée dans le cadre du projet Eclipse non mavenisé. Il faut ajouter cette dépendance dans le fichier [] :

1.

<dependencies>

2.

<!-- Android -->

3.

<dependency>

4.

<groupId>com.google.android</groupId>

5.

<artifactId>android</artifactId>

6.     <version>${platform.version}</version>

7.     <scope>provided</scope>

8.     </dependency>

9.     <!-- Support Android - NE PAS OUBLIER!!! -->

10.      <dependency>

11.      <groupId>com.google.android</groupId>

12.      <artifactId>support-v4</artifactId>

13.      <version>r6</version>

14.      </dependency>

15.</dependencies>

Les lignes 10-14 ajoutent la dépendance manquante. Exécutez de nouveau le projet. Il doit maintenant fonctionner. On remarque cependant qu'avec certaines configurations d'Eclipse, on a de nouveau une erreur :

1.

[2013-12-08 09:57:45 - Dex Loader] Unable to execute dex: Multiple dex files define Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat$AccessibilityServic eInfoVersionImpl;

2.

[2013-12-08 09:57:45 - exemple-07] Conversion to Dalvik format failed: Unable to execute dex: Multiple dex files define

Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat$AccessibilityServic eInfoVersionImpl;

Le message d'erreur est plus clair. Il indique qu'il y a plusieurs binaires (dex) Android pour [android/support/v4]. Dans ce cas, il faut ajouter la portée [provided] à la dépendance Maven [android/support/v4] ligne 5 ci-dessous :

1.

<dependency>

2.

<groupId>com.google.android</groupId>

3.

<artifactId>support-v4</artifactId>

4.

<version>r6</version>

5.

        <scope>provided</scope>

6.

</dependency>

Les différences de comportement des projets Android-Maven d'Eclipse, souvent pour une même version mais sur des machines différentes, ont été un constant challenge dans l'écriture de ce document. Il y a probablement un point de configuration des projets Maven / Android qui m'a échappé.

 9 Exemple-08 : architecture client / serveur

Nous abordons une architecture courante pour une application Android, celle où l'application Android communique avec des services web distants. On aura maintenant l'architecture suivante :

On a ajouté à l'application Android une couche [DAO] pour communiquer avec le serveur distant. Elle communiquera avec le serveur qui génère les nombres aléatoires affichés par la tablette Android. Ce serveur aura une architecture à deux couches suivante :

Spring / Tomcat

La couche [web] aura un fonctionnement de type REST (REpresentational State Transfer). Ce type de service web attend des requêtes HTTP du type : [commande] URL avec [commande] dans [GET, POST, PUT, DELETE]. Au lieu de renvoyer une page HTML sur les commandes [GET, POST] comme il est habituel, il envoie une information sous forme XML ou JSON (JavaScript Object Notation). Ici notre service web traitera une unique URL de type [/random/a/b/n] qui renverra une liste de n nombres aléatoires dans l'intervalle [a,b]. Cette liste sera renvoyée sous forme d'une unique chaîne JSON.

Nous allons décrire l'application dans l'ordre suivant :

Le serveur

•   sa couche [métier] ;

•   son service REST implémenté avec Spring MVC ;

Le client

•   sa couche [DAO] ;

•   sa couche [métier] ;

 9.1

Spring MVC

Spring / Tomcat

 9.1.1      Le projet Eclipse

Nous allons créer un projet web de type [Spring MVC]. Prenez l'option [File / New / Other]

•   en [1], on choisit un projet de type [Spring Project] ;

•   en [2], on choisit un projet de type [Spring MVC Project] ;

•   en [3], on le nomme [exemple-08-server] ;

•   en [4], on lui associe un dossier inexistant ou vide.

•   en [5], on donne le nom du paquetage de plus haut niveau de notre application. Ici on indique que les composants Spring seront trouvés dans le paquetage [.aleas] ;

Le projet Eclipse ainsi créé est le suivant [6] :

Le projet créé est un projet web qu'on peut immédiatement exécuter : [clic droit sur le projet / Run as / Run on server / choisir vFabric tc Server]. On obtient alors la page suivante :

On notera en [8] que l'assistant a utilisé comme nom de notre application le dernier terme du nom du package donné à l'étape [5]. Nous verrons un peu plus loin que la raison véritable est à chercher dans le fichier [].

 9.1.2      Anatomie d'un projet Spring MVC

Le projet [exemple-08-server] affiche beaucoup de branches. Nous allons nous intéresser qu'à une seule, la branche [src] [1]. Les autres branches sont des images de cette branche présentées sous un angle différent.

Spring MVC est comme son nom l'indique un framework MVC (Modèle – Vue – Contrôleur). Cela est reflété dans l'arborescence ci-dessus :

•      les vues sont des pages JSP (Java Server Pages) placées dans le dossier [views] ;

•      les contrôleurs sont des classes Java placées dans le package [.aleas] [3] ;

L'application étant une application web, elle est contrôlée comme toute application web Java par le fichier [WEB-INF / ]. Celui-ci est le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<web-app version="2.5" xmlns=";

3.

xmlns:xsi=";

4.

xsi:schemaLocation=" ">

5. 6.

<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->

7.

<context-param>

8.

<param-name>contextConfigLocation</param-name>

9.

<param-value></param-value>

10.

</context-param>

11. 12.

<!-- Creates the Spring Container shared by all Servlets and Filters -->

13.

<listener>

14.

<listener-class>.context.ContextLoaderListener</listener-class>

15.

</listener>

16.

17.      <!-- Processes application requests -->

18.      <servlet>

19.      <servlet-name>appServlet</servlet-name>

20.      <servlet-class>.servlet.DispatcherServlet</servlet-class>

21.      <init-param>

22.      <param-name>contextConfigLocation</param-name>

23.      <param-value></param-value>

24.      </init-param>

25.      <load-on-startup>1</load-on-startup>

26.      </servlet>

27.

28.   <servlet-mapping>

29.   <servlet-name>appServlet</servlet-name>

30.   <url-pattern>/</url-pattern> 31.  </servlet-mapping> 32.

33.</web-app>

•      lignes 18-26 : définissent la classe qui va traiter toutes les demandes faites à l'application. C'est le contrôleur principal, le C du MVC. Ce contrôleur principal route les demandes vers des contrôleurs secondaires appelés souvent simplement contrôleurs. Ce sont ces derniers qui traitent réellement les demandes. Nous en avons vu un précédemment : la classe [HomeController] ;

•      les classes qui traitent les demandes sont appelées des [servlets]. Une aplication peut définir plusieurs servlets. Celle-ci n'en définit qu'une aux lignes 18-26 ;

•      ligne 19 : le nom donné à la servlet – peut être quelconque ;

•      ligne 20 : la classe de la servlet. C'est ici une classe du framework Spring MVC. C'est le contrôleur principal, le C du MVC ;

•      lignes 21-24 : une servlet peut accepter des paramètres de configuration. C'est ici qu'on les met ;

•      lignes 22-23 : indiquent à la servlet [DispatcherServlet] où elle va trouver son fichier de configuration ;

•      ligne 25 : indique que la servlet doit être chargée dès que le serveur démarre. En l'absence de cette balise, la servlet n'est chargée que lorsque la première demande pour elle arrive ;

•      lignes 28-31 : cette balise associe des URL à une servlet. La ligne 30 indique que toute URL doit être traitée par la servlet indiquée ligne 29, donc par la servlet définie aux lignes 18-26 ;

La ligne 23 ci-dessus indique que le contrôleur principal est configuré par le fichier []. Celui-ci est le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<beans xmlns=";

3.

xmlns:xsi=";

4.

xsi:schemaLocation=" ">

5. 6.

<!-- Root Context: defines shared resources visible to all other web components -->

7. 8.

</beans>

Il est vide.

Le fichier [] a défini une unique servlet appelée [appServlet]. Par défaut, elle est configurée également par le fichier [appServlet / ]. Celui-ci est ici le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<beans:beans xmlns=";

3.

xmlns:xsi=";

4.

xmlns:beans=";

5.

xmlns:context=";

6.

xsi:schemaLocation="

7.

8.

">

9.

10.   <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure

--> 11.

12.   <!-- Enables the Spring MVC @Controller programming model -->

13.   <annotation-driven /> 14.

15. <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->

16. <resources mapping="/resources/**" location="/resources/" /> 17.

18.   <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEBINF/views directory -->

19.   <beans:bean class=".InternalResourceViewResolver">

20.   <beans:property name="prefix" value="/WEB-INF/views/" />

21.   <beans:property name="suffix" value=".jsp" />

22.   </beans:bean> 23.

24.    <context:component-scan base-package=".aleas" /> 25.</beans:beans>

•      la ligne 13 indique que l'application est configurée par des annotations placées dans le code Java des classes ;

•      la ligne 16 indique que l'URL [/resources/**] sera associée au dossier [resources] de l'application. On pourra mettre des images, scripts Javascript, feuilles de styles dans ce dossier ;

•      lignes 19-22 : indique qu'une vue nommée [V] sera implémentée par le fichier [WEB-INF / views / V.jsp] ;

•      ligne 24 : on demande à spring de scanner le dossier [.aleas] pour y trouver les composants Spring de l'application. Nous en aurons de deux types :

•      @Controller : qui désigne une classe Java capable de traiter certaines demandes des clients. Nous l'utiliserons pour l'unique cotrôleur de cette application ;

•      @Service : qui désigne un composant Spring à instancier une fois (singleton). Nous l'utiliserons pour l'implémentation de la couche [métier] ;

Nous serons amenés à compléter ce fichier de configuration. Il a été généré par défaut pour des contrôleurs qui affichent des pages HTML alors que nous voulons rendre des chaînes JSON. Ainsi les lignes 15-22 nous seront inutiles.

L'unique contrôleur est la classe [HomeController] suivante :

1. package .aleas; 2.

3. import .DateFormat; 4.

5.

6.    @Controller

7.    public class HomeController { 8.

9.    // la méthode [home] traite la demande GET /

10.   @RequestMapping(value = "/", method = RequestMethod.GET)

11.   public String home(Locale locale, Model model) {

12.   // locale : locale de l'application - injectée automatiquement par Spring

13.   // model : le modèle pour la vue qui sera retournée par la méthode 14.

15.   // la date du jour

16.   Date date = new Date();

17.   DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

18.   String formattedDate = dateFormat.format(date);

19.   // cette date est mise dans le modèle associée à la clé [serverTime]

20.   model.addAttribute("serverTime", formattedDate );

21.   // on demande à la vue [] de s'afficher

22.   return "home";

23.   }

24.

25.}

•      ligne 6 : l'annotation [@Controller] fait de la classe [HomeController] une classe capable de traiter les requêtes des clients de l'application web. La classe est un contrôleur C, le C du MVC ;

•      la ligne 10 indique quelle requête la méthode [home] peut traiter. Elle traite la requête [GET /] ; • ligne 11 : la méthode reçoit deux paramètres :

•      le premier est de type [Locale]. Spring reconnaît ce type et injecte automatiquement la locale du serveur, ici [fr_FR] pour indiquer le français (fr) de France (FR) ;

•      le second est de type [Model].  Spring reconnaît ce type et injecte automatiquement une instance

[Model] vide. Elle formera le modèle M de la vue V affichée par la méthode, le M et le V du MVC ;

•      lignes 16-18 : une date / heure est calculée ;

•      ligne 20 : le type [Model] s'utilise comme un dictionnaire. On y met des valeurs associées à des clés ;

•      ligne 22 : la méthode doit rendre le nom de la vue qui doit s'afficher, ici celle qui s'appelle [home]. Grâce au fichier de configuration présenté, c'est la page [WEB-INF / views / ] qui va être affichée. Elle utilisera comme modèle l'objet [Model] utilisé par la méthode ;

La page JSP [] est la suivante :

1.    <%@ taglib uri="; prefix="c" %>

2.    <%@ page session="false" %>

3.    <html>

4.    <head>

5.    <title>Home</title>

6.    </head>

7.    <body>

8.    <h1>

9.    Hello world!  10.</h1> 11.

12.<P>  The time on the server is ${serverTime}. </P>

13.</body>

14.</html>

•      la ligne 1 est souvent nécessaire mais pas ici. Elle définit ce qu'on appelle une bibliothèque de balises qu'on peut alors utiliser dans la page JSP ;

•      la ligne 12 utilise la variable [${serverTime}]. Sa valeur est cherchée dans le modèle de la page. Celui-ci est le modèle construit par la méthode [home] du contrôleur [HomeController]. Cette méthode avait créé une valeur associée à la clé [serverTime]. C'est cette valeur qui est ici récupérée.

Voilà ! On a fait le tour des éléments d'une application Spring MVC. Celle générée n'est pas tout à fait adaptée pour faire un serveur REST. Nous serons amenés à modifier sa configuration.

L'application générée est une application Maven dont le fichier [] est le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<project xmlns="; xmlns:xsi=";

3.

xsi:schemaLocation=" ">

4.

<modelVersion>4.0.0</modelVersion>

5.

<groupId></groupId>

6.

<artifactId>aleas</artifactId>

7.

<name>exemple-08-server</name>

8.

<packaging>war</packaging>

9.

<version>1.0.0-BUILD-SNAPSHOT</version>

10.

<properties>

11.

<java-version>1.6</java-version>

12.

<org.springframework-version>3.1.1.RELEASE</org.springframework-version>

13.

<org.aspectj-version>1.6.10</org.aspectj-version>

14.

<org.slf4j-version>1.6.6</org.slf4j-version>

15.

</properties>

16.

<dependencies>

17.

<!-- Spring -->

18.

<dependency>

19.

<groupId>org.springframework</groupId>

20.

<artifactId>spring-context</artifactId>

21.

<version>${org.springframework-version}</version>

22.

<exclusions>

23.

<!-- Exclude Commons Logging in favor of SLF4j -->

24.

<exclusion>

25.

<groupId>commons-logging</groupId>

26.

<artifactId>commons-logging</artifactId>

27.

 </exclusion>

28.

</exclusions>

29.

</dependency>

30.

<dependency>

31.

<groupId>org.springframework</groupId>

32.

<artifactId>spring-webmvc</artifactId>

33.

<version>${org.springframework-version}</version>

34.

</dependency>

35. 36.

<!-- AspectJ -->

37.

<dependency>

38.

<groupId>org.aspectj</groupId>

39.

<artifactId>aspectjrt</artifactId>

40.

<version>${org.aspectj-version}</version>

41.

</dependency>

42. 43.

<!-- Logging -->

44.

<dependency>

45.

<groupId>org.slf4j</groupId>

46.

<artifactId>slf4j-api</artifactId>

47.

<version>${org.slf4j-version}</version>

48.

</dependency>

49.

<dependency>

50.

<groupId>org.slf4j</groupId>

51.

<artifactId>jcl-over-slf4j</artifactId>

52.

<version>${org.slf4j-version}</version>

53.

<scope>runtime</scope>

54.

</dependency>

55.

<dependency>

56.

<groupId>org.slf4j</groupId>

57.

<artifactId>slf4j-log4j12</artifactId>

58.

<version>${org.slf4j-version}</version>

59.

<scope>runtime</scope>

60.

</dependency>

61.

<dependency>

62.

<groupId>log4j</groupId>

63.

<artifactId>log4j</artifactId>

64.

<version>1.2.15</version>

65.

<exclusions>

66.

<exclusion>

67.

<groupId></groupId>

68.

<artifactId>mail</artifactId>

69.

</exclusion>

70.

<exclusion>

71.

<groupId></groupId>

72.

<artifactId>jms</artifactId>

73.

</exclusion>

74.

<exclusion>

75.

<groupId></groupId>

76.

<artifactId>jmxtools</artifactId>

77.

</exclusion>

78.

<exclusion>

79.

<groupId></groupId>

80.

<artifactId>jmxri</artifactId>

81.

</exclusion>

82.      </exclusions>

83.      <scope>runtime</scope>

84.      </dependency>

85.

86.      <!-- @Inject -->

87.      <dependency>

88.      <groupId>javax.inject</groupId>

89.      <artifactId>javax.inject</artifactId>

90.      <version>1</version>

91.      </dependency>

92.

93.       <!-- Servlet -->

94.       <dependency>

95.       <groupId>javax.servlet</groupId>

96.       <artifactId>servlet-api</artifactId>

97.       <version>2.5</version>

98.       <scope>provided</scope>

99.       </dependency>

100.      <dependency>

101.      <groupId></groupId>

102.      <artifactId>jsp-api</artifactId>

103.      <version>2.1</version>

104.      <scope>provided</scope>

105.      </dependency>

106.      <dependency>

107.      <groupId>javax.servlet</groupId>

108.      <artifactId>jstl</artifactId>

109.      <version>1.2</version>

110.      </dependency>

111.

112.    <!-- Test -->

113.    <dependency>

114.    <groupId>junit</groupId>

115.    <artifactId>junit</artifactId>

116.    <version>4.7</version>

117.    <scope>test</scope>

118.    </dependency>       

119.    </dependencies>

120.    <build>

121.    <plugins>

122.    <plugin>

123.    <artifactId>maven-eclipse-plugin</artifactId>

124.    <version>2.9</version>

125.    <configuration>

126.    <additionalProjectnatures> 127.                       

<projectnature>.springnature</projectnature>

128.                    </additionalProjectnatures> 129. <additionalBuildcommands> 130.

<buildcommand>.springbuilder</buildcommand>

131.          </additionalBuildcommands>

132.          <downloadSources>true</downloadSources>

133.          <downloadJavadocs>true</downloadJavadocs>

134.          </configuration>

135.          </plugin>

136.          <plugin>

137.          <groupId>org.apache.maven.plugins</groupId>

138.          <artifactId>maven-compiler-plugin</artifactId>

139.          <version>2.5.1</version>

140.          <configuration>

141.          <source>1.6</source>

142.          <target>1.6</target>

143.

                    <compilerArgument>-Xlint:all</compilerArgument>

144.

                    <showWarnings>true</showWarnings>

145.

                    <showDeprecation>true</showDeprecation>

146.

                </configuration>

147.

            </plugin>

148.

            <plugin>

149.

                <groupId></groupId>

150.

                <artifactId>exec-maven-plugin</artifactId>

151.

                <version>1.2.1</version>

152.

                <configuration>

153.

                    <mainClass></mainClass>

154.

                </configuration>

155.

            </plugin>

156.

        </plugins>

157.

    </build>

158.

</project>

La plupart des dépendances sont ici inutiles. L'application générée a été configurée pour une application Spring MVC complète.

Modifiez les lignes 5 et 6 ci-dessus de la façon suivante :

<groupId>exemples</groupId>

<artifactId>exemple-08-server</artifactId>

Ceci fait, réexécutez le projet. La page web affichée est désormais la suivante :

En [1], l'URL a changé. C'est donc l'[artifactId] du projet Maven qui est utilisé pour donner son nom à l'application web. Parfois l'URL ne change pas. Vous pouvez alors procéder ainsi :

•      supprimez le projet [clic droit sur projet / Delete (sans suppression de dossier)] ;

•      réimportez-le ;

•      vérifiez l'encodage UTF-8 du projet ;

•      réexécutez le projet ;

 9.2

Le serveur REST

Rappelons que nous voulons construire l'architecture suivante :

Spring / Tomcat

Nous créons un nouveau projet [exemple-08-server-rest] par duplication du projet [exemple-08-server].

Modifiez les caractéristiques du projet dans [] de la façon suivante :

<groupId>exemples</groupId>

<artifactId>exemple-08-server-rest</artifactId>

<name>exemple-08-server-rest</name>

<packaging>war</packaging>

<version>1.0.0-BUILD-SNAPSHOT</version>

Nous modifions le nouveau projet de la façon suivante :

• en [1], nous créons deux nouveaux packages [.aleas.metier] pour y loger la couche [métier] et [] pour y loger la couche [web] ;

 9.2.1      La couche [métier]

Spring / Tomcat

La couche [métier] aura l'interface [IMetier] suivante :

1. package .aleas.metier; 2.

3. import ; 4.

5. public interface IMetier { 6.

7.   public List<Object> getAleas(int a, int b, int n);

8.   }

        •      ligne 7 : la méthode qui génère n nombres aléatoires entre [a,b]

Le code de la classe [Metier] implémentant cette interface est le suivant :

1. package .aleas.metier; 2.

3.    import .ArrayList;

4.    import ; 5. import .Random; 6.

7. import org.springframework.stereotype.Service; 8.

9. @Service

10.public class Metier implements IMetier { 11.

12.       @Override

13.       public List<Object> getAleas(int a, int b, int n) {

14.       // la liste des objets

15.       List<Object> réponses = new ArrayList<Object>();

16.       // qqs vérifications

17.       if (n < 1) {

18.       ré(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));

19.       }

20.       if (a < 0) {

21.       ré(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));

22.       }

23.       if (b < 0) {

24.       ré(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));

25.       }

26.       if (a >= b) {

27.       ré(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));

28.       }

29.       // erreur ?

30.       if (ré() != 0) {

31.       return réponses;

32.       }

33.       // on génère les nombres aléatoires

34.       Random random = new Random();

35.       for (int i = 0; i < n; i++) {

36.       // on génère une exception aléatoire 1 fois / 3

37.       int nombre = random.nextInt(3);

38.       if (nombre == 0) {

39.       ré(new AleaException("Exception aléatoire"));

40.       } else {

41.       // sinon on rend un nombre aléatoire entre deux bornes [a,b]

42.       ré(Integer.valueOf(a + random.nextInt(b - a + 1)));

43.       }

44.       }

45.       // résultat

46.       return réponses;

47.       }

48.}

Nous ne commentons pas la classe : nous l'avons déjà rencontrée dans [exemple-07]. On notera simplement ligne 9 l'annotation Spring [@Service] qui va faire que Spring va instancier la classe en un unique exemplaire et rendre sa référence disponible pour d'autres composants Spring, notamment le contrôleur [home].

La classe [Metier] crée des exceptions de type [AleaException] :

1. package .server.metier; 2.

3. public class AleaException extends RuntimeException { 4.

5.     private static final long serialVersionUID = 1L; 6.

7.   public AleaException() {

8.   }

9.

10.   public AleaException(String detailMessage) {

11.   super(detailMessage);

12.   }

13.

14.   public AleaException(Throwable throwable) {

15.   super(throwable);

16.   }

17.

18.   public AleaException(String detailMessage, Throwable throwable) {

19.   super(detailMessage, throwable);

20.   }

21.

22.}

 9.2.2      Le service REST

Spring / Tomcat

Le service REST est implémenté par Spring MVC. 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. 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.

Nous ajoutons le contrôleur [AleaController] suivant :

1.

package ;

2. 3.

import .aleas.metier.AleaException;

4.

5.

6.    @Controller

7.    public class AleaController { 8.

9.    // couche métier

10.   @Inject

11.   private IMetier metier; 12.

13.  // nombre aléatoire

14.  @RequestMapping(value = "/{a}/{b}/{n}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")

15.  @ResponseBody

16.  public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b,

@PathVariable("n") int n) throws JsonGenerationException,

17.  JsonMappingException, IOException { 18.

19.       // on utilise la couche métier

20.       List<Object> aleas = null;

21.       aleas = metier.getAleas(a, b, n);

22.       // on gère les données reçues

23.       for (int i = 0; i < (); i++) {

24.       Object o = (i);

25.       if (o instanceof AleaException) {

26.       // on ne retient de l'exception que son message

27.       String msg = ((AleaException) o).getMessage();

28.       // on enlève l'élément n° i actuel

29.       aleas.remove(i);

30.       // on le remplace par la nouvelle valeur

31.       (i, msg);

32.       }

33.       }

34.       // on rend la liste sérialisée en Json

35.       return new ObjectMapper().writeValueAsString(aleas);

36.       }

37.

38.}

•   ligne 16 : la méthode qui génère les nombres aléatoires. Son nom n'a pas d'importance. Lorsqu'elle s'exécute, les champs de la ligne 11 ont été initialisés par Spring MVC. Nous verrons comment. Par ailleurs, si elle s'exécute, c'est parce que le serveur web a reçu une requête HTTP GET pour l'URL de la ligne 14 ;

•   ligne 14 : l'URL traitée est de la forme /{a}/{b}/{n} où {x} représente une variable. Les variables {a}, {b} et {n} sont affectées aux paramètres de la méthode ligne 16. Cela se fait via l'annotation @PathVariable(" x "). On notera que {a}, {b} et {n} sont des composantes d'une URL et sont donc de type String. La conversion de String vers le type des paramètres peut échouer. Spring MVC lance alors une exception. Résumons : si avec un navigateur je demande l'URL /

100/200/5, la méthode getAleas de la ligne 16 s'exécutera avec les paramètres entiers a=100, b=200 et n=5 ;

•   ligne 14 : le type de la réponse est précisée par l'attribut [produces]. Cela fixe la valeur de l'entête HTTP [Content-type].

Ici, l'entête HTTP

Content-Type:application/json;charset=UTF-8

va être envoyé au client. Le client peut ou non utiliser cette information

•   ligne 21 : on demande à la couche [métier] n nombres aléatoires dans l'intervalle [a,b]. On se souvient que la méthode [metier].getAleas rend une liste d'objets où l'objet est de type [AleaException] ou [Integer] ;

•   lignes 23-33 : une boucle pour remplacer dans la liste reçue chaque exception par le message qu'elle contient ;

•   ligne 15 : l'annotation [@ResponseBody] indique que la méthode va générer la réponse au client. On ne passe pas par l'intermédiaire d'une vue ;

•   ligne 16 : la réponse est de type [String] ;

•   ligne 35 : on veut renvoyer la chaîne JSON de la liste [aleas]. Pour cela, on utilise la bibliothèque JSON [Jackson] :

•   new ObjectMapper() : rend un objet capable de sérialiser un objet en JSON et de désérialiser une chaîne JSON en objet,

•   [new ObjectMapper().WriteValueAsString(Object o)] : crée la chaîne JSON de l'objet o. Cette méthode peut lancer les exceptions indiquées lignes 16 et 17 ;

•   la chaîne JSON correspondant à l'objet [aleas] aura la forme d'une liste contenant des nombres et des messages d'erreur :

["Exception aléatoire","Exception aléatoire",171,167,145]

C'est donc cette chaîne que recevra le client.

Le fichier de configuration [servlet-context] reste ce qu'il était :

Son code est désormais le suivant :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<beans:beans ">

3. 4.

5. 6.

<context:component-scan base-package=".aleas" />

7. 8.

</beans:beans>

Jusqu'à la ligne 6 incluse, on a le contenu précédent.

•   ligne 6 : il est possible de configurer Spring à l'aide d'annotations dans le code Java. La ligne 6 dit à Spring d'exploiter les annotations qu'il trouvera dans le package [.aleas] ;

Il y trouvera diverses annotations, par exemple dans la classe [AleaController] :

1.    @Controller

2.    public class AleaController { 3.

4.    // couche métier

5.    @Inject

6.    private IMetier metier; 7.

8.   // nombre aléatoire

9.   @RequestMapping(value = "/{a}/{b}/{n}", method = RequestMethod.GET)

10.  @ResponseBody

11.  public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b,

@PathVariable("n") int n) throws JsonGenerationException,

12.JsonMappingException, IOException {

Une autre annotation que Spring trouvera est l'annotation [@Service] de la classe [Metier] :

@Service

public class Metier implements IMetier {

•   ligne 1 : l'annotation @Controller fait de la classe [AleaController] un contrôleur Spring MVC, c-à-d une classe capable de traiter des requêtes HTTP. Celles-ci sont définies par l'annotation @RequestMapping qu'on voit en ligne 9 ;

•   lignes 5 : l'annotation @Inject injecte des références sur des beans définis dans le fichier de configuration de Spring MVC ou bien par des annotations Java ;

L'annotation [@Inject] nécessite une nouvelle dépendance Maven dans [] :

<!-- @Inject -->

<dependency>

<groupId>javax.inject</groupId>

<artifactId>javax.inject</artifactId>

<version>1</version> </dependency>

•   ligne 5 : Spring recherche un composant Spring implémentant l'interface [IMetier]. Un composant Spring est un composant défini dans un fichier de configuration de Spring ou bien par des annotations Java. A cause de son annotation [@Service], la classe [Metier] fait partie des composants gérés par Spring. Comme elle implémente l'interface [IMetier] sa référence sera injectée dans le champ [metier] ligne 5 ;

Le fichier [] qui configure l'application est le suivant :

1.   <?xml version="1.0" encoding="UTF-8"?>

2.   <project xmlns="; xmlns:xsi=";

3.   xsi:schemaLocation=" ">

4.   <modelVersion>4.0.0</modelVersion>

5.   <groupId>exemples</groupId>

6.   <artifactId>exemple-08-server-rest</artifactId>

7.   <name>exemple-08-server-rest</name>

8.   <packaging>war</packaging>

9.   <version>1.0.0-BUILD-SNAPSHOT</version> 10.

11.   <properties>

12.   <java-version>1.6</java-version>

13.   <org.springframework-version>3.1.1.RELEASE</org.springframework-version>

14.   <jackson.mapper.version>1.5.6</jackson.mapper.version>

15.   </properties> 16.

17.      <dependencies>

18.      <!-- Spring -->

19.      <dependency>

20.      <groupId>org.springframework</groupId>

21.      <artifactId>spring-context</artifactId>

22.      <version>${org.springframework-version}</version>

23.      </dependency>

24.      <dependency>

25.      <groupId>org.springframework</groupId>

26.      <artifactId>spring-webmvc</artifactId>

27.      <version>${org.springframework-version}</version>

28.      </dependency>

29.      <!-- Jackson -->

30.      <dependency>

31.      <groupId>org.codehaus.jackson</groupId>

32.      <artifactId>jackson-mapper-asl</artifactId>

33.      <version>${jackson.mapper.version}</version>

34.      </dependency>

35.      <!-- @Inject -->

36.      <dependency>

37.      <groupId>javax.inject</groupId>

38.      <artifactId>javax.inject</artifactId>

39.      <version>1</version>

40.      </dependency>

41.

42.   </dependencies>

43.   <build> 44 .

45.   </build>

46.</project>

•   lignes 19-28 : les dépendances sur Spring ;

•   lignes 30-34 : la dépendance sur la librairie JSON Jackson ;

•   lignes 36-40 : la dépendance de l'annotation [@Inject]

 9.2.3      Exécution du serveur REST

Ci-dessus, on exécute le projet [exemple-08-server-rest]. Voici une copie d'écran qu'on peut obtenir :

Selon le navigateur que vous utilisez, vous pouvez avoir une erreur analogue à la suivante :

Cela signifie que le navigateur ne reconnaît pas l'entête HTTP envoyé par le serveur :

Content-Type:application/json;charset=UTF-8

Cette ligne vient du contrôleur [AleaController] :

@RequestMapping(value = "/{a}/{b}/{n}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")

C'est l'attribut [produces] qui a généré l'entête HTTP. Nous pouvons également écrire :

@RequestMapping(value = "/{a}/{b}/{n}", method = RequestMethod.GET, produces =

"text/plain;charset=UTF-8")

pour indiquer que nous envoyons du texte. Cela fait une différence. Un client recevant un entête HTTP [application/json] peut essayer de sérialiser la chaîne JSON en un objet alors que s'il reçoit l'entête [text/plain] il n'essaiera pas d'interpréter la chaîne reçue.

Le navigateur intégré d'Eclipse peut dans certaines configurations ne pas reconnaître l'entête [application/json]. Essayez alors un autre navigateur ou mettez [text/plain] dans l'attribut [produces]. Cela ne gênera pas le client que nous allons écrire.

 9.3

Le client Android du serveur REST

Le client Android aura l'architecture suivante :

Le client a trois composantes :

1.     la couche [Android] que nous avons étudiée dans l'exemple [exemple-07] ;

2.     la couche [métier] qui va présenter une interface analogue à celle de la couche [métier] du serveur ;

3.     la couche [DAO] qui s'adresse au service REST que nous avons étudié précédemment.

 9.3.1      Le projet Android

Le projet Android s'appellera [exemple-08-client-rest] et est obtenu par recopie du projet [exemple-07]. En effet, on veut réutiliser la couche [Android] de ce projet.

Modifiez les caractéristiques du nouveau projet dans [] de la façon suivante :

<groupId>exemples</groupId>

<artifactId>exemple-08-client-rest</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>apk</packaging>

<name>exemple-08-client-rest</name>

 9.3.2      Le manifeste de l'application Android

Le fichier [] du projet est un peu différent de celui du projet précédent :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <manifest xmlns:android=";

3.    package=".android"

4.    android:versionCode="1" 5.    android:versionName="1.0" > 6.

7.    <uses-sdk

8.    android:minSdkVersion="11" 9.        android:targetSdkVersion="16" /> 10.

11.    <uses-permission android:name="android.permission.INTERNET" /> 12.

13.   <application

14.   android:allowBackup="true"

15.   android:icon="@drawable/ic_launcher"

16.   android:label="@string/app_name"

17.   android:theme="@style/AppTheme" >

18.   <activity

19.   android:name=".android.activity.MainActivity"

20.   android:label="@string/app_name"

21.   android:windowSoftInputMode="stateHidden" >

22.   <intent-filter>

23.   <action android:name="" /> 24.

25.   <category android:name="android.intent.category.LAUNCHER" />

26.   </intent-filter>

27.   </activity> 28.    </application> 29.

30.</manifest>

La ligne importante est la ligne 11. C'est elle qui permet au client Android d'ouvrir des connexions réseau. Si on l'oublie, ça ne marche pas et les messages d'erreur ne sont pas toujours explicites quant à la cause de l'erreur.

 9.3.3      La couche [metier]

Rappelons l'interface de la couche [métier] du serveur :

1.

package .aleas.metier;

2. 3.

import ;

4. 5.

public interface IMetier {

6. 7.

public List<Object> getAleas(int a, int b, int n);

8.

}

Celle du client sera analogue :

1. package .android.metier; 2.

3. import ; 4.

5. import ; 6.

7. public interface IMetier { 8.

9.     public List<Object> getAleas(int a, int b, int n); 10.

11.    public void setDao(IDao dao); 12.

13.    public void setUrlServiceRest(String url); 14.}

•   ligne 9 : la méthode de génération des nombres aléatoires ;

•   ligne 11 : la couche [métier] a besoin de la couche [DAO]. C'est cette dernière qui assure les échanges avec le service REST ;

•   ligne 13 : la couche [métier] a besoin de connaître l'adresse du service REST. Nous verrons pourquoi.

L'implémentation [Metier] du client est la suivante :

1.

package .android.metier;

2. 3.

import ;

4.

5. 6.

public class Metier implements IMetier {

7. 8.

// couche [dao]

9.

private IDao dao;

10.

// service REST

11.

private String urlServiceRest;

12. 13.

// setters

14. 15.

public void setDao(IDao dao) {

16.

this.dao = dao;

17.

}

18. 19.

public List<Object> getAleas(int a, int b, int n) {

20.

// adresse du service REST

21.

String urlService = String.format("http://%s/{a}/{b}/{n}", urlServiceRest);

22.

// paramètres service REST

23.

Map<String, String> paramètres = new HashMap<String, String>();

24.

paramè("a", String.valueOf(a));

25.

paramè("b", String.valueOf(b));

26.

paramè("n", String.valueOf(n));

27.

// exécution service [DAO]

28.

Exception exception = null;

29.

String réponse = null;

30.

try {

31.

// exécution service - on récupère un [String]

32.

réponse = dao.executeRestService("get", urlService, null,

33.

paramètres);

34.

} catch (Exception ex) {

35.

// cas d'erreur - on note l'exception

36.

exception = ex;

37.

}

38.

// si exception

39.

if (exception != null) {

40.

List<Object> messages = new ArrayList<Object>();

41.

Throwable th = exception;

42.

while (th != null) {

43.

(th.getMessage());

44.

th = th.getCause();

45.

}

46.

return messages;

47.

}

48. 49.

// pas d'exception - on exploite la réponse JSON

50.

List<String> strings = new Gson().fromJson(réponse,

51.

new TypeToken<List<String>>() {

52.

}.getType());

53.

List<Object> objets = new ArrayList<Object>();

54.

for (String string : strings) {

55.

try {

56.

(Integer.valueOf(string));

57.

} catch (NumberFormatException ex) {

58.

(string);

59.

}

60.

}

61.

return objets;

62.

}

63. 64.

public void setUrlServiceRest(String url) {

65.

urlServiceRest = url;

66.

67.

68.}

}

•   ligne 9 : une référence sur la couche [DAO]. Elle sera initialisée lorsque l'activité Android instanciera la couche [métier] ;

•   ligne 19 : la méthode de génération des nombres aléatoires ;

•   ligne 21 : on construit l'URL complète du service REST demandé. On notera bien la syntaxe des variables a, b et n dans l'URL ;

•   lignes 23-26 : un dictionnaire dont les clés sont les variables de l'URL demandée ;

•   ligne 32 : on exécute la méthode [dao].executeRestService de la couche [DAO] avec les paramètres suivants :

1.     la méthode " get " ou " post " de la requête HTTP à émettre,

2.     l'URL complète du service REST à exécuter,

3.     un dictionnaire des données transmises par une opération HTTP POST. Donc null ici, puisqu'on fait  une opération HTTP GET,

4.     sous la forme d'un dictionnaire, les valeurs des variables de l'URL, ici les variables {a}, {b} et {n} ;

•   lignes 39-47 : si une exception se produit lors de l'appel du service REST, on met les messages d'erreur de cette exception et de ses causes dans une liste et on rend celle-ci à l'appelant qui pour l'instant sera la vue [Vue_01]. Celle-ci affichant ce qu'elle reçoit dans un [ListView], nous pourrons voir les messages d'erreur des exceptions qui se sont produites ;

•   lignes 49-51 : on a reçu une chaîne du serveur de la forme suivante :

[179,"Exception aléatoire",130,174,"Exception aléatoire"]

Avec une bibliothèque JSON, nous allons créer à partir de cette chaîne un objet List<String>. Nous utilisons ici une bibliothèque JSON de Google appelée Gson. C'est également la bibliothèque qu'utilise le framework [spring-android-resttemplate] que nous allons utiliser pour communiquer avec le serveur REST. Cela nous amènera à ajouter la dépendance suivante dans le fichier [] :

<!-- Gson JSON Processor -->

<dependency>

<groupId></groupId>

<artifactId>gson</artifactId>

<version>${-version}</version> </dependency>

La bibliothèque JSON n'est pas indispensable ici pour récupérer les chaînes de caractères. Nous l'utilisons car elle s'avère souvent nécessaire pour traiter les réponses JSON des serveurs REST ;

•   ligne 49 : new Gson() crée l'objet Gson qui nous permet de sérialiser un objet [Gson].toJson(Object o)] et de désérialiser une chaîne JSON dans un objet [Gson].fromJson(String json, Class typeObjet) ;

•   ligne 52 : on instancie la liste d'objets qu'on doit rendre au module appelant ;

•   lignes 53-59 : la liste de [String] est un ensemble de nombres entiers aléatoires et de messages d'erreur. On la parcourt pour créer une liste d'objets de type [Integer] ou [String] ; • ligne 60 : cette liste est rendue au module appelant ;

 9.3.4      La couche [DAO]

L'interface [IDao] de la couche [DAO] est la suivante :

1. package ; 2.

3. import ; 4.

5.    public interface IDao {

6.    public String executeRestService(String method, String urlService, Object request, Map<String, String> paramètres); 7.

8.   public void setTimeout(int millis);

9.   }

•      ligne 6 : la méthode [executeRestService] dont nous avons parlé précédemment ;

•      ligne 8 : une méthode pour fixer un temps de réponse maximal de la part du serveur REST. Passé ce délai, la couche [DAO] lance une exception [AleaException]. Ce temp est fixé en millisecondes.

L'implémentation est la suivante :

1. package ; 2.

3. import .android.activity.AleaException; 4.

5.

6. public class Dao implements IDao { 7.

8.    // client REST

9.    private RestTemplate restTemplate;

10.   // délai d'attente maximal 11.    private int timeout; 12.

13.   // constructeur

14.   public Dao() {

15.   // on crée un objet [RestTemplate]

16.   restTemplate = new RestTemplate();

17.   // on le configure - il doit être capable de gérer la chaîne qu'il va

18.   // recevoir

19.   restTemplate.getMessageConverters().add(new StringHttpMessageConverter());

20.   }

21.

22.   // exécution de l'appel au service REST

23.   public String executeRestService(String method, String urlService, Object request, Map<String, String> paramètres) { 24.

25.       // on vérifie que le serveur distant répond assez vite

26.       // une exception est lancée sinon

27.       checkResponsiveness(urlService);

28.       // vérification méthode HTTP

29.       method = method.toLowerCase(new Locale("fr-FR"));

30.       if (!method.equals("get") && !method.equals("post")) {

31.       throw new AleaException("[dao.executeRestService] L'argument [method] doit avoir la valeur post ou get");

32.       }

33.       try {

34.       // exécution service

35.       if (method.equals("get")) {

36.       return restTemplate.getForObject(urlService, String.class, paramètres);

37.       } else {

38.       return restTemplate.postForObject(urlService, request, String.class, paramètres);

39.       }

40.       } catch (Exception ex) {

41.       throw new AleaException("[dao.executeRestService] Une erreur s'est produite", ex);

42.   }

43.   }

44.

45.   private void checkResponsiveness(String urlService) {

46 . 47.

48.   }

49.

50.   // délai d'attente maximal

51.   public void setTimeout(int millis) {

52.   this.timeout = millis;

53.   }

54.

55.}

•      ligne 9 : la couche [DAO] s'appuie sur un client REST fourni par la bibliothèque  [spring-android-framework]. Le principal élément de cette bibliothèque est la classe [RestTemplate]. Le coeur du framework Spring n'a pas encore été porté sur Android. Alors qu'habituellement, le champ de la ligne 9 aurait été initialisé par Spring, ici il le sera par le constructeur. Pour avoir cette bibliothèque, nous ajouterons une dépendance dans le fichier [] :

<!-- Spring Android -->

<dependency>

<groupId>org.springframework.android</groupId>

<artifactId>spring-android-rest-template</artifactId>

<version>${spring-android-version}</version> </dependency>

•      ligne 16 : l'objet [RestTemplate] est instancié ;

•      ligne 19 : il est configuré. On lui donne une liste de " convertisseurs de messages ", ç-à-d des classes capables de traiter la réponse du serveur REST. Ici le serveur REST utilisé envoie une chaîne JSON. On va traiter celle-ci comme une chaîne de caractères sur laquelle on ne fera pas de traitement. Le " convertisseurs de messages " qui convient alors est la classe [StringHttpMessageConverter]. On peut être tenté d'utiliser le convertisseur [GsonHttpMessageConverter]. Ce convertisseur va alors transformer la chaîne JSON en l'objet qu'on lui indiquera, par exemple dans notre cas une liste de [String] ou encore une liste d'objets [Object]. Les tests montrent qu'on perd alors les caractères accentués présents dans la chaîne JSON envoyée par le serveur. Le convertisseur [StringHttpMessageConverter] n'a pas ce problème. Nous recevrons donc un type [String] qu'on désérialisera ultérieurement en [List<String>] ; • ligne 23 : la méthode [executeRestService] reçoit les paramètres suivants :

1.     la méthode " get " ou " post " de la requête HTTP à émettre,

2.     l'URL complète du service REST à exécuter,

3.     un dictionnaire des données transmises par une opération HTTP POST,

4.     sous la forme d'un dictionnaire, les valeurs des variables de l'URL ;

•      ligne 27 : on vérifie que le serveur REST répond au bout de timeout millisecondes. Si ce n'est pas le cas, la méthode

[checkResponsiveness] lance une exception ;

•      lignes 30-32 : vérification de la méthode HTTP ;

•      lignes 33-42 : appel du service REST par l'objet [RestTemplate] de Spring (ligne 9). Cette classe a de nombreuses méthodes pour dialoguer avec un service REST. Celles utilisées ici vont rendre comme résultat, la chaîne JSON renvoyée par le serveur ;

•      ligne 36 : les paramètres de la méthode [RestTemplate].getForObject sont les suivants :

•      0 : l'URL requêtée par exemple [http://localhost:8080/exemple-08-serve-rest/{a}/{b}/{n}],

•      1 : la classe de la réponse, ici un type [String],

•      2 : un dictionnaire contenant les paramètres effectifs de l'URL, ici les valeurs de a, b et n ;

•      ligne 38 : les paramètres de la méthode [RestTemplate].postForObject sont les mêmes si ce n'est le second paramètre [request] qui est la valeur postée ;

La méthode [checkResponsiveness] est la suivante :

1.

private void checkResponsiveness(String urlService) {

2.

// on crée l'URI du service distant

3.

String url = urlService.replace("{", "").replace("}", "");

4.

URI service = null;

5.

try {

6.

service = new URI(url);

7.

} catch (URISyntaxException ex) {

8.      throw new AleaException(String.format("Format d'URL incorrect [%s]", urlService), ex);

9.      }

10.        // on se connecte au service

11.        Socket client = null;

12.        try {

13.        // on se connecte au service avec attente maximale définie par

14.        // configuration

15.        client = new Socket();

16.        client.connect(new InetSocketAddress(service.getHost(), service.getPort()), timeout);

17.        } catch (IOException e) {

18.        throw new AleaException("Le service distant n'a pas répondu assez vite", e);

19.        } finally {

20.        // on libère les ressources

21.        if (client != null) {

22.        try {

23.        client.close();

24.        } catch (IOException ex) {

25.        Logger.getLogger(Dao.class.getName()).log(Level.SEVERE, null, ex);

26.        }

27.        }

28.        }

•   ligne 1 : le paramètre [urlService] est de la forme [http://localhost:8080/exemple-08-serve-rest/{a}/{b}/{n}];

•   ligne 3 : l'URL précédente devient [http://localhost:8080/exemple-08-serve-rest/a/b/n];

•   lignes 5-9 : à partir de cette URL, on essaie de construire un objet URI (Uniform Resource Identifier).  Si on n'y arrive pas c'est que l'URL est incorrecte ;

•   lignes 15-16 : on se connecte à la machine et au port définis par l'URI qui vient d'être construite et on donne un temps maximum de timeout millisecondes pour obtenir la réponse (ligne 16) ;

•   ligne 18 : si pour une raison ou une autre (absence du serveur ou délai d'attente dépassé), la connexion échoue alors on lance une exception.

 9.3.5      La couche [Android]

Nous gardons la couche [Android] du projet [exemple-07]. C'est possible car nous utilisons une interface [IMetier] qui englobe l'interface [IMetier] du projet [exemple-07].

Il y a une modification à faire dans [MainActivity]. Dans [exemple-07] nous avions écrit :

// instanciation couche [métier] métier = new Metier();

Dans [exemple-08-client-rest], il y a désormais une couche [métier] et une couche [DAO] à instancier. Le code devient le suivant :

1. public class MainActivity extends FragmentActivity { 2.

3.   

4.    // URL service REST

5.    final private String URL_SERVICE_REST = "172.19.81.34:8080/exemple-08-server-rest"; 6.

7.    @Override

8.    protected void onCreate(Bundle savedInstanceState) { 9.

10.

11.   // instanciation couche [dao]

12.   IDao dao = new Dao();

13.   dao.setTimeout(1000);

14.   // instanciation couche [métier]

15.   métier = new Metier();

16.   métier.setDao(dao);

17.   métier.setUrlServiceRest(URL_SERVICE_REST);

18.   }

19.

20 ..

•   ligne 12 : la couche [DAO] est instanciée ;

•   ligne 13 : on lui fixe son timeout ;

•   ligne 15 : on instancie la couche [métier] ;

•   ligne 16 : on lui injecte la référence de la couche [DAO] ;

•   ligne 17 : on lui injecte l'URL du service REST ;

Pour connaître l'adresse IP à mettre en ligne 5, ouvrez une fenêtre [DOS] et tapez la commande suivante :

1. dos>ipconfig 2.

3. Configuration IP de Windows 4.

5. .

6.

7. Carte Ethernet Connexion au réseau local :

8.

9.    Suffixe DNS propre à la connexion. . . :

10.   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4

11.   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34

12.   Masque de sous-réseau. . . . . . . . . : 255.255.0.0 13.   Passerelle par défaut. . . . . . . . . : 172.19.0.254 14.

15.Carte réseau sans fil Wi-Fi :

16.

17.   Statut du média. . . . . . . . . . . . : Média déconnecté 18.   Suffixe DNS propre à la connexion. . . :

19 .

La ligne 11 donne l'adresse IP de votre poste.

Les dépendances du projet Maven [exemple-08-client-rest] sont les suivantes :

1.

<?xml version="1.0" encoding="UTF-8"?>

2.

<project xmlns="; xmlns:xsi=";

3.

xsi:schemaLocation=" ">

4.

<modelVersion>4.0.0</modelVersion>

5.

<groupId>exemples</groupId>

6.    <artifactId>exemple-08-client-rest</artifactId>

7.    <version>0.0.1-SNAPSHOT</version>

8.    <packaging>apk</packaging> 9.     <name>exemple-08-client-rest</name>

10.

11.   <properties>

12.   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

13.   <platform.version> 4.1.1.4

14.   </platform.version>

15.   <android.plugin.version>3.5.3</android.plugin.version>

16.   <spring-android-version>1.0.1.RELEASE</spring-android-version>

17.   <-version>2.2.2<-version>

18.   </properties> 19.

20.      <dependencies>

21.      <!-- Android -->

22.      <dependency>

23.      <groupId>com.google.android</groupId>

24.      <artifactId>android</artifactId>

25.      <version>${platform.version}</version>

26.      <scope>provided</scope>

27.      </dependency>

28.      <!-- Support Android - NE PAS OUBLIER!!! -->

29.      <dependency>

30.      <groupId>com.google.android</groupId>

31.      <artifactId>support-v4</artifactId>

32.      <version>r6</version>

33.      <!-- <scope>provided</scope> -->

34.      </dependency>

35.      <!-- Spring Android -->

36.      <dependency>

37.      <groupId>org.springframework.android</groupId>

38.      <artifactId>spring-android-rest-template</artifactId>

39.      <version>${spring-android-version}</version>

40.      </dependency>

41.      <!-- Gson JSON Processor -->

42.      <dependency>

43.      <groupId></groupId>

44.      <artifactId>gson</artifactId>

45.      <version>${-version}</version>

46.      </dependency>

47.

48.   </dependencies>

49.   <build> 50 .

51.   </build>

52.</project>

•   lignes 22-27 : dépendance sur la plateforme Android ;

•   lignes 29-34 : dépendance sur la bibliothèque de support d'Android. Selon votre configuration d'Eclipse décommentez ou non la ligne 33 ;

•   lignes 36-40 : dépendance sur la bibliothèque Spring-Android ;

•   lignes 42-46 : dépendance sur la bibliothèque JSON Gson ;

 9.3.6      Exécution du client REST

Créez un environnement d'exécution pour le projet [exemple-08-client-rest] et exécutez-le sur l'émulateur de tablette. On obtient l'erreur suivante :

On a une exception. Lorsqu'on se renseigne sur elle, on découvre qu'elle est lancée parce qu'on a ouvert une connexion réseau dans le thread de l'activité. Les bonnes pratiques poussent à ouvrir les connexions réseau dans un thread différent de celui de l'activité. Cette exception vise à renforcer cet usage. Ceci dit, cette règle peut être contournée en ajoutant la ligne 14 ci-dessous dans [Mainactivity] :

1.

@Override

2.

protected void onCreate(Bundle savedInstanceState) {

3.

4. 5.

// instanciation couche [dao]

6.

IDao dao = new Dao();

7.

dao.setTimeout(1000);

8.

// instanciation couche [métier]

9.

métier = new Metier();

10.

métier.setDao(dao);

11.

métier.setUrlServiceRest(URL_SERVICE_REST);

12. 13.

// accès réseau

14.

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());

15.

}

La ligne 14 va autoriser les connexions réseau dans le thread de l'activité. Vérifiez-le en exécutant l'application.

Pour tester l'application avec une vraie tablette, vous devez mettre le PC et la tablette sur le même réseau Wifi. Utilisez pour cela la clé wifi qu'on vous a donnée.

Faites la commande Dos [ipconfig] pour découvrir l'adresse wifi du PC :

1.

Carte réseau sans fil Wi-Fi :

2. 3.

   Suffixe DNS propre à la connexion. . . :

4.

   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2

5.

   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25

6.

   Masque de sous-réseau. . . . . . . . . : 255.255.255.0

7.

Passerelle par défaut. . . . . . . . . : 192.168.1.1

La ligne 5 donne l'adresse Wifi du PC. Inscrivez cette adresse dans [MainActivity] :

// URL service REST final private String URL_SERVICE_REST = "192.168.1.25:8080/exemple-08-server-rest";

Inhibez le pare-feu de votre PC qui empêche toute connexion venant de l'extérieur et exécutez votre projet sur la tablette.

 10 Exemple-09 : un client REST asynchrone

Dans le projet précédent, nous avons contourné une bonne pratique qui dit qu'on ne doit pas ouvrir une connexion réseau dans le thread de l'activité. Nous allons ici la créer dans un autre thread. La tâche qui s'exécutera dans ce thread sera par ailleurs asynchrone. La vue [Vue_01] la lancera sans attendre sa fin. Elle sera avertie par un événement que la tâche a terminé son travail.

L'architecture évolue comme suit :

La tâche s'intercale entre la vue et la couche [métier]. Elle va exécuter les méthodes de la couche [métier] dans des threads différents de celui de l'activité. Elle se déroulera de façon asynchrone. Concrètement cela signifie que l'utilisateur peut continuer à interagir avec la vue en même temps que la tâche s'exécute en tâche de fond. La tâche doit avertir la vue lorsqu'elle a terminé son travail.

Pour implémenter la tâche, nous allons étendre une classe Android, la classe [AsyncTask].

 10.1

La classe [AsyncTask]

D'après la documentation Android :

An asynchronous task AsyncTask<Params, Progress, Result> is defined by a computation that runs on a background thread and whose result is published on the UI thread. An asynchronous task is defined by 3 generic types, called Params, Progress and Result, and 4 steps, called onPreExecute, doInBackground, onProgressUpdate and onPostExecute.

La classe [AsyncTask<Params, Progress, Result>] peut avoir différentes signatures. Nous utiliserons la signature AsyncTask<Object,Object,Void>. La classe a quatre méthodes principales :

•   la méthode [doInBackGround] est la méthode exécutée en tâche de fond. Sa signature est la suivante

protected abstract Result doInBackground (Params params)

Elle reçoit une suite de paramètres de type Params. Pour nous ce type sera Object. C'est le premier type générique de notre signature AsyncTask<Object,Object,Void>. Elle rend une donnée de type Result. Nous, nous ne rendrons aucun résultat. C'est le troisième type générique de notre signature AsyncTask<Object,Object,Void> ;

•   la méthode [onProgressUpdate] permet de publier dans le thread de l'activité l'avancement de la tâche. Sa signature est la suivante :

protected void onProgressUpdate (Progress values)

Nous n'utiliserons pas cette méthode. Dans la signature AsyncTask<Object,Object,Void>, le type Progress est le deuxième type générique, ici Object ;

•   la méthode [onPreExecute] est exécutée dans le thread de l'activité avant l'exécution de la méthode [doInBackGround]. Sa signature est la suivante :

protected void onPreExecute ()

C'est dans cette méthode que la [Task] enverra à son boss la notification [WORK_STARTED]. La notification est donc envoyée dans le thread de l'UI ;

•   la méthode [onPostExecute] est exécutée dans le thread de l'activité après l'exécution de la méthode [doInBackGround]. Sa signature est la suivante :

protected void onPostExecute (Result result)

où le type Result est le troisième type générique de la signature AsynTask<Object, Object, Void>, donc Void ici. C'est dans cette méthode que la tâche transmettra son résultat à l'objet qui l'a appelée. Cette information remontera jusqu'à la vue. Celle-ci pourra utiliser cette information pour se mettre à jour. C'est possible car on sera alors dans le thread de l'activité.

 10.2

Le projet Android

Créez un projet [exemple-09-client-rest-asynchrone] par copie du projet [exemple-08-client-rest] :

Modifiez les caractéristiques du projet dans [] de la façon suivante :

<groupId>exemples</groupId>

<artifactId>exemple-09-client-rest-asynchrone</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>apk</packaging>

<name>exemple-09-client-rest-asynchrone</name>

 10.3

Les éléments de l'interface asynchrone

Nous allons rassembler les éléments de l'interface asynchrone dans un package [.android.tasks] :

La tâche asynchrone aura l'interface [ITask] suivante :

1.

package .android.tasks;

2. 3.

import .android.metier.IMetier;

4. 5.

public interface ITask {

6.

// la tâche asynchrone doit savoir qui l'appelle

7.

// pour lui rendre son résultat

8.

void setCaller(ICaller caller);

9.

10.

// la tâche asynchrone doit avoir accès à la couche [métier]

11. 12.}

void setMetier(IMetier métier);

     

La tâche interagit avec la vue et la couche [métier] :

•   elle doit avoir une référence sur la couche [métier]. La méthode [setMetier] de la ligne 11 sert à injecter cette référence ;

•   elle doit savoir à qui elle doit rendre son résultat. Comme elle s'exécute dans un autre thread que la vue qui l'a appelée, celle-ci n'attend pas son résultat. La tâche doit savoir à qui rendre le résultat qu'elle va obtenir. C'est la méthode [setCaller] de la ligne 8 qui sert à injecter cette information.

L'interface [ICaller] est la suivante :

1.

package .android.tasks;

2. 3.

public interface ICaller {

4.

// définit une méthode pour récupérer l'info produite par une tâche

5.

public void callBack(Object info);

6.

}

On se rappelle que la tâche [ITask] reçoit un objet [ICaller]. Pour rendre l'information [info] qu'elle a produite, la tâche utilisera la méthode [ICaller].callBack(info).

La tâche chargée d'obtenir les nombres aléatoires est la tâche [AleaTask] suivante :

1.

package .android.tasks;

2. 3.

import .android.metier.IMetier;

4.

import .AsyncTask;

5. 6.

public class AleaTask extends AsyncTask<Object, Object, Void> implements ITask {

7. 8.

// couche [métier]

9.

private IMetier métier;

10.

// le caller

11.

private ICaller caller;

12.

// l'info produite par la tâche

13.

private Object info;

14. 15.

@Override

16.

protected Void doInBackground(Object params) {

17.

// on est dans un autre thread que celui de l'UI

18.

// on récupère les trois paramètres - on suppose qu'ils sont corrects

19.

int a = (Integer) params[0];

20.

int b = (Integer) params[1];

21.

int n = (Integer) params[2];

22.

// on appelle la couche [métier]

23.

info = métier.getAleas(a, b, n);

24.

// fin

25.

return null;

26.

}

27. 28.

protected void onPostExecute(Void result) {

29.

// on est dans le thread de l'UI

30.

// on rend l'info au caller

31.

caller.callBack(info);

32.

}

33. 34.

// setters

35.

public void setMetier(IMetier métier) {

36.

this.métier = métier;

37.

}

38. 39.

public void setCaller(ICaller caller) {

40.

this.caller = caller;

41.

42.

43.}

}

     

•   ligne 6 : la tâche implémente l'interface [ITask] d'où la présence des méthodes [setMetier] (ligne 35) et [setCaller] (ligne 39) ;

•   ligne 6 : la tâche étend la classe AsyncTask<Object, Object, Void> d'où la présence des méthodes [doInBackground] (ligne 16) et [onPostExecute] (ligne 28) ;

•   ligne 16 : c'est la méthode [doInBackground] qui est exécutée en tâche de fond. C'est dans cette méthode qu'on doit faire les connexions réseau. Cela signifie pour nous que c'est dans cette méthode qu'on doit appeler la couche [métier] ;

•   ligne 16 : la méthode va recevoir trois paramètres, dans l'ordre les valeurs a, b et n pour calculer n nombres aléatoires dans l'intervalle [a,b]. La notation [Object params] est analogue à la notation [Object[] params]. On reçoit un tableau d'objets ;

•   lignes 19-21 : on récupère les trois paramètres  a, b et n ;

•   ligne 23 : on appelle la couche [métier]. La méthode [métier.getaleas] rend un type [List<Object>] qu'on met dans le type [Object] de la ligne 13. La couche [métier] ne lance pas d'exception. C'est pourquoi on n'a pas utilisé de try / catch ;

•   ligne 28 : la méthode [onPostExecute] est exécutée une fois que la méthode [doInBackground] a terminé son travail. Par ailleurs, elle s'exécute dans le thread de l'activité Android. L'information [info] produite par la méthode [doInBackground] peut alors être utilisée pour mettre à jour l'interface visuelle de l'activité ;

•   ligne 31 : on appelle la méthode [callBack] du [ICaller] de la ligne 11 pour rendre l'information [info] produite par la méthode [doInBackground].

 10.4

L'appel de la tâche dans la vue

La tâche est appelée par la vue. Celle-ci auparavant appelait directement la couche [métier]. Les changements se font dans la méthode [doExecuter] de la classe [Vue_01] :

La méthode [doExecuter] devient la suivante :

1.      protected void doExecuter() {

2.      // on efface les éventuels msg d'erreur précédents

3.      txtErrorAleas.setText("");

4.      txtErrorIntervalle.setText("");

5.      // on teste la validité des saisies

6.      if (!isPageValid()) {

7.      return;

8.      }

9.      // on efface les réponses précédentes

10.       listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, new String[] {}));

11.       // on demande à une tâche asynchrone les nombres aléatoires

12.       // on crée la tâche asynchrone

13.       AleaTask task = new AleaTask();

14.       // on lui injecte la méthode de rappel

15.       task.setCaller(new ICaller() {

16.       public void callBack(Object info) {

17.       // on traite l'info reçue

18.       showInfo(info);

19.       }

20.       });

21.       // on lui injecte la couche [métier]

22.       task.setMetier(activité.getMétier());

23.       // on l'exécute

24.       task.executeOnExecutor(.AsyncTask.THREAD_POOL_EXECUTOR, a, b, nbAleas);

25.       }

26.

27.    protected void showInfo(Object info) { 28 .

29.   }

30.

•   ligne 27 : ce qui était fait précédemment dans la méthode [doExécuter] à réception des nombres aléatoires migre dans la méthode [showInfo]. C'est cette méthode qui sera appelée par la tâche asynchrone lorsqu'elle aura fini son travail ;

•   ligne 13 : la tâche asynchrone est créée ;

•   lignes 15-20 : on lui injecte un objet de type [ICaller] qui indique à la tâche la méthode à rappeler lorsqu'elle a terminé son travail. On utilise une technique appelée la classe anonyme. La syntaxe est la suivante :

new Interface(){

// méthodes d'implémentation de l'interface }

donc ici :

1.

// on lui injecte la méthode de rappel

2.

task.setCaller(new ICaller() {

3.

public void callBack(Object info) {

4.

// on traite l'info reçue

5.

showInfo(info);

6.

}

7.

});

       

•   lorsque la tâche aura terminé son travail, elle appellera la méthode de la ligne 3 ci-dessus, en lui passant l'objet [info] qu'elle aura créé ;

•   ligne 5 : la méthode [callBack] appelle la méthode [showInfo] de la classe [Vue_01].

Si on n'utilisait pas la méthode de la classe anonyme, alors la vue [Vue_01] devrait implémenter elle-même l'interface [ICaller] et avoir une méthode [void callBack(Object)].

Commentons les dernières lignes de la méthode [doExécuter] :

1.

// on lui injecte la couche [métier]

2.

task.setMetier(activité.getMétier());

3.

// on l'exécute

4.

task.executeOnExecutor(.AsyncTask.THREAD_POOL_EXECUTOR, a, b, nbAleas);

•   ligne 2 : on injecte la couche [métier] dans la tâche. Celle-ci est désormais configurée. Elle peut être exécutée ; • ligne 4 : on exécute la tâche. On aurait pu également écrire :

task.execute(a, b, nbAleas)

On demande à la tâche de s'exécuter. Celle-ci attend un tableau de paramètres ou bien une suite de paramètres. Ici, on lui passe les valeurs qu'elle attend [a, b, nbAleas]. Avec cette syntaxe, un seul thread est alloué pour les tâches asynchrones. On ne peut donc avoir qu'une seule tâche asynchrone à la fois. Si on veut pouvoir en avoir plusieurs, il faut utiliser la syntaxe de la ligne 4 ci-dessus. Dans notre cas, un thread suffit mais on a voulu montrer la syntaxe du cas plus général. Lorsque la ligne 4 ci-dessus a été exécutée, deux threads s'exécutent en parallèle : celui de l'activité Android et celui de la tâche asynchrone. L'utilisateur peut alors interagir avec l'interface visuelle. Il faudrait mettre ici un indicateur montrant qu'une opération en tâche de fond est en cours.

La méthode [showInfo] va être appelée par la tâche lorsque celle-ci aura terminé son travail :

1.     protected void showInfo(Object info) {

2.     // on récupère une liste d'objets

3.     @SuppressWarnings("unchecked")

4.     List<Object> data = (List<Object>) info;

5.     // on affiche les objets de la liste

6.     List<String> strings = new ArrayList<String>();

7.     for (Object o : data) {

8.     (o.toString());

9.     }

10.      // on affiche les réponses

11.      listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, strings));

12.}

•   ligne 4 : on sait que la couche [métier] renvoie un type [List<Object>]. On caste donc l'objet [info] reçu en paramètre vers un type [List<Object>] ;

•   lignes 6-9 : à partir de cette liste d'objets on crée une liste de [String] ;

•   ligne 11 : qu'on associe au [ListView] de l'interface ;

 10.5

Modification de l'activité

La classe [MainActivity] est modifiée pour ne plus autoriser les connexions réseau dans le thread de l'activité :

1.

@Override

2.

protected void onCreate(Bundle savedInstanceState) {

3.

4.

// instanciation couche [métier]

5.

métier = new Metier();

6.

métier.setDao(dao);

7.

métier.setUrlServiceRest(URL_SERVICE_REST);

8. 9.

// accès réseau dans le thread de l'UI

10.

// StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());

11.

}

La ligne 10 est mise en commentaires.

 10.6

Exécution du client REST asynchrone

Créez un environnement d'exécution pour le projet [exemple-09-client-rest-asynchrone] et exécutez-le à la fois sur l'émulateur de tablette et sur la tablette elle-même si vous avez un réseau wifi.

 11 Exemple-10 : annulation d'une tâche asynchrone

Dans le projet précédent, nous n'avons rien prévu pour annuler la tâche asynchrone si elle tarde à répondre. Puisque lorsqu'elle est lancée, l'utilisateur peut continuer à interagir avec l'interface visuelle on peut lui présenter un bouton [Annuler] qu'il peut utiliser pour annuler la tâche qu'il a lancée. Par ailleurs, on peut lui proposer également un indicateur visuel qui montre qu'une tâche de fond est en cours. Le nouveau projet répond à ces deux demandes.

Créez un projet [exemple-10-client-rest-asynchrone-annuler] en dupliquant le projet [exemple-10-client-rest-asynchrone].

Modifiez les caractéristiques du fichier [] de la façon suivante :

<groupId>exemples</groupId>

<artifactId>exemple-10-client-rest-annuler</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>apk</packaging>

<name>exemple-10-client-rest-annuler</name>

 11.1

L'activité [MainActivity]

L'activité [MainActivity] évolue pour inclure une image d'attente :

1.

@Override

2.

protected void onCreate(Bundle savedInstanceState) {

3.

// classique

4.

super.onCreate(savedInstanceState);

5.

// il faut installer le sablier avant de créer la vue

6.

requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

7.

// création de la vue

8.

setContentView(R.layout.activity_main);

C'est la ligne 6 qui installe une image d'attente, pour l'instant cachée, en haut et à droite de la barre d'état réservée à l'activité :

Pour afficher l'image on écrit :

[Activity].setProgressBarIndeterminateVisibility(true);

et pour la cacher :

[Activity].setProgressBarIndeterminateVisibility(false);

où [Activity] désigne l'activité qui affiche l'image.

 11.2

La vue XML [vue_01]

La vue XML [vue_01] doit intégrer un bouton supplémentaire, le bouton [Annuler] qui annulera la tâche asynchrone lancée par la vue. Nous allons placer le bouton [Annuler] par-dessus le bouton [Exécuter]. A un moment donné, seul l'un d'eux sera visible. Le code XML des deux boutons est le suivant :

1.    <Button

2.    android:id="@+id/btn_Executer"

3.    android:layout_width="wrap_content"

4.    android:layout_height="wrap_content"

5.    android:layout_alignParentLeft="true"

6.    android:layout_below="@+id/txt_a"

7.    android:layout_marginTop="20dp" 8.        android:text="@string/btn_executer" /> 9.

10.      <Button

11.      android:id="@+id/btn_Annuler"

12.      android:layout_width="wrap_content"

13.      android:layout_height="wrap_content"

14.      android:layout_alignParentLeft="true"

15.      android:layout_below="@+id/txt_a"

16.      android:layout_marginTop="20dp"

17.android:text="@string/btn_annuler" />

Les lignes 10-17 sont d'abord obtenues par copier / coller des lignes 1-8 puis on change les éléments suivants :

•   ligne 11 : l'identifiant du bouton sera [btn_Annuler] ;

•   ligne 17 : son texte sera fourni par la chaîne [btn_annuler] dans le fichier [] :

    <string name="btn_executer">Exécuter</string>

    <string name="btn_annuler">Annuler</string>

Comme le bouton [Annuler] a été obtenu par copier / coller du bouton [Exécuter], ils occupent la même place dans la vue. Ils sont superposés. On fera en sorte qu'à un moment donné, un seul des deux soit visible.

 11.3

Le fragment [Vue_01]

Le code du fragment [Vue_01] associé à la vue XML [vue_01] évolue de la façon suivante :

1. public class Vue_01 extends Fragment { 2.

3.    // les éléments de l'interface visuelle

4.    private Button btnExecuter;

5.    private Button btnAnnuler;

6.   

7.    // la tâche asynchrone 8.  private AleaTask task; 9.

10.  @Override

11.  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

12

13.       // bouton Exécuter

14.       btnExecuter = (Button) view.findViewById(R.id.btn_Executer);

15.       btnExecuter.setOnClickListener(new OnClickListener() {

16.       public void onClick(View arg0) {

17.       doExecuter();

18.       }

19.       });

20.       // bouton Annuler

21.       btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);

22.       btnAnnuler.setOnClickListener(new OnClickListener() {

23.       public void onClick(View arg0) {

24.       // on annule la tâche

25.       task.cancel(true);

26.       // on arrête l'attente

27.       cancelWaiting();

28.       }

29.});

•   ligne 5 : la référence du bouton [Annuler] ;

•   ligne 21 : on récupère cette référence ;

•   lignes 22-28 : on gère le clic sur le bouton [Annuler] ;

•   ligne 25 : on annule la tâche asynchrone référencée ligne 8. Cette tâche étend la tâche [AsyncTask<Object,Void,Object>]. La classe [AsyncTask] a une méthode [cancel] qui permet d'annuler la tâche. Cette méthode admet comme paramètre un booléen : à vrai, la tâche s'arrête immédiatement. A faux, on laisse sa méthode [doInBackground] se terminer mais la méthode [onPostExecute] ne sera pas exécutée ;

•   ligne 27 : on annule l'attente.

La méthode [doExécuter] référencée ligne 17 évolue comme suit :

1.

protected void doExecuter() {

2.

3.

// on exécute la tâche

4.

task.executeOnExecutor(.AsyncTask.THREAD_POOL_EXECUTOR, a, b, nbAleas);

5.

// on commence l'attente

6.

beginWaiting();

7.

}

Après avoir lancé la tâche asynchrone ligne 4, on commence l'attente du résultat ligne 6. C'est une attente visuelle. L'utilisateur peut continuer à interagir avec l'interface.

La méthode [beginWaiting] est la suivante :

1.

private void beginWaiting() {

2.

// le bouton [Annuler] remplace le bouton [Exécuter]

3.

btnExecuter.setVisibility(View.INVISIBLE);

4.

btnAnnuler.setVisibility(View.VISIBLE);

5.

// on met le sablier

6.

activité.setProgressBarIndeterminateVisibility(true);

7.

}

La méthode [cancelWaiting] appelée lorsqu'on clique sur le bouton [Annuler] est la suivante :

1.

protected void cancelWaiting() {

2.

// le bouton [Exécuter] remplace le bouton [Annuler]

3.

btnAnnuler.setVisibility(View.INVISIBLE);

4.

btnExecuter.setVisibility(View.VISIBLE);

5.

// on enlève le sablier

6.

activité.setProgressBarIndeterminateVisibility(false);

7.

}

Enfin, la méthode [showInfo] qui affiche le résultat envoyé par la tâche asynchrone évolue comme suit :

1.

protected void showInfo(Object info) {

2.

// on termine l'attente

3.

cancelWaiting();

4.

// on récupère une liste d'objets

5.

@SuppressWarnings("unchecked")

6.

List<Object> data = (List<Object>) info;

7.

8.

}

Créez un environnement d'exécution pour le projet [exemple-10-client-rest-asynchrone-annuler] et exécutez-le. Pour voir l'action du bouton [Annuler] modifiez la couche [métier] de votre serveur REST de la façon suivante :

1.

@Service

2.

public class Metier implements IMetier {

3. 4.

@Override

5.

public List<Object> getAleas(int a, int b, int n) {

6.

// on s'arrête 5 secondes

7.

try {

8.

Thread.sleep(5000);

9.

} catch (InterruptedException e) {

10.

// TODO Auto-generated catch block

11.

e.printStackTrace();

12.

}

13. 14.

// la liste des objets

15.

List<Object> réponses = new ArrayList<Object>();

16.

// qqs vérifications

17.

•   lignes 7-12 : on arrête artificiellement le thread de la couche [métier] pendant 5 secondes. Du coup la tâche asynchrone ne rendra son résultat qu'au bout de 5 secondes ce qui permet de voir le bouton [Annuler] ainsi que le sablier.

 12 Exemple 11 : gestion de plusieurs tâches asynchrones

Dans l'exemple précédent, nous n'avons géré qu'une tâche asynchrone. Dans cet exemple, nous allons en gérer plusieurs. Nous gardons le même projet que précédemment mais au lieu que les N nombres aléatoires soient générés par une seule tâche asynchrone, nous allons utiliser N tâches asynchrones qui vont chacune générer un nombre aléatoire. Pour gérer la fin de l'attente, nous allons simplement compter les réponses envoyées par les tâches asynchrones. Lorsqu'on en aura N, on arrêtera l'attente. Cette méthode marche bien lorsque la vue connaît les tâches lancées (elle peut alors les annuler) et le nombre de résultats attendus (elle peut alors les compter). Pour des cas plus complexes, une méthode différente est proposée dans

[].

 12.1

Le serveur [REST]

Notre serveur REST doit évoluer. On ne lui demande plus N nombres aléatoires d'un coup mais un seul à chaque fois.

Nous commençons par dupliquer le projet [exemple-08-server-rest] dans [exemple-11-server-rest] :

Modifiez les identifiants du projet dans [] de la façon suivante :

1.

<modelVersion>4.0.0</modelVersion>

2.

<groupId>exemples</groupId>

3.

<artifactId>exemple-11-server-rest</artifactId>

4.

<name>exemple-11-server-rest</name>

5.

<packaging>war</packaging>

6.

<version>1.0.0-BUILD-SNAPSHOT</version>

 12.1.1      La couche [métier]

Spring / Tomcat

L'interface [IMetier] évolue de la façon suivante :

1.

package .aleas.metier;

2. 3.

public interface IMetier {

4. 5.

// rend un nombre aléatoire dans l'intervalle [a,b]

6.

public int getAlea(int a, int b);

7.

}

L'unique méthode de cette interface rend un nombre aléatoire dans l'intervalle [a,b].

L'implémentation [Metier] de cette interface est la suivante :

1. package .aleas.metier; 2.

3. import .Random; 4.

5. import org.springframework.stereotype.Service; 6.

7.    @Service

8.    public class Metier implements IMetier { 9.

10.   @Override

11.   public int getAlea(int a, int b) {

12.   // on s'arrête 5 secondes

13.   //try {

14.   // Thread.sleep(5000);

15.   //} catch (InterruptedException e) {

16.   // // TODO Auto-generated catch block

17.   // e.printStackTrace();

18.   //}

19.

20.      // vérifications

21.      if (a < 0) {

22.      throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0");

23.      }

24.      if (b < 0) {

25.      throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0");

26.      }

27.      if (a >= b) {

28.      throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b");

29.      }

30.      // on génère le nombre aléatoire

31.      Random random = new Random();

32.      // on génère une exception aléatoire 1 fois / 3

33.      int nombre = random.nextInt(3);

34.      if (nombre == 0) {

35.      throw new AleaException("Exception aléatoire");

36.      } else {

37.      // sinon on rend un nombre aléatoire entre deux bornes [a,b]

38.      return a + random.nextInt(b - a + 1);

39.      }

40.      }

41.}

•   lignes 13-18 : on arrête artificiellement pendant 5 secondes le thread de la couche [métier], toujours pour que l'utilisateur puisse voir le bouton [Annuler] ;

•   le nombre aléatoire est généré ligne 38. S'il n'est pas généré ici, c'est qu'une exception de type [AleaException] a été lancée entre-temps.

 12.1.2      Le contrôleur Spring MVC

Spring / Tomcat

Le contrôleur [AleaController] devient le suivant :

1. package ; 2.

3. import .aleas.metier.AleaException; 4.

5.

6.    @Controller



7.    public class AleaController { 8.

9.    // couche métier

10.   @Inject

11.   private IMetier metier; 12.

13.  // nombre aléatoire

14.  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")

15.  @ResponseBody

16.  public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonGenerationException, JsonMappingException, IOException { 17.

18.   // on utilise la couche métier

19.   int alea = 0;

20.   AleaException aleaException = null;

21.   try {

22.   alea = metier.getAlea(a, b);

23.   } catch (AleaException ex) {

24.   aleaException = ex;

25.   }

26.   // on va renvoyer la chaîne Json d'un dictionnaire 27. Map<String, String> map = new HashMap<String, String>(); 28.    // exception ?

29.      if (aleaException == null) {

30.      ("erreur", "0");

31.      ("alea", String.valueOf(alea));

32.      } else {

33.      ("erreur", "1");

34.      ("msg", aleaException.getMessage());

35.      }

36.      // on rend la chaîne Json du dictionnaire

37.

return new ObjectMapper().writeValueAsString(map);

38.

39.

40.}

}

•   ligne 14 : l'URL attendue est désormais simplement [/a/b] pour demander un nombre aléatoire dans l'intervalle [a,b]. Le contrôleur rendra son résultat sous la forme de deux chaînes JSON :

{"erreur":"0","alea":"127"}

s'il n'y a pas eu d'erreur ou bien

{"erreur":"1","msg":"message de l'exception"}

s'il y a eu une erreur.

•   ligne 16 : il n'y a plus que deux paramètres nommés a et b ;

•   lignes 19-25 : le nombre aléatoire dans l'intervalle [a,b] est demandé à la couche [métier] ;

•   lignes 27-35 : le dictionnaire de la réponse est construit ;

•   ligne 37 : on renvoie la chaîne JSON de ce dictionnaire ;

Exécutez cette application web (clic droit sur projet / Run as / Run on server). On obtient deux sortes de résultats :

Vous pouvez rencontrer deux problèmes :

•   l'URL présentée est [localhost:8080/exemple-08-server-rest]. Eclipse a gardé l'URL du projet [exemple-08]. Supprimez le projet [clic droit / Delete] sans supprimer le dossier puis réimportez le projet ;

•   vous n'obtenez pas le caractère accentué de [aléatoire]. Vérifiez que votre projet est en UTF-8. Lorsqu'on importe un projet, Eclipse le met par défaut en [Cp1252] ;

 12.2

Le client Android du serveur REST

Dupliquez le projet [exemple-10-client-rest-asynchrone-annuler] dans [exemple-11-client-rest-asynchrones] :

Modifiez les caractéristiques du projet Maven dans [] :

<modelVersion>4.0.0</modelVersion>

<groupId>exemples</groupId>

<artifactId>exemple-11-client-rest-asynchrones</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>apk</packaging>

<name>exemple-11-client-rest-asynchrones</name>

 12.2.1      La couche [DAO]

Activité Android

L'interface [IDao] est actuellement la suivante :

1. package ; 2.

3. import ; 4.

5.    public interface IDao {

6.    public String executeRestService(String method, String urlService, Object request, Map<String, String> paramètres); 7.

8.   public void setTimeout(int millis);

9.   }

La méthode [executeRestService] rend la chaîne JSON envoyée par le serveur REST. Cela nous convient. La couche [DAO] reste inchangée.

 12.2.2      La couche [métier]

Activité Android

L'interface [IMetier] est actuellement la suivante :

1. package .android.metier; 2.

3. import ; 4.

5. import ; 6.

7. public interface IMetier { 8.

9.     public List<Object> getAleas(int a, int b, int n); 10.

11.    public void setDao(IDao dao); 12.

13.    public void setUrlServiceRest(String url); 14.}

La méthode [getAleas] de la ligne 9 ne convient plus. Elle est désormais remplacée par la méthode [getAlea] suivante :

public Object getAlea(int a, int b);

Elle permet d'obtenir soit le nombre aléatoire soit le message d'erreur envoyé par le serveur REST. La classe d'implémentation [Metier] évolue alors comme suit :

1.     public Object getAlea(int a, int b) {

2.     // adresse du service REST

3.     String urlService = String.format("http://%s/{a}/{b}", urlServiceRest);

4.     // paramètres service REST

5.     Map<String, String> paramètres = new HashMap<String, String>();

6.     paramè("a", String.valueOf(a));

7.     paramè("b", String.valueOf(b));

8.     // exécution service [DAO]

9.     String réponse = null;

10.      try {

11.      // exécution service - on récupère un [String]

12.      réponse = dao.executeRestService("get", urlService, null, paramètres);

13.      // on exploite la réponse JSON

14.      Map<String, String> data = new Gson().fromJson(réponse, new TypeToken<Map<String, String>>() {

15.      }.getType());

16.      // on récupère les éléments du dictionnaire

17.      String erreur = ("erreur");

18.      String alea = ("alea"); 19. String msg = ("msg");

20.       // on traite les différents cas

21.       if (erreur != null && erreur.equals("0") && alea != null) {

22.       return Integer.valueOf(alea);

23.       }

24.       if (erreur != null && erreur.equals("1") && msg != null) {

25.       return msg;

26.       }

27.       // pas normal - on lance une exception

28.       throw new AleaException(String.format("Le serveur a renvoyé une réponse invalide [%s]", réponse));

29.       } catch (Exception ex) {

30.       // cas d'erreur - on récupère les msg d'erreur de la pile d'exceptions

31.       List<String> messages = new ArrayList<String>();

32.       Throwable th = ex;

33.       while (th != null) {

34.       (th.getMessage());

35.       th = th.getCause();

36.       }

37.       return messages;

38.       }

39.       }

On se rappelle que la chaîne JSON envoyée par le serveur REST a deux formes :

{"erreur":"0","alea":"127"}

s'il n'y a pas eu d'erreur ou bien

{"erreur":"1","msg":"message de l'exception"}

s'il y a eu une erreur.

Ces chaînes JSON peuvent être transformées en dictionnaires Map<String, String> de clés ["erreur","alea"] pour la première chaîne et ["erreur","msg"] pour la seconde.

•   la chaîne JSON reçue est transformée en dictionnaire ligne 15 ;

•   lignes 18-20 : on récupère les valeurs associée aux trois clés possibles ;

•   lignes 22-24 : le cas où on a récupéré le nombre aléatoire. On rend un type [Integer] ;

•   lignes 25-27 : le cas où on a reçu un message d'erreur. On rend un type [String] ;

•   ligne 29 : si on a reçu ni un nombre aléatoire, ni un message d'erreur alors la réponse est invalide. On lance une exception pour l'indiquer ;

•   ligne 30 : on attrape ici toutes les exceptions qui ont pu se produire dans le catch. On renvoie comme résultat un objet List<String> constitué des messages d'erreur tirés de la pile d'exceptions.

 12.2.3      La tâche asynchrone

La tâche asynchrone [AleaTask] va être appelée de façon répétée. Sa méthode [doInBackground] évolue comme suit :

1.

@Override

2.

protected Void doInBackground(Object params) {

3.

// on est dans un autre thread que celui de l'UI

4.

// on récupère les deux paramètres - on suppose qu'ils sont corrects

5.

int a = (Integer) params[0];

6.

int b = (Integer) params[1];

7.

// on appelle la couche [métier]

8.

info = métier.getAlea(a, b);

9.

// fin

10. 11.}

return null;

•   lignes 5-6 : la vue ne passe plus que deux paramètres à la tâche asynchrone, les valeurs a et b de l'intervalle [a,b] ;

•   ligne 8 : c'est la couche [métier] qui génère le nombre aléatoire ;

 12.2.4      Le fragment [Vue_01]

Tâche asynchrone

Couche

[metier]

Activité Android

Le code de la classe [MainActivity] évolue comme suit :

1.    // les tâches asynchrones

2.    private AsyncTask<?, ?, ?>[] tasks;

3.    private int nbInfos;

4.    // les réponses

5.    List<String> réponses = new ArrayList<String>(); 6.

7.

8.     protected void doExecuter() {

9.     // on efface les éventuels msg d'erreur précédents

10.      txtErrorAleas.setText("");

11.      txtErrorIntervalle.setText("");

12.      // on teste la validité des saisies

13.      if (!isPageValid()) {

14.      return;

15.      }

16.      // on efface les réponses précédentes

17.      listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, new String[] {}));

18.

// on demande à une tâche asynchrone les nombres aléatoires

19.

// on crée les tâches asynchrones

20.

tasks = new AleaTask[nbAleas];

21.

nbInfos = 0;

22.

for (int i = 0; i < nbAleas; i++) {

23.

// on crée la tâche n° i

24.

AleaTask task = new AleaTask();

25.

tasks[i] = task;

26.

// on lui injecte la méthode de rappel

27.

task.setCaller(new ICaller() {

28.

public void callBack(Object info) {

29.

// on traite l'info reçue

30.

showInfo(info);

31.

}

32.

});

33.

// on lui injecte la couche [métier]

34.

task.setMetier(activité.getMétier());

35.

// on l'exécute

36.

task.executeOnExecutor(.AsyncTask.THREAD_POOL_EXECUTOR, a, b);

37.

}

38.

// on commence l'attente

39. 40.}

beginWaiting();

•   ligne 2 : les tâches asynchrone lancées par la vue seront mémorisées dans un tableau afin de pouvoir les annuler ;

•   ligne 3 : on va compter le nombre d'informations rendues par les tâches aléatoires afin de savoir si l'attente est terminée ou non ;

•   ligne 5 : on va cumuler les réponses des différentes tâches dans une liste. Celle-ci sera visualisée par le [ListView] de

l'interface ;

•   ligne 20 : le tableau des tâches est créé ;

•   ligne 21 : on remet à zéro le compteur des informations rendues par les tâches ;

•   lignes 22-37 : on crée les [nbAleas] tâches et on les lance. Chacune va générer un nombre aléatoire ;

La méthode qui affiche l'information reçue évolue comme suit :

1. protected void showInfo(Object info) { 2.     // a-t-on terminé ?

3.    nbInfos++;

4.    if (nbInfos == nbAleas) {

5.    // on termine l'attente

6.    cancelWaiting();

7.    }

8.    if (info instanceof Integer || info instanceof String) {

9.    // on ajoute l'information à la liste des réponses

10.   ré(info.toString()); 11. } else {

12.        if (info instanceof List<?>) {

13.        // liste d'exceptions

14.        for (Object object : (List<?>) info) {

15.        ré(object.toString());

16.        }

17.        } else {

18.        // bizarre

19.        // on ajoute l'information à la liste des réponses

20.        ré("bizarre : " + info.toString());

21.        }

22.        }

23.        // on affiche les réponses

24.        listRéponses.setAdapter(new ArrayAdapter<String>(activité, android.R.layout.simple_list_item_1, .text1, réponses));

25.        }

•   ligne 3 : on a reçu une information. On incrémente leur compteur ;

•   on reçoit trois types d'objets de type [Integer], [String] et [List<String>]. Ligne 9, on s'occupe des deux premiers ;

•   lignes 12-16 : on s'occupe du troisième type ; • ligne 17 : cas improbable

•   ligne 24 : on associe la liste [réponses] au [ListView] pour qu'il l'affiche.

 12.2.5      L'activité [MainActivity]

Tâche asynchrone

Couche

[metier]

Activité Android

L'activité change peu. On doit simplement changer l'URL du serveur REST :

1.

// URL service REST

2.

//final private String URL_SERVICE_REST = "172.19.81.34:8080/exemple-11-server-rest";

3.

final private String URL_SERVICE_REST = "192.168.1.25:8080/exemple-11-server-rest";

Adaptez ces adresses à votre configuration. L'adresse de la ligne 2 est pour l'émulateur de tablette. Celle de la ligne 3 est pour la tablette lorsqu'elle est connectée au réseau wifi. Dans les deux cas, faite [ipconfig] dans une fenêtre DOS pour avoir l'adresse IP de votre PC.

 12.2.6      Exécution

Créez un environnement d'exécution pour le projet [exemple-11-server-rest] et exécutez-le.

 13 Exemple-12 : composants de saisie de données

Nous allons écrire un nouveau projet pour présenter quelques composants usuels dans les formulaires de saisie de données :

 13.1

Le projet Android

Construisez un projet Maven Android [exemple-12-formulaire]. Achevé, il ressemblera à ceci :

 13.2

La vue XML du formulaire

La vue XML du formulaire est dans [] :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <ScrollView xmlns:android=";

3.    android:layout_width="match_parent" 4.    android:layout_height="match_parent" > 5.

6.    <RelativeLayout

7.    android:layout_width="match_parent" 8.        android:layout_height="wrap_content" > 9.

10.   <TextView

11.   android:id="@+id/textViewFormulaireTitre"

12.   android:layout_width="wrap_content"

13.   android:layout_height="wrap_content"

14.   android:layout_alignParentLeft="true"

15.   android:layout_alignParentTop="true"

16.   android:layout_marginLeft="50dp"

17.   android:layout_marginTop="30dp"

18.   android:text="@string/formulaire_titre" 19.            android:textSize="30sp" /> 20.

21.   <Button

22.   android:id="@+id/formulaireButtonValider"

23.   android:layout_width="wrap_content"

24.   android:layout_height="wrap_content"

25.   android:layout_alignLeft="@+id/TextViewFormulaireCombo"

26.   android:layout_below="@+id/TextViewFormulaireCombo"

27.   android:layout_marginTop="50dp"

28.   android:text="@string/formulaire_valider" /> 29.

30.   <TextView

31.   android:id="@+id/textViewFormulaireCheckBox"

32.   android:layout_width="wrap_content"

33.   android:layout_height="wrap_content"

34.   android:layout_alignLeft="@+id/textViewFormulaireTitre"

35.   android:layout_below="@+id/textViewFormulaireTitre"

36.   android:layout_marginTop="30dp"

37.   android:text="@string/formulaire_checkbox" 38. android:textSize="20sp" /> 39.

40.   <TextView

41.   android:id="@+id/textViewFormulaireRadioButton"

42.   android:layout_width="wrap_content"

43.   android:layout_height="wrap_content"

44.   android:layout_alignLeft="@+id/textViewFormulaireCheckBox"

45.   android:layout_below="@+id/textViewFormulaireCheckBox"

46.   android:layout_marginTop="30dp"

47.   android:text="@string/formulaire_radioButton" 48. android:textSize="20sp" /> 49.

50.        <TextView

51.   android:id="@+id/textViewFormulaireSeekBar"

52.   android:layout_width="wrap_content"

53.   android:layout_height="wrap_content"

54.   android:layout_alignLeft="@+id/textViewFormulaireRadioButton"

55.   android:layout_below="@+id/textViewFormulaireRadioButton"

56.   android:layout_marginTop="30dp"

57.   android:text="@string/formulaire_seekBar" 58. android:textSize="20sp" /> 59.

60.   <TextView

61.   android:id="@+id/textViewFormulaireEdtText"

62.   android:layout_width="wrap_content"

63.   android:layout_height="wrap_content"

64.   android:layout_alignLeft="@+id/textViewFormulaireSeekBar"

65.   android:layout_below="@+id/textViewFormulaireSeekBar"

66.   android:layout_marginTop="30dp"

67.   android:text="@string/formulaire_saisie" 68. android:textSize="20sp" /> 69.

70.   <TextView

71.   android:id="@+id/textViewFormulaireBool"

72.   android:layout_width="wrap_content"

73.   android:layout_height="wrap_content"

74.   android:layout_alignLeft="@+id/textViewFormulaireEdtText"

75.   android:layout_below="@+id/textViewFormulaireEdtText"

76.   android:layout_marginTop="30dp"

77.   android:text="@string/formulaire_bool" 78.            android:textSize="20sp" /> 79.

80.   <TextView

81.   android:id="@+id/textViewFormulaireDate"

82.   android:layout_width="wrap_content"

83.   android:layout_height="200dp"

84.   android:layout_alignLeft="@+id/textViewFormulaireBool"

85.   android:layout_below="@+id/textViewFormulaireBool"

86.   android:layout_marginTop="50dp"

87.   android:gravity="center"

88.   android:text="@string/formulaire_date" 89.            android:textSize="20sp" /> 90.

91.   <TextView

92.   android:id="@+id/textViewFormulaireMultilignes"

93.   android:layout_width="150dp"

94.   android:layout_height="wrap_content"

95.   android:layout_alignBaseline="@+id/textViewFormulaireTitre"

96.   android:layout_alignParentTop="true"

97.   android:layout_marginLeft="400dp"

98.   android:layout_toRightOf="@+id/textViewFormulaireTitre"

99.   android:text="@string/formulaire_multilignes" 100. android:textSize="20sp" /> 101.

102.    <TextView

103.    android:id="@+id/textViewFormulaireTime"

104.    android:layout_width="wrap_content"

105.    android:layout_height="200dp"

106.    android:gravity="center"

107.    android:layout_alignLeft="@+id/textViewFormulaireMultilignes"

108.    android:layout_below="@+id/textViewFormulaireMultilignes"

109.    android:layout_marginTop="50dp"

110.    android:text="@string/formulaire_time" 111.            android:textSize="20sp" /> 112.

113.        <TextView

114.

            android:id="@+id/TextViewFormulaireCombo"

115.

            android:layout_width="wrap_content"

116.

            android:layout_height="wrap_content"

117.

            android:layout_alignLeft="@+id/textViewFormulaireTime"

118.

            android:layout_below="@+id/textViewFormulaireTime"

119.

            android:layout_marginTop="50dp"

120.

            android:text="@string/formulaire_combo"

121.

            android:textSize="20sp" />

122. 123.

        <CheckBox

124.

            android:id="@+id/formulaireCheckBox1"

125.

            android:layout_width="wrap_content"

126.

            android:layout_height="wrap_content"

127.

            android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"

128.

            android:layout_marginLeft="100dp"

129.

            android:layout_toRightOf="@+id/textViewFormulaireCheckBox"

130.

            android:text="@string/formulaire_checkbox1" />

131. 132.

        <RadioGroup

133.

            android:id="@+id/formulaireRadioGroup"

134.

            android:layout_width="wrap_content"

135.

            android:layout_height="wrap_content"

136.

            android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"

137.

            android:layout_alignLeft="@+id/formulaireCheckBox1"

138.

            android:orientation="horizontal" >

139. 140.

            <RadioButton

141.

                android:id="@+id/formulaireRadioButton1"

142.

                android:layout_width="wrap_content"

143.

                android:layout_height="wrap_content"

144.

                android:text="@string/formulaire_radiobutton1" />

145. 146.

            <RadioButton

147.

                android:id="@+id/formulaireRadioButton2"

148.

                android:layout_width="wrap_content"

149.

                android:layout_height="wrap_content"

150.

                android:text="@string/formulaire_radionbutton2" />

151. 152.

            <RadioButton

153.

                android:id="@+id/formulaireRadionButton3"

154.

                android:layout_width="wrap_content"

155.

                android:layout_height="wrap_content"

156.

                android:text="@string/formulaire_radiobutton3" />

157.

        </RadioGroup>

158. 159.

        <SeekBar

160.

            android:id="@+id/formulaireSeekBar"

161.

            android:layout_width="300dp"

162.

            android:layout_height="wrap_content"

163.

            android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"

164.

            android:layout_alignLeft="@+id/formulaireCheckBox1" />

165. 166.

        <EditText

167.

            android:id="@+id/formulaireEditText1"

168.

            android:layout_width="wrap_content"

169.

            android:layout_height="wrap_content"

170.

            android:layout_alignBaseline="@+id/textViewFormulaireEdtText"

171.

            android:layout_alignLeft="@+id/formulaireCheckBox1"

172.

            android:ems="10"

173.

            android:inputType="text" >

174.

        </EditText>

175. 176.

        <Switch

177.

            android:id="@+id/formulaireSwitch1"

178.

            android:layout_width="wrap_content"

179.

            android:layout_height="wrap_content"

180.

            android:layout_alignBaseline="@+id/textViewFormulaireBool"

181.

            android:layout_alignLeft="@+id/formulaireCheckBox1"

182.

            android:text="@string/formulaire_switch"

183.

            android:textOff="Non"

184.

            android:textOn="Oui" />

185. 186.

        <TimePicker

187.

            android:id="@+id/formulaireTimePicker1"

188.

            android:layout_width="wrap_content"

189.

            android:layout_height="wrap_content"

190.

            android:layout_alignBottom="@+id/textViewFormulaireTime"

191.

            android:layout_alignLeft="@+id/formulaireEditTextMultiLignes" />

192. 193.

        <EditText

194.

            android:id="@+id/formulaireEditTextMultiLignes"

195.

            android:layout_width="wrap_content"

196.

            android:layout_height="wrap_content"

197.

            android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"

198.

            android:layout_alignBottom="@+id/textViewFormulaireMultilignes"

199.

            android:layout_marginLeft="100dp"

200.

            android:layout_toRightOf="@+id/textViewFormulaireMultilignes"

201.

            android:ems="10"

202.

            android:inputType="textMultiLine" >

203.

        </EditText>

204. 205.

        <Spinner

206.

            android:id="@+id/formulaireDropDownList"

207.

            android:layout_width="200dp"

208.

            android:layout_height="50dp"

209.

            android:layout_alignBottom="@+id/TextViewFormulaireCombo"

210.

            android:layout_alignLeft="@+id/formulaireEditTextMultiLignes" >

211.

        </Spinner>

212. 213.

        <DatePicker

214.

            android:id="@+id/formulaireDatePicker1"

215.

            android:layout_width="wrap_content"

216.

            android:layout_height="wrap_content"

217.

            android:layout_alignBottom="@+id/textViewFormulaireDate"

218.

            android:layout_alignLeft="@+id/formulaireCheckBox1" >

219.

        </DatePicker>

220. 221.

        <TextView

222.

            android:id="@+id/textViewSeekBarValue"

223.

            android:layout_width="30dp"

224.

            android:layout_height="wrap_content"

225.

            android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"

226.

            android:layout_marginLeft="30dp"

227.

            android:layout_toRightOf="@+id/formulaireSeekBar"

228.

            android:text="" />

229.

    </RelativeLayout>

230. 231.

</ScrollView>

Les principaux composants du formulaire sont les suivants :

•   ligne 2 : un layout [ScrollView] vertical. Il permet de présenter un formulaire plus grand que l'écran de la tablette. On obtient la totalité du formulaire par défilement ;

•   lignes 123-130 : une case à cocher      

•   lignes 132-157 : un groupe de trois boutons radio           

•   lignes 159-164 : une barre de recherche             

•   lignes 166-174 : une boîte de saisie    

•   lignes 176-184 : un switch oui / non 

•   lignes 186-191 : une boîte de saisie de l'heure   

•   lignes 193-203 : une boîte de saisie multi-lignes               

•   lignes 205-211 : une liste déroulante  

•   lignes 213-219 : une boîte de saisie d'une date 

•   tous les autres composants sont des [TextView] qui affichent des textes.

 13.3

Les chaînes de caractères du formulaire

Les chaînes de caractères du formulaire sont définies dans le fichier [res / values / ] suivant :

 

1. <?xml version="1.0" encoding="utf-8"?> 2. <resources> 3.

4.    <string name="app_name">exemple-12</string>

5.    <string name="action_settings">Settings</string>

6.    <string name="formulaire_titre">Formulaire</string>

7.    <string name="formulaire_checkbox">Cases à cocher</string>

8.    <string name="formulaire_radioButton">Boutons Radio</string>

9.    <string name="formulaire_seekBar">Seek Bar</string>

10.   <string name="formulaire_saisie">Champ de saisie</string>

11.   <string name="formulaire_bool">Booléen</string>

12.   <string name="formulaire_date">Date</string>

13.   <string name="formulaire_time">Heure</string>

14.   <string name="formulaire_multilignes">Champ de saisie multilignes</string>

15.   <string name="formulaire_listview">Liste</string>

16.   <string name="formulaire_combo">Liste déroulante</string>

17.   <string name="formulaire_checkbox1">1</string>

18.   <string name="formulaire_checkbox2">2</string>

19.   <string name="formulaire_radiobutton1">1</string>

20.   <string name="formulaire_radionbutton2">2</string>

21.   <string name="formulaire_radiobutton3">3</string>

22.   <string name="formulaire_switch"></string>

23.   <string name="formulaire_valider">Valider</string> 24.

25.</resources>

 13.4

Le fragment du formulaire

     

La classe [FormulaireFragment] est la suivante :

1. package .android; 2.

3.    import .ArrayList;

4.    import ; 5.

6.

7.    // un fragment est une vue affichée par un conteneur de fragments

8.    public class FormulaireFragment extends Fragment { 9.

10.   // la vue

11.   private View rootView; 12.

13.   // les champs de la vue affichée par le fragment

14.   private Spinner dropDownList;

15.   private Button buttonValider;

16.   private CheckBox checkBox1;

17.   private RadioGroup radioGroup;

18.   private SeekBar seekBar;

19.   private EditText saisie;

20.   private Switch switch1;

21.   private DatePicker datePicker1;

22.   private TimePicker timePicker1; 23.      private EditText multiLignes; 24.

25.   // l'activité

26.   private MainActivity activité; 27.

28.   @Override

29.   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

30.   // le fragment est associé à la vue [formulaire]

31.   rootView = inflater.inflate(R.layout.formulaire, container, false);

32.   // on récupère l'unique activité

33.   activité = (MainActivity) getActivity();

34.   // on récupère les champs du formulaire

35.   // la case à cocher

36.   checkBox1 = (CheckBox) rootView.findViewById(R.id.formulaireCheckBox1);

37.   // les boutons radio

38.   radioGroup = (RadioGroup) rootView.findViewById(R.id.formulaireRadioGroup);

39.   // on coche le premier bouton

40.   RadioButton radioButton1 = (RadioButton) rootView.findViewById(R.id.formulaireRadioButton1);

41.   radioButton1.setChecked(true);

42.   // le seekBar

43.   seekBar = (SeekBar) rootView.findViewById(R.id.formulaireSeekBar);

44.   seekBar.setMax(100);

45.   final TextView seekBarValue = (TextView) rootView.findViewById(R.id.textViewSeekBarValue);

46.   seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { 47.

48.      public void onStopTrackingTouch(SeekBar seekBar) {

49.      }

50.

51.      public void onStartTrackingTouch(SeekBar seekBar) {

52.      }

53.

54.       public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

55.       seekBarValue.setText(String.valueOf(progress));

56.       }

57.       });

58.       // le champ de saisie

59.       saisie = (EditText) rootView.findViewById(R.id.formulaireEditText1);

60.       // le switch

61.       switch1 = (Switch) rootView.findViewById(R.id.formulaireSwitch1);

62.       // la date

63.   datePicker1 = (DatePicker) rootView.findViewById(R.id.formulaireDatePicker1);

64.   datePicker1.setCalendarViewShown(false);

65.   // le champ de saisie multilignes

66.   multiLignes = (EditText) rootView.findViewById(R.id.formulaireEditTextMultiLignes);

67.   // l'heure

68.   timePicker1 = (TimePicker) rootView.findViewById(R.id.formulaireTimePicker1);

69.   // la liste déroulante

70.   dropDownList = (Spinner) rootView.findViewById(R.id.formulaireDropDownList);

71.   List<String> list = new ArrayList<String>();

72.   ("list 1"); 73.    ("list 2");

74.       ("list 3");

75.       ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(activité, android.R.layout.simple_spinner_item, list);

76.       dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

77.       dropDownList.setAdapter(dataAdapter);

78.       // le bouton

79.       buttonValider = (Button) rootView.findViewById(R.id.formulaireButtonValider);

80.       buttonValider.setOnClickListener(new OnClickListener() {

81.       public void onClick(View arg0) {

82.       doValider();

83.       }

84.       });

85.

86.   // on retourne la vue créée

87.   return rootView;

88.   }

89.

90.    @SuppressLint("DefaultLocale") 91.       protected void doValider() {

92 .

93.   }

94.}

•   lignes 29-88 : dans la méthode [onCreateView] on récupère les références de tous les composants du formulaire XML [formulaire] (ligne 31) ;

•   ligne 41 : la méthode [setChecked] permet de cocher un bouton radio ou une case à cocher ;

•   ligne 44 : [SeekBar].setMax() permet de fixer la valeur maximale de la barre de réglage. La valeur minimale est 0 ;

•   ligne 46 : on gère les événements de la barre de réglage. On veut, à chaque changement opéré par l'utilisateur, afficher la valeur de la règle dans le [TextView] de la ligne 45 ;

•   ligne 54 : le paramètre [progress] représente la valeur de la règle ;

•   ligne 64 : par défaut le composant [DatePicker] affiche et une boîte de saisie de la date et un calendrier. La ligne 64 élimine le calendrier ;

•   lignes 71-74 : une liste de [String] qu'on va associer à une liste déroulante ;

•   lignes 75-77 : cette liste est associée à la liste déroulante ;

•   lignes 80-84 : on associe la méthode [doValider] au clic sur le bouton [Valider] ;

La méthode [doValider] a pour but d'afficher les valeurs saisies par l'utilisateur. Son code est le suivant :

1.

protected void doValider() {

2.

// liste des messages à afficher

3.

List<String> messages = new ArrayList<String>();

4.

// case à cocher

5.

boolean isChecked = checkBox1.isChecked();

6.

(String.format("CheckBox1 [checked=%s]", isChecked));

7.

// les boutons radio

8.

int id = radioGroup.getCheckedRadioButtonId();

9.

String radioGroupText = id == -1 ? "" : ((RadioButton) rootView.findViewById(id)).getText().toString();

10.

(String.format("RadioGroup [checked=%s]", radioGroupText));

11.

// le SeekBar

12.

int progress = seekBar.getProgress();

13.

(String.format("SeekBar [value=%d]", progress));

14.   // le champ de saisie

15.   String texte = String.valueOf(saisie.getText());

16.   (String.format("Saisie simple [value=%s]", texte));

17.   // le switch

18.   boolean état = switch1.isChecked();

19.   (String.format("Switch [value=%s]", état));

20.   // la date

21.   int an = datePicker1.getYear();

22.   int mois = datePicker1.getMonth() + 1;

23.   int jour = datePicker1.getDayOfMonth();

24.   (String.format("Date [%d, %d, %d]", jour, mois, an));

25.   // le texte multi-lignes

26.   String lignes = String.valueOf(multiLignes.getText());

27.   (String.format("Saisie multi-lignes [value=%s]", lignes));

28.   // l'heure

29.   int heure = timePicker1.getCurrentHour();

30.   int minutes = timePicker1.getCurrentMinute();

31.   (String.format("Heure [%d, %d]", heure, minutes));

32.   // liste déroulante

33.   int position = dropDownList.getSelectedItemPosition();

34.   String selectedItem = String.valueOf(dropDownList.getSelectedItem());

35.   (String.format("DropDownList [position=%d, item=%s]", position, selectedItem));

36.   // affichage

37.   doAfficher(messages);

38.   }

39.

40.   private void doAfficher(List<String> messages) {

41 .

42.   }

•      ligne 3 : les valeurs saisies vont être cumulées dans une liste de messages ;

•      ligne 5 : la méthode [CheckBox].isCkecked() permet de savoir si une case est cochée ou non ;

•      ligne 8 : la méthode [RadioGroup].getCheckedButtonId() permet d'obtenir l'id du bouton radio qui a été coché ou -1 si aucun n'a été coché ;

•      ligne 9 : le code [rootView.findViewById(id)] permet de retrouver le bouton radio coché et d'avoir ainsi son libellé ;

•      ligne 12 : la méthode [SeekBar].getProgress()] permet d'avoir la valeur d'une barre de réglage ;

•      ligne 18 : la méthode [Switch].isChecked() permet de savoir si un switch est On (true) ou Off (false) ;

•      ligne 21 : la méthode [DatePicker].getYear() permet d'avoir l'année choisie avec un objet [DatePicker] ;

•      ligne 22 : la méthode [DatePicker].getMonth() permet d'avoir le mois choisi avec un objet [DatePicker] dans l'intervalle [0,11] ;

•      ligne 23 : la méthode [DatePicker].getDayOfMonh() permet d'avoir le jour du mois choisi avec un objet [DatePicker] dans

l'intervalle [1,31] ;

•      ligne 29 : la méthode [TimePicker].getCurrentHour() permet d'avoir l'heure choisie avec un objet [TimePicker] ;

•      ligne 30 : la méthode [TimePicker].getCurrentMinute() permet d'avoir les minutes choisies avec un objet [TimePicker] ;

•      ligne 33 : la méthode [Spinner].getSelectedItemPosition() permet d'avoir la position de l'élément sélectionné dans une liste déroulante ;

•      ligne 34 : la méthode [Spinner].getSelectedItem() permet d'avoir l'objet sélectionné dans une liste déroulante ;

La méthode [doAfficher] qui affiche la liste des valeurs saisies est la suivante :

1.

private void doAfficher(List<String> messages) {

2.

// on construit le texte à affiche

3.

StringBuilder texte = new StringBuilder();

4.

for (String message : messages) {

5.

texte.append(String.format("%s\n", message));

6.

}

7.

// on l'affiche

8.

new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();

9.

}

•      ligne 1 : la méthode reçoit une liste de messages à afficher ;

•      lignes 3-6 : un objet [StringBuilder] est construit à partir de ces messages. Pour concaténer des chaînes, le type

[StringBuilder] est plus efficace que le type [String] ; •    ligne 8 : une boîte de dialogue affiche le texte de la ligne 3 :

•      ligne 8 :

•      setTitle affiche [1],

•      setMessage affiche [2],

•      setNeutralButton affiche [3]. Le 1er paramète est le libellé du bouton, le second une référence sur le gestionnaire du clic sur ce bouton. Ici nous n'en avons pas. Un clic sur le bouton fermera simplement la boîte de dialogue ;

 13.5

L'activité [MainActivity]

La classe [MainActivity] reste ce qu'elle était dans les exemples précédents à quelques détails près :

1. package .android; 2.

3. import .Locale; 4.

5.

6. public class MainActivity extends FragmentActivity { 7.

8.     // le gestionnaire de fragments ou sections 9.     SectionsPagerAdapter mSectionsPagerAdapter; 10.

11.    // le conteneur des fragments 12. MyPager mViewPager; 13.

14.  @Override

15.  protected void onCreate(Bundle savedInstanceState) {

16 .

17.   }

18.

19.   // navigation

20.   public void navigateToView(int i) { 21 .

22.   }

23.

24.   // notre gestionnaire de fragments

25.   // à redéfinir pour chaque application

26.   // doit définir les méthodes suivantes

27.   // getItem, getCount, getPageTitle

28.   public class SectionsPagerAdapter extends FragmentPagerAdapter { 29.

30.   // les fragments

31.   Fragment[] fragments = { new FormulaireFragment() }; 32.

33.      // constructeur

34.

public SectionsPagerAdapter(FragmentManager fm) {

35.

super(fm);

36.

}

37. 38.

// doit rendre le fragment n° i avec ses éventuels arguments

39.

@Override

40.

public Fragment getItem(int position) {

41.

// on rend le fragment

42.

return fragments[position];

43.

}

44. 45.

// rend le nombre de fragments à gérer

46.

@Override

47.

public int getCount() {

48.

// 1 fragments

49.

return 1;

50.

}

51. 52.

// rend le titre du fragment n° position

53.

@Override

54.

public CharSequence getPageTitle(int position) {

55.

Locale l = Locale.getDefault();

56.

switch (position) {

57.

case 0:

58.

return getString(R.string.formulaire_titre).toUpperCase(l);

59.

}

60.

return null;

61.

}

62.

63.

64.}

}

Les modifications sont :

•      ligne 31 : on instancie le fragment du formulaire ;

•      ligne 58 : le titre du fragment [formulaire] ;

 14 Exemple-13 : utilisation d'un patron de vues

L'application [exemple-13-template] est obtenue par recopie du projet [exemple-06]. Les deux projets sont identiques si ce n'est qu'on change l'apparence des vues :

Chacune des deux vues est structurée de la même façon :

•   en [1], un entête ;

•   en [2], une colonne de gauche qui pourrait contenir des liens ; •   en [3], un bas de page ;

•   en [4], un contenu.

Ceci est obtenu en modifiant la vue de base [] de l'activité ;

Le code XML de la vue [activity_main] est le suivant :

1.    <LinearLayout xmlns:android=";

2.    xmlns:tools=";

3.    android:layout_width="match_parent"

4.    android:layout_height="match_parent"

5.    android:gravity="center"

6.    android:orientation="vertical" > 7.

8.    <LinearLayout

9.    android:id="@+id/header"

10.   android:layout_width="match_parent"

11.   android:layout_height="100dp"

12.   android:layout_weight="0.1"

13.   android:background="@color/lavenderblushh2" > 14.

15.   <TextView

16.   android:id="@+id/textViewHeader"

17.   android:layout_width="match_parent"

18.   android:layout_height="wrap_content"

19.   android:layout_gravity="center"

20.   android:gravity="center_horizontal"

21.   android:text="@string/txt_header"

22.   android:textAppearance="?android:attr/textAppearanceLarge"

23.   android:textColor="@color/red" /> 24. </LinearLayout> 25.

26.   <LinearLayout

27.   android:layout_width="match_parent"

28.   android:layout_height="fill_parent"

29.   android:layout_weight="0.8" 30.        android:orientation="horizontal" > 31.

32.   <LinearLayout

33.   android:id="@+id/left"

34.   android:layout_width="100dp"

35.   android:layout_height="match_parent" 36.            android:background="@color/lightcyan2" > 37.

38.   <TextView

39.   android:id="@+id/txt_left"

40.   android:layout_width="fill_parent"

41.   android:layout_height="fill_parent"

42.   android:gravity="center_vertical|center_horizontal"

43.   android:text="@string/txt_left"

44.   android:textAppearance="?android:attr/textAppearanceLarge"

45.   android:textColor="@color/red" /> 46. </LinearLayout> 47.

48.   <.android.MyPager

49.   xmlns:android=";

50.   xmlns:tools=";

51.   android:id="@+id/pager"

52.   android:layout_width="match_parent"

53.   android:layout_height="match_parent"

54.   android:layout_marginLeft="20dp"

55.   android:background="@color/floral_white"

56.   tools:context=".MainActivity" /> 57. </LinearLayout> 58.

59.      <LinearLayout

60.      android:id="@+id/bottom"

61.      android:layout_width="match_parent"

62.      android:layout_height="100dp"

63.      android:layout_weight="0.1"

64.        android:background="@color/wheat1" > 65.

66.   <TextView

67.   android:id="@+id/textViewBottom"

68.   android:layout_width="fill_parent"

69.   android:layout_height="fill_parent"

70.   android:gravity="center_vertical|center_horizontal"

71.   android:text="@string/txt_bottom"

72.   android:textAppearance="?android:attr/textAppearanceLarge"

73.   android:textColor="@color/red" /> 74. </LinearLayout> 75.

76.</LinearLayout>

•   l'entête [1] est obtenu avec les lignes 8-24 ;

•   la bande gauche [2] est obtenue avec les lignes 32-46 ;

•   le bas de page [3] est obtenu avec les lignes 59-74 ;

•   le contenu [4] est obtenu avec les lignes 48-57. On notera le nom [pager] de ce conteneur (ligne 51).

Maintenant rappelons le code dans l'activité [MainActivity] qui affiche une vue :

1.

// instanciation de notre gestionnaire de fragments

2.

mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

3. 4.

// on récupère la référence du conteneur de fragments

5.

mViewPager = (MyPager) findViewById(R.id.pager);

6.

// il est associé à notre gestionnaire de fragments

7.

mViewPager.setAdapter(mSectionsPagerAdapter);

8.

// on inhibe le swipe

9.

mViewPager.setSwipeEnabled(false);

•   ligne 5 : le composant d'id [pager] est le conteneur qui recevra les vues gérées par le [ViewPager]. Le reste (entête, bande gauche, bas de page) est conservé.

On a là une méthode analogue à celle des templates des facelets de JSF2 (Java Server Faces).

La vue XML [activity_main] utilise des informations trouvées dans les fichiers [res / values / ] et [res / values / ] :

Le fichier [] est le suivant :

1.

<?xml version="1.0" encoding="utf-8"?>

2.

<resources>

3. 4.

    <color name="red">#FF0000</color>

5.

    <color name="blue">#0000FF</color>

6.

    <color name="wheat">#FFEFD5</color>

7.

    <color name="floral_white">#FFFAF0</color>

8.

    <color name="lavenderblushh2">#EEE0E5</color>

9.    <color name="lightcyan2">#D1EEEE</color> 10.    <color name="wheat1">#FFE7BA</color> 11.

12.</resources>

et le fichier [] le suivant :

1. <?xml version="1.0" encoding="utf-8"?> 2. <resources> 3.

4.    <string name="app_name">exemple-13</string>

5.    <string name="action_settings">Settings</string>

6.    <string name="vue1_titre">Vue n° 1</string>

7.    <string name="textView_nom">Quel est votre nom :</string>

8.    <string name="btn_Valider">Validez</string> 9.    <string name="btn_vue2">Vue n° 2</string>

10.   <string name="vue2_titre">Vue n° 2</string>

11.   <string name="btn_vue1">Vue n° 1</string>

12.   <string name="textView_bonjour">"Bonjour "</string>

13.   <string name="txt_header">Header</string>

14.   <string name="txt_left">Left</string> 15.    <string name="txt_bottom">Bottom</string>

16.   

17.</resources>


 

 15 Exemple-14 : le composant [ListView]

Le composant [ListView] permet de répéter une vue particulière pour chaque élément d'une liste. La vue répétée peut être d'une complexité quelconque, d'une simple chaîne de caractères à une vue permettant de saisir des informations pour chaque élément de la liste. Nous allons créer le [ListView] suivant :

Chaque vue de la liste a trois composants :

•   un [TextView] d'information ;

•   un [CheckBox] ;

•   un [TextView] cliquable ;

 15.1

Le projet Eclipse

Le projet [exemple-14-listview] est obtenue par recopie du projet précédent [exemple-13-template] :

Les éléments nouveaux de ce projet sont :

•   la vue XML [list_data] [1] qui est la vue répétée par le composant [ListView] ;

•   la classe [ListAdapter] [2] qui est le code Java associé à la vue XML [list_data] ;

Le fichier [] est adapté au nouveau projet :

 

1.

<modelVersion>4.0.0</modelVersion>

 

2.

<groupId>exemples</groupId>

 

3.

<artifactId>exemple-14-listview</artifactId>

 

4.

<version>0.0.1-SNAPSHOT</version>

 

5.

<packaging>apk</packaging>

 

6.

<name>exemple-14-listview</name>

 15.2

La vue [Vue1] initiale

       

La vue XML [] affiche la zone [1] ci-dessus. Son code est le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    android:layout_width="match_parent" 4.    android:layout_height="match_parent" > 5.

6.    <TextView

7.    android:id="@+id/textView_titre"

8.    android:layout_width="wrap_content"

9.    android:layout_height="wrap_content"

10.   android:layout_alignParentLeft="true"

11.   android:layout_alignParentTop="true"

12.   android:layout_marginLeft="88dp"

13.   android:layout_marginTop="26dp"

14.   android:text="@string/vue1_titre" 15.        android:textSize="50sp" /> 16.

17.      <Button

18.      android:id="@+id/button_vue2"

19.      android:layout_width="wrap_content"

20.      android:layout_height="wrap_content"

21.      android:layout_alignLeft="@+id/listView1"

22.      android:layout_below="@+id/listView1"

23.      android:layout_marginTop="50dp"

24.        android:text="@string/btn_vue2" /> 25.

26.   <ListView

27.   android:id="@+id/listView1"

28.   android:layout_width="600dp"

29.   android:layout_height="200dp"

30.   android:layout_alignParentLeft="true"

31.   android:layout_below="@+id/textView_titre"

32.   android:layout_marginLeft="30dp"

33.   android:layout_marginTop="50dp" > 34.    </ListView> 35.

36.</RelativeLayout>

•   lignes 6-15 : le composant [TextView] [2] ;

•   lignes 26-34 : le composant [ListView] [3] ;

•   lignes 17-24 : le composant [Button] [4] ;

 15.3

La vue répétée par le [ListView]

La vue répétée par le [ListView] est la vue [list_data] suivante :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    android:id="@+id/RelativeLayout1"

4.    android:layout_width="match_parent"

5.    android:layout_height="match_parent" 6.    android:background="@color/wheat" > 7.

8.    <TextView

9.    android:id="@+id/txt_Libellé"

10.   android:layout_width="100dp"

11.   android:layout_height="wrap_content"

12.   android:layout_marginLeft="20dp"

13.   android:layout_marginTop="20dp" 14.        android:text="@string/txt_dummy" /> 15.

16.   <CheckBox

17.   android:id="@+id/checkBox1"

18.   android:layout_width="wrap_content"

19.   android:layout_height="wrap_content"

20.   android:layout_alignBottom="@+id/txt_Libellé"

21.   android:layout_marginLeft="37dp"

22.   android:layout_toRightOf="@+id/txt_Libellé" 23. android:text="@string/txt_dummy" /> 24.

25.      <TextView

26.      android:id="@+id/textViewRetirer"

27.      android:layout_width="wrap_content"

28.      android:layout_height="wrap_content"

29.      android:layout_alignBaseline="@+id/txt_Libellé"

30.      android:layout_alignBottom="@+id/txt_Libellé"

31.      android:layout_marginLeft="68dp"

32.      android:layout_toRightOf="@+id/checkBox1"

33.      android:text="@string/txt_retirer"

34.        android:textColor="@color/blue" 35. android:textSize="20sp" /> 36.

37.</RelativeLayout>

•   lignes 8-14 : le composant [TextView] [1] ;

•   lignes 16-23 : le composant [CheckBox] [2] ;

•   lignes 25-35 : le composant [TextView] [3] ;

 15.4

Le fragment [Vue1Fragment]

Le fragment [Vue1Fragment] gère la vue XML [vue1]. Son code est le suivant :

1. package .android; 2.

3. import ; 4.

5.

6.    // un fragment est une vue affichée par un conteneur de fragments

7.    public class Vue1Fragment extends Fragment { 8.

9.    // les champs de la vue affichée par le fragment

10.   private ListView listView;

11.   private Button btnVue2;

12.   // l'activité

13.   private MainActivity activité;

14.   // l'adaptateur de liste 15.      private ListAdapter adapter; 16.

17.       @Override

18.       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

19.       // le fragment est associé à la vue [vue1]

20.       View rootView = inflater.inflate(R.layout.vue1, container, false);

21.       // on récupère les composants de la vue

22.       listView = (ListView) rootView.findViewById(R.id.listView1);

23.       btnVue2 = (Button) rootView.findViewById(R.id.button_vue2);

24.       // gestionnaire d'évts

25.       // bouton [Vue2]

26.       btnVue2.setOnClickListener(new OnClickListener() {

27.       public void onClick(View arg0) {

28.       // on passe à la vue n° 2

29.       navigateToView2();

30.       }

31.       });

32.       // on récupère l'unique activité

33.       activité = (MainActivity) getActivity();

34.       // on associe des données au [ListView]

35.       adapter = new ListAdapter(activité, R.layout.list_data, activité.getListe(), this);

36.       listView.setAdapter(adapter);

37.       // on retourne la vue créée

38.       return rootView;

39.   }

40.

41.   private void navigateToView2() {

42.   // on navigue vers la vue 2

43.   activité.navigateToView(1);

44.   }

45.

46.   public void doRetirer(int position) {

47 .

48.   }

49.}

Nous ne commentons que ce qui est nouveau :

•      ligne 20 : la vue XML [vue1] est associée au fragment ;

•      ligne 22 : on récupère une référence sur le composant [ListView] de [vue1] ;

•      ligne 36 : on associe à ce [ListView] une source de données de type [ListAdapter] (ligne 35). Nous allons construire cette classe. Elle dérive de la classe [ArrayAdapter] que nous avons déjà eu l'occasion d'utiliser pour associer des données à un [ListView] ;

•      ligne 35 : nous passons diverses informations au constructeur de [ListAdapter] :

•      une référence sur l'activité courante,

•      l'id de la vue qui sera instanciée pour chaque élément de la liste,

•      une source de données pour alimenter la liste,

•      une référence sur le fragment. Celle-ci sera utilisée pour faire gérer le clic sur un lien [Retirer] du [ListView] par la méthode [doRetirer] de la ligne 46 ;

La source de données est définie dans [MainActivity] ;

1.

// une liste de données

2.

private List<Data> liste;

3. 4.

// constructeur

5.

public MainActivity() {

6.

// on crée une liste de données

7.

liste = new ArrayList<Data>();

8.

for (int i = 0; i < 20; i++) {

9.

(new Data("Texte n° " + i, false));

10. 11.}

}

Le constructeur de la classe [MainActivity] crée 20 données du type [Data] suivant :

1.

package .android;

2. 3.

public class Data {

4. 5.

// données

6.

private String texte;

7.

private boolean isChecked;

8. 9.

// constructeur

10.

public Data(String texte, boolean isCkecked) {

11.

this.texte = texte;

12.

this.isChecked = isCkecked;

13.

}

14. 15.

// getters et setters

16.

public String getTexte() {

17.

return texte;

18.

}

19. 20.

public void setTexte(String texte) {

21.

this.texte = texte;

22.

}

23. 24.

public boolean isChecked() {

25.

return isChecked;

26.

}

27. 28.

public void setChecked(boolean isChecked) {

29.

this.isChecked = isChecked;

30.

31.

32.}

}

     

•      ligne 6 : le texte qui va alimenter le premier [TextView] de [list_data] ;

•      ligne 7 : le booléen qui va servir à cocher ou non le [checkBox] de [list_data] ;

 15.5

L'adaptateur [ListAdapter] du [ListView]

La classe [ListAdapter]

•      configure la source de données du [ListView] ;

•      gère l'affichage des différents éléments du [ListView] ;

•      gère les événements de ces éléments ;

Son code est le suivant :

1.

package .android;

2. 3.

import ;

4.

5.

public class ListAdapter extends ArrayAdapter<Data> {

6. 7.

// le contexte d'exécution

8.

private Context context;

9.

// l'id du layout d'affichage d'une ligne de la liste

10.

private int layoutResourceId;

11.

// les données de la liste

12.

private List<Data> data;

13.

// le fragment qui affiche le [ListView]

14.

private Vue1Fragment fragment;

15.

// l'adapteur

16.    final ListAdapter adapter = this; 17.

18.   // constructeur

19.   public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {

20.   super(context, layoutResourceId, data);

21.   // on mémorise les infos

22.   this.context = context;

23.   this.layoutResourceId = layoutResourceId;

24.   this.data = data;

25.   this.fragment = fragment;

26.   }

27.

28.  @Override

29.  public View getView(final int position, View convertView, ViewGroup parent) {

30 .

31.   }

32.}

•      ligne 5 : la classe [ListAdapter] étend la classe [ArrayAdapter] ;

•      ligne 19 : le constructeur ;

•      ligne 20 : ne pas oublier d'appeler le constructeur de la classe parent [ArrayAdapter] avec les trois premiers paramètres ;

•      lignes 22-25 : on mémorise les informations du constructeur ;

•      ligne 29 : la méthode [getView] va être appelée de façon répétée par le [ListView] pour générer la vue de l'élément n° [position]. Le résultat [View] rendu est une référence sur la vue créée.

Le code de la méthode [getView] est le suivant :

1.    @Override

2.    public View getView(final int position, View convertView, ViewGroup parent) {

3.    // on crée la ligne

4.    View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);

5.    // le texte

6.    TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);

7.    textView.setText((position).getTexte());

8.    // la case à cocher

9.    CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);

10.   checkBox.setChecked((position).isChecked());

11.   // le lien [Retirer]

12.   TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);

13.   txtRetirer.setOnClickListener(new OnClickListener() { 14.

15.   public void onClick(View v) {

16.   fragment.doRetirer(position);

17.   }

18.   });

19.   // on gère le clic sur la case à cocher

20.   checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { 21.

22.       public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

23.       (position).setChecked(isChecked);

24.       }

25.       });

26.       // on rend la ligne

27.       return row;

28.}

•      la méthode reçoit trois paramètres. Nous n'allons utiliser que le premier ;

•      ligne 4 : on crée la vue de l'élément n° [position]. C'est la vue [list_data] dont l'id a été passé comme deuxième paramètre au constructeur. Ensuite on procède comme d'habitude. On récupère les références des composants de la vue qu'on vient d'instancier ;

•      ligne 6 : on récupère la référence du [TextView] n° 1 ;

•      ligne 7 : on lui assigne un texte provenant de la source de données qui a été passée comme troisième paramètre au constructeur ;

•      ligne 9 : on récupère la référence du [CheckBox] n° 2 ;

•      ligne 10 : on le coche ou non avec une valeur provenant de la source de données du [ListView] ;

•      ligne 12 : on récupère la référence du [TextView] n° 3 ;

•      lignes 13-18 : on gère le clic sur le lien [Retirer] ;

•      ligne 16 : c'est la méthode [Vue1Fragment].doRetirer qui va gérer ce clic. Il paraît en effet plus logique de faire gérer cet événement par le fragment qui affiche le [ListView]. Il a une vue d'ensemble que n'a pas la classe [ListAdapter]. La référence du fragment [Vue1Fragment] avait été passée comme quatrième paramètre au constructeur de la classe ;

•      lignes 20-25 : on gère le clic sur la case à cocher. L'action faite sur elle est répercutée sur la donnée qu'elle affiche. Ceci pour la raison suivante. Le [ListView] est une liste qui n'affiche qu'une partie de ces éléments. Ainsi un élément de la liste est-il parfois caché, parfois affiché. Lorsque l'élément n° i doit être affiché, la méthode [getView] de la ligne 2 ci-dessus est appelée pour la position n° i. La ligne 10 va recalculer l'état de la case à cocher à partir de la donnée à laquelle elle est liée. Il faut donc que celle-ci mémorise l'état de la case à cocher au fil du temps ;

 15.6

Retirer un élément de la liste

Le clic sur le lien [Retirer] est gérée dans le fragment [Vue1Fragment] par la méthode [doRetirer] suivante :

1.    public void doRetirer(int position) {

2.    // on enlève l'élément n° [position] dans la liste

3.    List<Data> liste = activité.getListe();

4.    liste.remove(position);

5.    // on note la position du scroll pour y revenir

6.    // lire

7.    // []

8.    // position du 1er élément visible complètement ou non

9.    int firstPosition = listView.getFirstVisiblePosition();

10.   // offset Y de cet élément par rapport au haut du ListView

11.   // mesure la hauteur de la partie éventuellement cachée

12.   View v = listView.getChildAt(0);

13.   int top = (v == null) ? 0 : v.getTop();

14.   // on réassocie des données au [ListView]

15.   listView.setAdapter(adapter);

16.   // on se positionne au bon endroit du ListView 17.     listView.setSelectionFromTop(firstPosition, top); 18.

19.}

•      ligne 1 : on reçoit la position dans le [ListView] du lien [Retirer] qui a été cliqué ;

•      ligne 3 : on récupère la liste de données ;

•      ligne 4 : on retire l'élément de n° [position] ;

•      ligne 15 : on réassocie les nouvelles données au [ListView]. Sans cela, visuellement rien ne change.

•      lignes 5-13, 17 : une gymnastique assez complexe. Sans elle, il se passe la chose suivante :

•      le [ListView] affiche les lignes 15-18 de la liste de données,

•      on supprime la ligne 16,

•      la ligne 15 ci-dessus le réinitialise alors totalement et le [ListView] affiche alors les lignes 0-3 de la liste de données ;

Avec les lignes ci-dessus, la suppression se fait et le [ListView] reste positionné sur la ligne qui suit la ligne supprimée.

 15.7

La vue XML [Vue2]

Le code XML de la vue est le suivant :

1.    <?xml version="1.0" encoding="utf-8"?>

2.    <RelativeLayout xmlns:android=";

3.    android:layout_width="match_parent" 4.    android:layout_height="match_parent" > 5.

6.    <TextView

7.    android:id="@+id/textView_titre"

8.    android:layout_width="wrap_content"

9.    android:layout_height="wrap_content"

10.   android:layout_alignParentLeft="true"

11.   android:layout_alignParentTop="true"

12.   android:layout_marginLeft="88dp"

13.   android:layout_marginTop="26dp"

14.   android:text="@string/vue2_titre" 15.        android:textSize="50sp" /> 16.

17.   <Button

18.   android:id="@+id/button_vue1"

19.   android:layout_width="wrap_content"

20.   android:layout_height="wrap_content"

21.   android:layout_below="@+id/textViewResultats"

22.   android:layout_marginTop="25dp" 23.        android:text="@string/btn_vue1" /> 24.

25.   <TextView

26.   android:id="@+id/textViewResultats"

27.   android:layout_width="wrap_content"

28.   android:layout_height="wrap_content"

29.   android:layout_below="@+id/textView_titre"

30.   android:layout_marginTop="50dp" 31.        android:text="" /> 32.

33.</RelativeLayout>

•      lignes 6-15 : le composant [TextView] n° 1 ;

•      lignes 25-31 : le composant [TextView] n° 2 ;

•      lignes 17-23 : le composant [Button] n° 3 ;

 15.8

Le fragment [Vue2Fragment]

Le fragment [Vue2Fragment] gère la vue XML [vue2]. Son code est le suivant :

1. package .android; 2.

3. import .Bundle; 4.

5.

6.    // un fragment est une vue affichée par un conteneur de fragments

7.    public class Vue2Fragment extends Fragment { 8.

9.    // les champs de la vue

10.   private Button btnVue1;

11.   private TextView txtRésultats; 12. // l'activité

13.    private MainActivity activité; 14.

15.       @Override

16.       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

17.       // le fragment est associé à la vue [vue2]

18.       View rootView = inflater.inflate(R.layout.vue2, container, false);

19.       // on récupère les composants de la vue

20.       btnVue1 = (Button) rootView.findViewById(R.id.button_vue1);

21.       txtRésultats = (TextView) rootView.findViewById(R.id.textViewResultats);

22.       // gestionnaire d'évts

23.       // bouton [Vue1]

24.       btnVue1.setOnClickListener(new OnClickListener() {

25.       public void onClick(View arg0) {

26.       // on passe à la vue n° 1

27.       navigateToView1();

28.       }

29.       });

30.       // on récupère l'unique activité

31.       activité = (MainActivity) getActivity();

32.       // on retourne la vue créée

33.       return rootView;

34.       }

35.

36.        @Override

37.        public void setMenuVisibility(final boolean visible) {

38.        super.setMenuVisibility(visible);

39.        if (visible) {

40.        // la vue est visible - on affiche les éléments de la liste qui ont été

41.        // sélectionnés

42.        StringBuilder texte = new StringBuilder("Eléments sélectionnés [");

43.        for (Data data : activité.getListe()) {

44.        if (data.isChecked()) {

45.        texte.append(String.format("(%s)", data.getTexte()));

46.        }

47.        }

48.        texte.append("]");

49.

txtRésultats.setText(texte);

50.

}

51.

}

52. 53.

private void navigateToView1() {

54.

// on navigue vers la vue 1

55.

activité.navigateToView(0);

56.

57.

58.}

}

Le code important est dans la méthode [setMenuVisibility] de la ligne 37. Elle est exécutée dès que la vue affichée par le fragment va devenir visible ou cachée à l'utilisateur.

•      ligne 38 : on appelle la méthode de la classe parent. Obligatoire ;

•      ligne 39 : si la vue va devenir visible, alors on calcule le texte à afficher dans le [TextView] n° 2 ;

•      lignes 43-46 : on parcourt la liste des données affichée par le [ListView]. Elle est stockée dans l'activité ;

•      ligne 45 : si la donnée n° i a été cochée, on ajoute le libellé associé dans un type [StringBuilder] ;

•      ligne 49 : le [TextView] affiche le texte calculé ;

 15.9

Exécution

Créez une configuration d'exécution pour ce projet et exécutez-la.

 15.10

Amélioration

Dans l'exemple précédent nous avons utilisé une source de données List<Data> où la classe [Data] était la suivante :

1. package .android; 2.

3. public class Data { 4.

5.    // données

6.    private String texte; 7.   private boolean isChecked; 8.

9.    // constructeur

10.   public Data(String texte, boolean isCkecked) {

11.   this.texte = texte;

12.   this.isChecked = isCkecked;

13.   }

14 . 15.

16.}

Ligne 7, on avait utilisé un booléen pour gérer la case à cocher des éléments du [ListView]. Souvent, le [ListView] doit afficher des données qu'on peut sélectionner en cochant une case sans que pour autant l'élément de la source de données ait un champ booléen correspondant à cette case. On peut alors procéder de la façon suivante :

La classe [Data] devient la suivante :

1.

package .android;

2. 3.

public class Data {

4. 5.

// données

6.

private String texte;

7. 8.

// constructeur

9.

public Data(String texte) {

10.

this.texte = texte;

11.   }

12.

13. // getters et setters 14 .

15.}

On crée une classe [CheckedData] dérivée de la précédente :

1. package .android; 2.

3. public class CheckedData extends Data { 4.

5.    // élément coché

6.    private boolean isChecked; 7.

8.    // constructeur

9.    public CheckedData(String text, boolean isChecked) {

10.   // parent

11.   super(text);

12.   // local

13.   this.isChecked = isChecked;

14.   }

15.

16.   // getters et setters

17 .

18.}

Il suffit ensuite de remplacer partout dans le code, le type [Data] par le type [CheckedData]. Par exemple dans [MainActivity] :

1.

// une liste de données à cocher

2.

private List<CheckedData> liste;

3. 4.

// constructeur

5.

public MainActivity() {

6.

// on crée une liste de données

7.

liste = new ArrayList<CheckedData>();

8.

for (int i = 0; i < 20; i++) {

9.

(new CheckedData("Texte n° " + i, false));

10. 11.}

}

Le projet de cette version vous est fourni sous le nom [exemple-15-listview].

 16 Exemple-16 : utiliser un menu

Nous reprenons le projet [exemple-13-template] que nous dupliquons dans le projet [exemple-16-menu] [1] :

Nous supprimons les boutons des vues 1 et 2 pour les remplacer par des options de menu. En [2] et [3], nous voyons les options de menu de la vue n° 1.

 16.1

La définition XML du menu

Le fichier [res / menu / main] est présent par défaut et contient la définition d'un menu vide. Nous le modifions de la façon suivante :

1. <menu xmlns:android="; > 2.

3.    <item

4.    android:id="@+id/menuActions"

5.    android:showAsAction="ifRoom"

6.    android:title="@string/menuActions">

7.    <menu>

8.    <item

9.    android:id="@+id/actionValider"

10.   android:title="@string/actionValider"/>

11.   </menu>

12.   </item>

13.   <item

14.   android:id="@+id/menuNavigation"

15.   android:showAsAction="ifRoom"

16.   android:title="@string/menuNavigation">

17.   <menu>

18.   <item

19.   android:id="@+id/navigationVue1"

20.   android:title="@string/navigationVue1"/>

21.   <item

22.   android:id="@+id/navigationVue2"

23.   android:title="@string/navigationVue2"/> 24. </menu>

25.    </item> 26.

27.</menu>

Ce menu correspond à la hiérarchie suivante (onglet Layout lorsque le fichier XML est visualisé) :

Les éléments du menu sont définis par les informations suivantes :

•   android:id : l'identifiant de l'élément ;

•   android:title : le libellé de l'élément ;

•   android:showsAsAction : indique si l'élément de menu peut être placé dans la barre d'actions de l'activité. [ifRoom] indique que l'élément doit être placé dans la barre d'actions s'il y a de la place pour lui ;

 16.2

La gestion du menu dans l'activité [MainActivity]

Dans l'activité principale, la gestion du menu se fait de la façon suivante :

1.

@Override

2.

public boolean onCreateOptionsMenu(Menu menu) {

3.

for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {

4.

mSectionsPagerAdapter.getItem(i).setHasOptionsMenu(true);

5.

}

6.

return true;

7.

}

Les fragments vont gérer leur propre menu. Pour indiquer cela, on doit appeler la méthode [Fragment].setHasOptionsMenu(). Ci-dessus, on demande les références des différents fragments à notre gestionnaire de pages qui étend la classe [FragmentPagerAdapter].

 16.3

La gestion du menu dans le fragment [Vue1Fragment]

Dans les fragments, le menu est géré par les méthodes [onCreateOptionsMenu] et [onOptionsItemSelected]. La méthode [onCreateOptionsMenu] est la suivante :

1.

// le menu

2.

private boolean menuDone = false;

3. 4.

@Override

5.

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {

6.

if (!menuDone) {

7.

// on nettoie le menu

8.

menu.clear();

9.

// on le remplit

10.

inflater.inflate(R.menu.main, menu);

11.

// on cache l'option [Navigation / Vue 1]

12.

menu.getItem(1).getSubMenu().getItem(0).setVisible(false);

13.

}

14. 15.}

menuDone = true;

     

•   c'est la ligne 10 qui crée le menu à partir de sa définition XML ;

•   ligne 12 : l'option [Vue 1] du sous-menu [Navigation] est cachée. On ne garde que les options [Actions / Valider] et [Navigation / Vue 2] ;

•   la méthode de la ligne 5 n'est appelée que si la méthode [Fragment].setHasOptionsMenu() a été appelée auparavant. On a vu que c'était l'activité qui l'avait fait. On constate aux tests que la méthode [onCreateOptionsMenu] est appelée plusieurs fois et que les options se cumulent alors dans le menu. Aussi utilise-t-on la variable [menuDone] de la ligne 2 pour éviter ce phènomène ;

•   ligne 8 : le menu est vidé de tous ses éléments avant d'être régénéré ;

Note : la solution utilisée ici n'est pas optimale. Le menu devrait être généré une unique fois par l'activité, les fragments se contentant de cacher les options qui ne les concernent pas et d'afficher celles qui les concernent.

La méthode [onOptionsItemSelected] gère les clics sur les options du menu :

1.

@Override

2.

public boolean onOptionsItemSelected(MenuItem item) {

3.

// on gère l'action du menu

4.

switch (item.getItemId()) {

5.

case R.id.navigationVue2:

6.

menuDone=false;

7.

navigateToView2();

8.

break;

9.

case R.id.actionValider:

10.

doValider();

11.

break;

12.

}

13. 14.}

return true;

•   ligne 2 : on reçoit la référence de l'élément de menu qui a été cliqué ;

•   ligne 4 : pour reconnaître l'élément cliqué, on utilise son id ;

•   lignes 5-8 : dans le cas de l'élément [Navigation / Vue 2], on navigue vers la vue n° 2 (ligne 7). Par ailleurs, on met le booléen [menuDone] à faux pour régénérer le menu lorsqu'on reviendra plus tard sur la vue n° 1 ; • lignes 9-11 : dans le cas de l'élément [Actions / Valider], on exécute la méthode [doValider] ;

 16.4

La gestion du menu dans le fragment [Vue2Fragment]

On retrouve un code similaire dans le fragment de la vue n° 2 :

1.

@Override

2.

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {

3.

if (!menuDone) {

4.

// on nettoie le menu

5.

menu.clear();

6.

// on le remplit

7.

inflater.inflate(R.menu.main, menu);

8.

// on cache des options

9.

menu.getItem(0).setVisible(false);

10.

menu.getItem(1).getSubMenu().getItem(1).setVisible(false);

11.

}

12.

menuDone = true;

13.

}

14. 15.

@Override

16.

public boolean onOptionsItemSelected(MenuItem item) {

17.

// on gère l'action du menu

18.

switch (item.getItemId()) {

19.

case R.id.navigationVue1:

20.

menuDone=false;

21.

navigateToView1();

22.

break;

23.

}

24.

return true;

25.

}

•   ligne 9 : on cache l'option [Actions] ;

•   ligne 10 : on cache l'option [Navigation / Vue 2] ;

•   lignes 19-22 : lors du clic sur l'option [Navigation / Vue1], on appelle la méthode [navigateToView1] ;

 16.5

Exécution

Créez un contexte d'exécution pour ce projet et exécutez-le.

 17 Exercice d'application

 17.1

Introduction

Pour appliquer ce qui a été vu précédemment, nous proposons maintenant un travail consistant à écrire un client Android pour tablette permettant de simuler des calculs de feuille de salaire des employés d'une association.

L'application aura une architecture client / serveur :

•   le serveur [1] est founi ;

•   il faut construire le client Android [2].

Pour faire ce travail un certain nombre d'éléments sont fournis dans le fichier des exemples [].

 17.2

Installation et test du serveur REST

Le binaire Java du serveur REST est fourni :

Pour lancer le serveur REST, procédez de la façon suivante :

•   ouvrez une fenêtre DOS ;

•   placez vous dans le dossier du jar ;

•   tapez la commande :

java -jar

Cela suppose que le binaire [] est dans le PATH de votre machine. Si ce n'est pas le cas, tapez le chemin complet de [], par exemple :

D:\Programs\devjava\java\jdk1.7.u45\bin\java -jar

Une fenêtre DOS va s'ouvrir et afficher des logs :

1.    . ____          _            __ _ _

2.    /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

3.    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

4.    \\/ ___)| |_)| | | | | || (_| |  ) ) ) )

5.    ' |____| .__|_| |_|_| |_\__, | / / / /

6.    =========|_|==============|___/=/_/_/_/7. :: Spring Boot ::             (v0.5.0.M6) 8.

9. 2014-01-08 14:47:01.157  INFO 4064 --- [           main] boot.Application               

: Starting Application on Gportpers3 with PID 4064 (C:\Users\Serge TahÚ\Desktop\part3\ started by ST)

10.2014-01-08 14:47:01.192  INFO 4064 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing

[email protected]7 d29af05: startup date [Wed Jan 08 14:47:01 CET 2014]; root of context hierarchy

11.2014-01-08 14:47:02.027  INFO 4064 --- [           main]

o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'transactionManager': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; .DataSourceTransactionManagerA utoConfiguration; factoryMethodName=transactionManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource

[org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguratio

n.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false;

.HibernateJpaAutoConfigurat ion; factoryMethodName=transactionManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource

[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]

12.2014-01-08 14:47:02.396  INFO 4064 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean

'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class

org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$Enhanc erByCGLIB$$a8a9b58a] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

13.2014-01-08 14:47:02.439  INFO 4064 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

14.2014-01-08 14:47:02.462  INFO 4064 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

15.2014-01-08 14:47:02.480  INFO 4064 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean

'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

16.2014-01-08 14:47:03.150  INFO 4064 --- [           main]

.StandardService   : Starting service Tomcat

17.2014-01-08 14:47:03.151  INFO 4064 --- [           main] .StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42 18.2014-01-08 14:47:03.311  INFO 4064 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].

[/]       : Initializing Spring embedded WebApplicationContext

20.2014-01-08 14:47:03.578  INFO 4064 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].

[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'

21.2014-01-08 14:47:03.578  INFO 4064 --- [ost-startStop-1]

.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started

22.2014-01-08 14:47:03.734  INFO 4064 --- [ost-startStop-1]

o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class .servlet.resource.ResourceHttpRequestHandler]

23.2014-01-08 14:47:04.144  INFO 4064 --- [ost-startStop-1]

j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistenceunit 'default'

24.2014-01-08 14:47:04.369  INFO 4064 --- [ost-startStop-1]

o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.1.Final}

25.2014-01-08 14:47:04.385  INFO 4064 --- [ost-startStop-1] org.hibernate.Version           : HHH000412: Hibernate Core {4.2.1.Final}

26.2014-01-08 14:47:04.392  INFO 4064 --- [ost-startStop-1] .Environment   : HHH000206: hibernate.properties not found

27.2014-01-08 14:47:04.394  INFO 4064 --- [ost-startStop-1] .Environment   : HHH000021: Bytecode provider name : javassist

28.2014-01-08 14:47:04.431  INFO 4064 --- [ost-startStop-1] .Ejb3Configuration      : HHH000204: Processing PersistenceUnitInfo [

29.      name: default

30.      ]

31.2014-01-08 14:47:04.787  INFO 4064 --- [ost-startStop-1]

o.h.s.j.c.i.ConnectionProviderInitiator  : HHH000130: Instantiating explicit connection provider: .connection.InjectedDataSourceConnectionProvider

32.2014-01-08 14:47:05.095  INFO 4064 --- [ost-startStop-1] org.hibernate.dialect.Dialect  

: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect

33.2014-01-08 14:47:05.138  INFO 4064 --- [ost-startStop-1]

o.h.e.t.i.TransactionFactoryInitiator    : HHH000268: Transaction strategy: .JdbcTransactionFactory

34.2014-01-08 14:47:05.147  INFO 4064 --- [ost-startStop-1]

.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory

35.2014-01-08 14:47:05.839  INFO 4064 --- [ost-startStop-1]

s.w.s.m.m.a.RequestMappingHandlerMapping :

Mapped"{[/employes],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}

" onto public .String

.getEmployes(.HttpServletResponse)

36.2014-01-08 14:47:05.840  INFO 4064 --- [ost-startStop-1]

s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/salaire/{SS}/{HeuresTravaillÚes}/ {JoursTravaillÚs}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public .String

.getSalaire(.String,double,int,.HttpServletResponse)

37.2014-01-08 14:47:05.935  INFO 4064 --- [ost-startStop-1]

o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type

[class .servlet.resource.ResourceHttpRequestHandler]

38.2014-01-08 14:47:05.936  INFO 4064 --- [ost-startStop-1]

o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class .servlet.resource.ResourceHttpRequestHandler]

39.2014-01-08 14:47:06.521  INFO 4064 --- [ost-startStop-1]

.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 2942 ms

40.2014-01-08 14:47:06.892  INFO 4064 --- [           main] boot.Application : Started Application in 6.339 seconds


•   ligne 35 : l'URL REST [/employes] est découverte ;

•   ligne 36 : l'URL REST [/salaire/{SS}/{HeuresTravaillées}/{JoursTravaillés] est découverte ;

Le seveur REST travaille avec la base de donnée MySQL5 [dbpam_hibernate] et y accède avec le login [root] sans mot de passe. Le script de génération de la base de données [] est fourni :

Créez la base de données [dbpam_hibernate] et faites en sorte que le login root sans mot de passe puisse y accéder.

Testez les deux URL du service REST (après avoir lancé MySQL) :

L'URL [/employes] renvoie une chaîne JSON avec deux clés :

•   erreur : vaut 0 si pas d'erreur, 1 sinon ;

•   employes : est associée à la chaîne JSON de la liste des employés ;

L'URL [/salaire] attend trois paramètres [SS/ht/jt] :

•   SS : le n° de sécurité sociale de l'employé dont on veut la feuille de salaire ;

•   ht : le nombre d'heures travaillées :

•   jt : le nombre de jours travaillés ;

Elle renvoie une chaîne JSON avec deux clés :

•   erreur : vaut 0 si pas d'erreur, 1 sinon ;

•   salaire : est associée à la chaîne JSON d'une feuille de salaire ;

Ici a été utilisé un n° SS erroné. L'URL [/salaire] renvoie une chaîne JSON avec deux clés :

•   erreur : à 1 parce qu'il y a une erreur ;

•   message : le message de l'erreur

Voici un autre exemple où on a arrêté le SGBD :

Le client Android a pour objet de récupérer les informations renvoyées par le serveur REST et de les mettre en forme.

 17.3

Les vues du client Android

Les différentes vues du client Android sont les suivantes :

Il faut tout d'abord se connecter au service REST :

•   en [1], on donne l'URL du service REST. Mettez l'adresse IP wifi de la machine du serveur REST ; ;

•   en [2], on se connecte ;

On arrive alors à la page de simulation :

•   en [3], on choisit un employé ;

•   en [4], on indique un nombre d'heures ;

•   en [5], on indique un nombre de jours ;

•   en [6], on demande la simulation ;

La page de simulation obtenue est la suivante :

•   en [7], la simulation obtenue ; •          en [8], on l'enregistre ;

•   en [9], la liste des simulations ;

•   en [10], on retire une simulation ;

•   en [11], il n'y a plus de simulations ;

•   en [12], on retourne au formulaire de simulation ;

•   en [13], on retrouve un formulaire vide ;

•   en [14], on termine la session de simulations ;

•   en [15], on retrouve le formulaire de connexion initial.

 17.4

Travail à faire

Réalisez cette application. Vous trouverez dans le dossier des exemples divers éléments pour vous aider. Tout d'abord, le binaire du client Android décrit ci-dessus vous est donné :

•   reliez la tablette au PC. La tablette est reconnue comme un disque supplémentaire et son système de fichiers est

accessible ;

•   transférez le fichier [apk] ci-dessus sur la tablette ;

•   sur la tablette, utilisez le gestionnaire de fichiers pour retrouver le fichier que vous venez de copier ;

•   installez l'application en touchant le fichier [apk] ;

•   l'application se retrouve désormais avec les autres applications de la tablette. Pour la lancer, touchez son icône.

L'autre élément pour vous aider est le squelette du client Android présenté précédemment.

 Vous y trouverez notamment :

•   le projet de la couche [DAO] : [client-pam-dao-skel]. Il est complet ;

•   le projet de la couche [métier] : [client-pam-metier-skel].  Il est à compléter ;

•   le projet de la couche [présentation] : [client-pam-android-skel]. Il est à compléter.

Le projet qui vous est donné est exécutable. Il a déjà les vues nécessaires. Il y a simplement du code à rajouter pour que l'application fasse ce qu'elle a à faire.

Les données affichées par les différentes vues proviennent du serveur REST :

La liste déroulante [1] est remplie avec des informations provenant de l'URL [localhost:8080/employes] :

La page de simulation :

est remplie avec les informations provenant de 'URL [localhost:8080/salaire] :

La page des simulations :

est obtenue à partir d'une liste des simulations faites par l'utilisateur. Celles-ci doivent donc être mémorisées. L'information [3] est la somme de deux informations de la feuille de salaire :


 18 Conclusion

Ce document permet de démarrer la programmation Android mais pas de la maîtriser. Pour cette maîtrise, on consultera le site de référence pour la programmation Android []. On pourra également consulter le document [] qui propose un modèle générique pour gérer les tâches asynchrones d'une application Android et qui l'applique à une application client / serveur de gestion de rendez-vous.



14