1. Apprentissage de la programmation Android
Le PDF du document est disponible |ICI|.
Les exemples du document sont disponibles |ICI|.
1.1. Introduction
1.1.1. Contenu
Ce document est la réécriture de plusieurs documents existants :
- Introduction à la programmation de tablettes Android par l'exemple ;
- Commander un Arduino avec une tablette Android ;
- Introduction à la programmation de tablettes Android par l'exemple - version 2
et introduit les nouveautés suivantes :
- le document 1 présentait une architecture appelée AVAT (Activité-Vues-Actions-Tâches) pour faciliter la programmation asynchrone dans une application Android. Dans ce document, la bibliothèque standard RxJava est utilisée pour gérer les actions asynchrones ;
- le document 2 utilisait l'IDE Eclipse avec un plugin Android. Ce document utilise Android Studio ;
- le document 3 est repris tel quel ;
- le document 4 utilisait la bibliothèque [Android Annotations] (AA) avec l'IDE Intellij IDEA Community Edition. Ce document reprend la totalité du document 4 avec les différences suivantes :
- l'IDE est désormais Android Studio ;
- le système de build est Gradle pour tous les projets client ou serveur (dans le document 4, on utilisait parfois Maven)
- la programmation asynchrone est réalisée avec la bibliothèque RxJava (dans le document 4, on utilisait la bibliothèque AA) ;
- ce document explore des domaines pas ou peu abordés dans les documents précédents :
- la notion d'adjacence de fragments ;
- la sauvegarde / restauration de l'activité et de ses fragments ;
- le cycle de vie des fragments ;
Enfin, il présente le squelette d'un client Android communiquant avec un service web / jSON dans lequel on factorise un grand nombre d'éléments que l'on retrouve régulièrement dans ce type de clients. Ce squelette est repris par tous les exemples à partir du chapitre 2. C'est la partie vraiment innovante du document.
Les exemples suivants sont présentés :
Nature | |
Importation d'un projet Android existant | |
Un projet Android basique | |
Un projet [Android Annotations] basique | |
Vues et événements | |
Navigation entre vues | |
Navigation par onglets | |
Utilisation de la bibliothèque [Android Annotations] avec Gradle | |
Gestion des fragments d'une application Android | |
Navigation entre vues revisitée | |
Architecture à deux couches | |
Architecture client / serveur | |
Gérer l'asynchronisme avec RxJava | |
Composants de saisie de données | |
Utilisation d'un patron de vues | |
Le composant ListView | |
Utiliser un menu | |
Utilisation d'une classe parent pour les fragments | |
Sauvegarde et restauration de l'état de l'activité et des fragments | |
Client météo | |
Squelette d'un client Android communiquant avec un service web / jSON. On y factorise un grand nombre d'éléments que l'on retrouve régulièrement dans ce type de clients Android. | |
Gestion des rendez-vous d'un cabinet de médecins | |
Exercice d'application - Gestion d'une paie basique | |
Exercice d'application - Commande de cartes Arduino |
Ce document a été utilisé en dernière année de l'école d'ingénieurs IstiA de l'université d'Angers [istia.univ-angers.fr]. Cela explique le ton parfois un peu particulier du texte. Les deux exercices d'application sont des textes de TP dont on ne donne que les grandes lignes de la solution. Celle-ci est à construire par le lecteur.
Le code source des exemples est disponible |ICI|. Pour exécuter ces exemples, vous devez suivre la procédure du paragraphe 6.12.
Ce document est un document de première formation à la programmation Android. Il ne se veut pas exhaustif. Il cible essentiellement les débutants.
Le site de référence pour la programmation Android est à l'URL [http://developer.android.com/guide/components/index.html]. C'est là qu'il faut aller pour avoir une vue d'ensemble de la programmation Android.
1.1.2. Pré-requis
Le pré-requis à une utilisation optimale du document est une bonne maîtrise du langage Java.
1.1.3. Les outils utilisés
Les exemples qui suivent ont été testés dans l'environnement suivant :
- machine Windows 10 pro 64 bits ;
- JDK 1.8 ;
- Android SDK API 23 ;
- Android Studio, version 2.1 ;
- Emulateur Genymotion, version 2.6.0 ;
Pour suivre ce document vous devez installer :
- un JDK (cf paragraphe 6.8) ;
- le gestionnaire d'émulateurs Android Genymotion (cf paragraphe 6.9) ;
- le gestionnaire de dépendances Maven (cf paragraphe 6.10) ;
- l'IDE [Android Studio] (cf paragraphe 6.11) ;
1.2. Exemple-01 : importation d'un exemple Android
1.2.1. Création du projet
Créons avec Android Studio un premier projet Android. Tout d'abord créons un dossier [exemples] vide où seront placés tous nos projets :
![]() |
puis créons un projet avec Android Studio. Nous allons tout d'abord importer un des exemples livrés avec l'IDE [1-5] :
![]() |

![]() | ![]() |
L'import du projet peut aboutir à des erreurs dûes à l'inadéquation entre l'environnement utilisé lors de la création du projet et celui utilisé ici pour son exécution. C'est une occasion pour voir comment résoudre ce type d'erreurs. Ici, nous avons l'erreur suivante :
![]() | ![]() |
Le projet importé est configuré par le fichier [build.gradle] [2] suivant :
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
'main', // main sample code; look here for the interesting stuff.
'common', // components that are reused by multiple samples
'template'] // boilerplate code that is generated by the sample template process
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- l'erreur signalée est dûe aux lignes 31, 34-35 : nous n'avons pas de SDK 21. Nous remplaçons cette version par la version 23 dont nous disposons.
Dans le fichier [build.gradle], Android Studio fait des suggestions comme ci-dessous :
![]() |
Pour accepter les suggestions, on fait [alt-entrée] sur la suggestion :
![]() |
On peut avoir égaleemnt une erreur sur la version de Gradle :
![]() |
Cette erreur vient d'une inadéquation entre la version de Gradle demandée par le fichier [build.gradle] du projet (la 2.10 ligne 6 ci-dessous) :
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
et celle inscrite dans le fichier [<projet>/gradle/wrapper/gradle-wrapper.properties] :
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
Ligne 6, ci-dessus, il faut remplacer 2.8 par 2.10.
Pour avoir accès au fichier [<projet>/gradle/wrapper/gradle-wrapper.properties], il faut utiliser la perspective projet du projet :
![]() | ![]() | ![]() |
Ceci corrigé, on peut alors compiler l'application [1], lancer l'émulateur Genymotion [2] puis exécuter le projet [3] :
![]() | ![]() | ![]() |
![]() |

Arrêtons l'application :
![]() |
On peut maintenant fermer le projet. Nous allons en créer un nouveau.
![]() |
1.2.2. Quelques points sur l'IDE
1.2.2.1. Les perspectives
L'IDE Android Studio (AS) offre différentes perspectives pour travailler avec un projet. Nous en utiliserons principalement deux :
- la perspective [Android] [1] :
- la perspective [Project] [4] ;
![]() | ![]() |
![]() |
La plupart du temps nous travaillerons avec la perspective [Android]. Lorsque nous dupliquerons un projet dans un autre, nous aurons besoin de la perspective [Project].
1.2.2.2. Gestion de l'exécution
Il y a plusieurs façons d'exécuter / arrêter / réexécuter un projet AS. Il y a tout d'bord les boutons de la barre d'outils :
![]() | ![]() | ![]() |
Le bouton [Rerun] [3], arrête l'exécution du projet [2] puis le relance [1].
1.2.2.3. Gestion du cache
Android Studio entretient un cache des projets qu'il gère dans le but de rendre l'IDE la plus réactive possible. Avec la version Android 2.1 (mai 2016), souvent ce cache ne prenait pas en compte les modifications de code que l'on venait de faire. Dans ce cas, il faut invalider ce cache :
![]() | ![]() |
Avec Android 2.1 (mai 2016), l'opération précédente devait être faite de nombreuses fois et parfois cela n'était pas suffisant à résoudre l'anomalie détectée. La solution a été d'inhiber la technologie [Instant Run] :
![]() | ![]() |
- en [3-4], tout a été désactivé ;
Dans tout ce qui suit, on a travaillé avec cette configuration du cache et on n'a pas rencontré de problèmes.
1.2.2.4. Gestion des logs
Lors de l'exécution d'un projet, des logs s'affichent dans le moniteur Android :
![]() | ![]() |
Dans l'onglet [Android Monitor] [1], les logs s'affichent dans l'onglet [logcat] [2]. Le bouton [3] permet d'effacer les logs. Ce bouton est utile lorsqu'on veut voir les logs d'une action particulière :
- on efface les logs ;
- sur le périphérique Android, on fait l'action dont on veut les logs ;
- les logs qui apparaissent alors sont ceux liés à l'action faite ;
Il existe plusieurs niveux de logs [4]. Par défaut, c'est le mode [Verbose] qui est sélectionné. Il signifie que les logs de tous les niveaux sont affichés. On peut avec [4], sélectionner un niveau particulier.
Les logs sont très utiles pour savoir à quels moments de l'exécution d'un projet certaines méthodes sont affichées. Nous y aurons souvent recours. Prenons le code de la classe [MainActivity] du projet [Exemple-01] :
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
Ci-dessus, les méthodes [onCreate, ligne 14], [onCreateOptionsMenu, ligne 26] sont des méthodes de la classe parent [Activity] (ligne 9). Elles sont appelées à différents moments du cycle de vie de l'application. Parfois elles sont exécutées plusieurs fois. Même lorsqu'on lit les docs, il est parfois difficile de dire si une telle méthode du cycle de vie va s'exécuter avant ou après une méthode qu'on aurait écrite nous-mêmes. Or cette information est souvent importante à connaître. On peut alors mettre des logs comme ci-dessous :
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity","onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity","onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- lignes 7, 14 et 21 on utilise la classe [Log]. Cette classe permet d'écrire des logs sur la console Android [logcat]. Les logs sont classés en divers niveaux (info, warning, debug, verbose, error). [Log.d] affiche des logs de niveau [debug]. Son premier argument est la source du message de log. En effet, diverses sources peuvent émettre des messages sur la console de logs. Afin de pouvoir les différencier, on utilise ce premier argument. Le second argument est le message à écrire sur la console de logs ;
Si nous exécutons de nouveau le projet [Exemple-01], nous obtenons les logs suivants :
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
On découvre ainsi que la méthode [onCreate] qui crée l'activité Android est exécutée avant la méthode [onCreateOptionsMenu] qui crée le menu de l'application.
Maintenant si on clique sur l'option de menu dans l'émulateur Android [1] :
![]() |
le log suivant est ajouté dans la console de logs :
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
Dans la suite, nous ajouterons souvent dans le code Android des instructions de logs. La plupart du temps, nous ne les commenterons pas. Elles sont là juste pour inviter le lecteur à regarder la console de logs afin de comprendre progressivement le cycle de vie d'une application Android.
1.2.2.5. Gestion de l'émulateur [Genymotion]
Parfois, l'émulateur Genymotion plante et on ne peut plus le relancer. Cela vient du fait que des processus Virtualbox sont restés vivants dans le gestionnaire des tâches. Ouvez celui-ci [Ctrl-Alt-Supp] et supprimez toutes les tâches Virtualbox présentes :
![]() | ![]() |
Ceci fait, relancez l'émulateur Genymotion à partir d'Android Studio.
1.2.2.6. Gestion du binaire APK créé
La compilation du projet produit un binaire de suffixe .apk :
![]() | ![]() | ![]() |
Il y a deux versions : celle dite [debug] et l'autre dite [debug-unaligned]. C'est la première qu'il faut utiliser, l'autre étant une version intermédiaire. Le binaire .pak produit en [4] peut être transféré directement sur un émulateur ou un périphérique Android. Pour le transférer sur un émulateur, il suffit de le tirer / déposer sur l'émulateur avec la souris.
1.3. Exemple-02 : un projet Android basique
Créons avec Android Studio un nouveau projet Android [1-12] :
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
En [13], on exécute l'application. On obtient alors l'affichage [14] sur l'émulateur Genymotion.
1.3.1. Configuration Gradle
Le projet créé est configuré par le fichier [build.gradle] suivant :
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
Ce fichier a été généré par l'IDE avec les éléments de sa configuration. C'est un fichier minimal que nous allons progressivement enrichir.
- lignes 3-12 : les caractéristiques de l'application Android ;
- lignes 22-25 : ses dépendances. C'est surtout là que nous amènerons des modifications selon les exemples étudiés ;
1.3.2. Le manifeste de l'application
![]() |
Le fichier [AndroidManifest.xml] [1] fixe les caractéristiques du binaire de l'application Android. Son contenu est ici le suivant :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- ligne 3 : le paquetage du projet Android ;
- ligne 10 : le nom de l'activité ;
Ces deux renseignements viennent des saisies faites lors de la création du projet :
![]() |
- la ligne 3 du manifeste (package) vient de la saisie [4] ci-dessus. Un certain nombre de classes sont automatiquement générées dans ce package ;
![]() |
- la ligne 10 du manifeste (nom de l'activité) vient de la saisie [1] ci-dessus ;
Revenons au manifeste :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- ligne 10 : l'activité principale de l'application. Elle référence la classe [1] ci-dessus ;
- ligne 6 : l'icône [2] de l'application. Elle peut être changée ;
- ligne 7 : le libellé de l'aplication. Il se trouve dans le fichier [strings.xml] [3] :
<resources>
<string name="app_name">Exemple-02</string>
</resources>
Le fichier [strings.xml] contient les chaînes de caractères utilisées par l'application. Ligne 2, le nom de l'application provient de la saisie faite lors de la construction du projet [4] :
![]() |
- ligne 10 : une balise d'activité. Une application Android peut avoir plusieurs activités ;
- ligne 12 : l'activité est désignée comme étant l'activité principale ;
- ligne 13 : et elle doit apparaître dans la liste des applications qu'il est possible de lancer sur l'appareil Android.
1.3.3. 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. La classe [MainActivity] générée est la suivante :
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- ligne 6 : la classe [MyActivity] étend la classe Android [AppCompatActivity]. Ce sera le cas de toutes les activités futures ;
- ligne 9 : 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 10 : la méthode [onCreate] de la classe parente est appelée. Il faut toujours le faire ;
- ligne 11 : le fichier [activity_main.xml] [2] est la vue associée à l'activité. La définition XML de cette vue est la suivante :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lignes b-k : 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 (à droite de, à gauche de, dessous, au-dessus) ;
- lignes m-p : un composant de type [TextView] qui sert à afficher du texte ;
- ligne n : le texte affiché. Il est déconseillé de mettre du texte en dur dans les vues. Il est préférable de déplacer ces textes dans le fichier [res/values/strings.xml] [3] :
Le texte affiché sera donc [Hello World!]. Où sera-t-il affiché ? Le conteneur [RelativeLayout] va remplir l'écran. Le [TextView] qui est son seul et unique élément sera affiché en haut et à gauche de ce conteneur, donc en haut et à gauche de l'écran ;
Que signifie [R.layout.activity_main] ligne 11 ? Chaque ressource Android (vues, fragments, composants, ...) se voit attribuer un identifiant. Ainsi une vue [V.xml] se trouvant dans le dossier [res / layout] sera identifiée par [R.layout.V]. R est une classe générée dans le dossier [app / build / generated] [1-3] :
![]() |
La classe [R] est la suivante :
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name=0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container=0x7f040001;
...
public static final int activity_main=0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
- ligne 14 : l'attribut [R.layout.activity_main] est l'identifiant de la vue [res / layout / activity_main.xml] ;
- ligne 7 : l'attribut [R.string.app_name] est l'identifiant de la chaîne [app_name] dans le fichier [res / values / string.xml] :
- ligne 19 : l'attribut [R.mipmap.ic_launcher] est l'identifiant de l'image [res / mipmap / ic_launcher] ;
On se souviendra donc que lorsqu'on référence [R.layout.activity_main] dans le code, on référence un attribut de la classe [R]. L'IDE nous aide à connaître les différents éléments de cette classe :
![]() | ![]() |
1.3.4. Exécution de l'application
Pour exécuter une application Android, il nous faut créer une configuration d'exécution :
![]() | ![]() | ![]() |
- en [1], choisir [Edit Configurations] ;
- le projet a été créé avec une configuration [app] que nous allons supprimer [2] pour la recréer ;
- en [3], créer une nouvelle configuration d'exécution ;
![]() |
- en [4], choisir [Android Application] ;

- en [5], dans la liste déroulante choisir le module [app] ;
- en [6-8], garder les valeurs proposées par défaut ;
- en [7], l'activité par défaut est celle définie dans le fichier [AndroidManifest.xml] (ligne 1 ci-dessous) :
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- en [8], sélectionner [Show Chooser Dialog] qui permet de choisir le périphérique d'exécution de l'application (émulateur, tablette) ;
- en [9], on indique que ce choix doit être mémorisé ;
- valider la configuration ;
![]() |
- en [11], lancer le gestionnaire des émulateurs [Genymotion] (cf paragraphe 6.9) ;
![]() |
- en [12], sélectionner un émulateur de tablette et lancez le [13] ;
![]() | ![]() |
- en [14], exécutez la configuration d'exécution [app] ;
- en [15] est présenté le formulaire de choix du périphérique d'exécution. Un seul est ici disponible : l'émulateur [Genymotion] lancé précédemment ;
L'émulateur logiciel affiche au bout d'un moment la vue suivante :

1.3.5. Le cycle de vie d'une activité
Revenons sur le code de l'activité [MainActivity] :
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
La méthode [onCreate] des lignes 8-12 fait partie des méthodes qui peuvent être appelées au cours du cycle de vie d'une activité. La documentation Android donne la liste de celles-ci :
![]() |
- [1] : la méthode [onCreate] est appelée au démarrage de l'activité. C'est dans cette méthode qu'on associe l'activité à une vue et qu'on récupère les références des composants de celle-ci ;
- [2-3] : les méthodes [onStart, onResume] sont ensuite appelées. On voit que la méthode [onResume] est la dernière méthode à être exécutée avant d'arriver à l'état [4] de l'activité en cours d'exécution ;
1.4. Exemple-03 : réécriture du projet [Exemple-02] avec la bibliothèque [Android Annotations]
Nous allons maintenant introduire la bibliothèque [Android Annotations] qui facilite l'écriture des applications Android. Pour cela on duplique l'exemple [Exemple-02] dans [Exemple-03] en suivant la procédure [1-16].
![]() | ![]() |
- en [1], prenez la perspective [Project] pour voir la totalité du projet Android ;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
Note : entre [14] et [15], on est passé d'une perspective [Android] à une perspective [Project] (cf paragraphe 1.2.2.1).
Nous modifions ensuite le fichier [res / values / strings.xml] [17] :
![]() |
Le fichier [strings.xml] est modifié de la façon suivante :
<resources>
<string name="app_name">Exemple-03</string>
</resources>
Maintenant, nous exécutons la nouvelle application qui a repris toute la configuration de [Exemple-02] :
![]() | ![]() |
En [19], nous obtenons le même résultat qu'avec [Exemple-02] mais avec un nouveau nom.
Nous allons maintenant introduire la bibliothèque [Android Annotations] que nous appellerons par facilité AA. Cette bibliothèque introduit de nouvelles classes pour annoter les sources Android. Ces annotations vont être utilisées par un processeur qui va créer de nouvelles classes Java dans le module, classes qui participeront à la compilation de celui-ci au même titre que les classes écrites par le développeur. On a ainsi la chaîne de compilation suivante :
![]() |
Nous allons tout d'abord mettre dans le fichier [build.gradle] les dépendances sur le compilateur d'annotations AA (processeur ci-dessus) :
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- les lignes 4-5 ajoutent les deux dépendances qui forment la bibliothèque AA ;
Le fichier [build.gradle] est modifié de nouveau pour utiliser un plugin appelé [android-apt] qui modifie le processus de compilation en deux étapes :
- traitement des annotations Android, ce qui donne naissance à de nouvelles classes ;
- compilation de l'ensemble des classes du projet ;
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
- ligne 8 : version du plugin [android-apt] qui sera cherchée dans le dépôt Maven central (ligne 3) ;
- ligne 13 : activation de ce plugin ;
A ce stade, vérifiez que la configuration d'exécution [app] fonctionne toujours.
Nous allons maintenant introduire une première annotation de la bibliothèque AA dans la classe [MainActivity] :
![]() |
La classe [MainActivity] est pour l'instant la suivante :
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Nous avons déjà expliqué ce code au paragraphe 1.3.3. Nous le modifions de la façon suivante :
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- ligne 7 : l'annotation [@EActivity] est une annotation AA (ligne 3). Son paramètre est la vue associée à l'activité ;
Cette annotation va produire une classe [MainActivity_] dérivée de la classe [MainActivity] et c'est cette classe qui sera la véritable activité. Nous devons donc modifier le manifeste du projet [AndroidManifest.xml] de la façon suivante :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- ligne 11 : la nouvelle activité ;
Ceci fait, nous pouvons compiler le projet [1] :
![]() | ![]() |
- en [2], on voit la classe [MainActivity_] générée dans le dossier [app / build / generated / source / apt / debug] ;
La classe [MainActivity_] générée est la suivante :
//
// DO NOT EDIT THIS FILE.
// Generated using AndroidAnnotations 4.0.0.
//
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- lignes 24-25 : la classe [MainActivity_] étend la classe [MainActivity] ;
Nous ne chercherons pas à expliquer le code des classes générées par AA. Elles gèrent la complexité que les annotations cherchent à cacher. Mais il peut être parfois bon de l'examiner lorsqu'on veut comprendre comment sont 'traduites' les annotations qu'on utilise.
On peut désormais exécuter de nouveau la configuration [app]. On obtient le même résultat qu'auparavant. Nous allons désormais partir de ce projet que nous dupliquerons pour présenter les notions importantes de la programmation Android.
1.5. Exemple-04 : vues et événements
1.5.1. Création du projet
On suivra la procédure décrite pour dupliquer [Exemple-02] dans [Exemple-03] au paragraphe 1.4 :
Nous :
- dupliquons le projet [Exemple-03] dans [Exemple-04] (après avoir supprimé le dossier [app / build] de [Exemple-03]) ;
- chargeons le projet [Exemple-04] ;
- changeons le nom du projet dans le fichier [app / res / values / strings.xml] (perspective Android) ;
- supprimons le fichier [Exemple-04 / Exemple-04.iml] (perspective Project) ;
- compilons puis exécutons le projet ;
![]() | ![]() |
1.5.2. Construire une vue
Nous allons maintenant modifier, avec l'éditeur graphique, la vue affichée par le projet [Exemple-04] :
![]() | ![]() |
- en [1-4], créez une nouvelle vue XML ;
- en [5], nommez la vue ;
- en [6], 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 ", " au-dessus de " ;
![]() |
Le fichier [vue1.xml] généré [7] est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</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 [Design] dans la vue [vue1.xml] affichée ;
- en [2-4], mettez-vous en mode tablette ;
![]() | ![]() | ![]() |
- en [5], mettez-vous à l'échelle 1 de la tablette ;
- en [6], choisissez le mode 'paysage' pour la tablette ;
- la copie d'écran [7] résume les choix faits.
![]() | ![]() | ![]() |
- en [1], prendre un [Large Text] et le tirer sur la vue [2] ;
- en [3], double-cliquer sur le composant ;
- en [4], modifier le texte affiché. Plutôt que de le mettre en 'dur' dans la vue XML, nous allons l'externaliser dans le fichier [res / values / string.xml]
![]() | ![]() | ![]() |
- en [5], on ajoute une nouvelle valeur dans le fichier [strings.xml] ;
- en [8], on donne un identifiant à la chaîne ;
- en [9], on donne la valeur de la chaîne ;
- en [10], la nouvelle vue après validation de l'étape précédente ;
![]() | ![]() | ![]() |
- après un double clic sur le composant, on change son identifiant [11] ;
- en [12], dans les propriétés du composant, on change la taille des caractères [50sp] ;
- en [13], la nouvelle vue ;
Le fichier [vue1.xml] a évolué comme suit :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- les modifications faites dans l'interface graphique sont aux lignes 10, 11 et 14. Les autres attributs du [TextView] sont des valeurs par défaut ou bien découlent du positionnement du composant dans la vue ;
- lignes 7-8 : la taille du composant est celle du texte qu'elle contient (wrap_content) en hauteur et largeur ;
- ligne 13 : le haut du composant est aligné avec le haut de la vue (ligne 13), 50 pixels dessous (ligne 13) ;
- ligne 12 : le côté gauche du composant est aligné avec la gauche de la vue (ligne 13), 213 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 :
Placer les composants les uns par rapport aux autres est un exercice qui peut s'avérer frustrant, les réactions de l'éditeur graphique étant parfois surprenantes. Il peut être préférable d'utiliser les propriétés des composants :
Le composant [textView1] doit être placé à 50 pixels sous le titre et à 50 pixels du bord gauche du conteneur :
![]() | ![]() | ![]() |
- en [1], le bord supérieur (top) du composant est aligné par rapport au bord inférieur (bottom) du composant [textViewTitreVue1] à une distance de 50 pixels [3] (top) ;
- en [2], le bord gauche (left) du composant est aligné par rapport au bord gauche du conteneur à une distance de 50 pixels [3] (left) ;
Le composant [editTextNom] doit être placé à 60 pixels à droite du composant [textView1] et aligné par le bas sur ce même composant ;
![]() | ![]() |
- en [1], le bord gauche (left) du composant est aligné par rapport au bord droit (right) du composant [textView1] à une distance de 60 pixels [2] (left). Il est aligné sur le bord inférieur (bottom:bottom) du composant [textView1] [1] ;
Le composant [buttonValider] doit être placé à 60 pixels à droite du composant [editTextNom] et aligné par le bas sur ce même composant ;
![]() | ![]() |
- en [1], le bord gauche (left) du composant est aligné par rapport au bord droit (right) du composant [editTextNom] à une distance de 60 pixels [2] (left). Il est aligné sur le bord inférieur du composant (bottom:bottom) [editTextNom] [1] ;
Le composant [buttonVue2] doit être placé à 50 pixels sous le composant [textView1] et aligné par la gauche sur ce même composant ;
![]() | ![]() |
- en [1], le bord gauche (left) du composant est aligné par rapport au bord gauche (left) du composant [textView1] et est placé dessous (top:bottom) à une distance de 50 pixels [2] (top) ;
Le fichier XML généré est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
On y retrouve tout ce qui a été fait de façon graphique. Une autre façon de créer une vue est alors d'écrire directement ce fichier. Lorsqu'on est habitué, cela peut être plus rapide que d'utiliser l'éditeur graphique.
- ligne 38, on trouve une information que nous n'avons pas montrée. Elle est donnée via les propriétés du composant [editTextNom] [1] :
![]() | ![]() |
Tous les textes proviennent du fichier [strings.xml] [2] suivant :
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
Maintenant, modifions l'activité [MainActivity] pour que cette vue soit affichée au démarrage de l'application :
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- ligne 7 : c'est la vue [vue1.xml] qui est désormais affichée par l'activité ;
Modifiez le fichier [AndroidManifest.xml] de la façon suivante :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- ligne 12 : cette ligne de configuration empêche le clavier d'apparaître dès l'affichage de la vue [vue1]. En effet celle-ci a un champ de saisie qui a le focus lors de l'affichage de la vue. Ce focus fait apparaître par défaut le clavier virtuel ;
Exécutez l'application et vérifiez que c'est bien la vue [vue1.xml] qui est affichée :

1.5.3. Gestion des événements
Gérons maintenant le clic sur le bouton [Valider] de la vue [Vue1] :

Le code de [MainActivity] évolue comme suit :
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// gestionnaire d'évt
@Click(R.id.buttonValider)
protected void doValider() {
// on affiche le nom saisi
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- lignes 17-18 : on associe le champ [protected EditText editTextNom] au composant d'identifiant [R.id.editTextNom] de l'interface visuelle. Le champ associé au composant doit être accessible dans la classe dérivée [MainActivity_] et pour cette raison ne peut être de portée [private]. Le champ identifié par [R.id.editTextNom] provient de la vue [vue1.xml] :
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
Note : ne pas mettre de caractères accentués dans les identifiants [id]. AA ne les gère pas correctement.
- ligne 32 : l'annotation [@Click(R.id.buttonValider)] désigne la méthode qui gère l'événement 'Click' sur le bouton d'identifiant [R.id.buttonValider]. Cet identifiant provient également de la vue [vue1.xml] :
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- ligne 35 : 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 ;
- ligne 26, l'annotation [@AfterViews] annote la méthode à exécuter lorsque tous les champs annotés par [@ViewById] sont initialisés. Il est important de savoir quand ces champs sont initialisés. Par exemple, est-ce que dans la méthode [onCreate] on peut utiliser la référence de la ligne 18 ? Pour répondre à cette question, nous avons mis des logs ;
Exécutez le projet [Exemple-04] et vérifiez qu'il se passe quelque chose lorsque vous cliquez sur le bouton [Valider]. Nous obtenons les logs suivants :
On en conclut que lorsque la méthode [onCreate] s'exécute, les champs annotés par [@ViewById] ne sont pas encore initialisés. De nouveau, le lecteur débutant est encouragé à mettre ce type de logs dans les méthodes qui gèrent le cycle de vie de l'application.
1.6. Exemple-05 : 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 [AppCompatActivity] qui affiche des vues de type [Fragment]. Ce sera la méthode utilisée dans des applications à venir.
1.6.1. Création du projet
On duplique le projet [Exemple-04] dans [Exemple-05]. Pour cela, on suivra la procédure décrite pour dupliquer [Exemple-02] dans [Exemple-03] au paragraphe 1.4 et qui a été reproduite dans le paragraphe 1.5.
![]() | ![]() |
1.6.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 possibles.

- en [1-4], on crée un nouvelle activité ;

- en [5], le nom de la classe qui sera générée ;
- en [6], le nom de la vue (vue2.xml) associée à la nouvelle activité ;
![]() |
- en [7-8], les fichiers impactés par la configuration précédente ;
L'activité [SecondActivity] est la suivante :
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- ligne 11 : l'activité est associée à la vue [vue2.xml] ;
La vue [vue2.xml] est la suivante :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
</RelativeLayout>
C'est une vue pour l'instant vide avec un gestionnaire de disposition de type [RelativeLayout] (ligne 2). Ligne 11, on voit qu'elle a été associée à la nouvelle activité.
Le manifeste du module Android [AndroidManifest.xml] a évolué de la façon suivante :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
Ligne 20 , une seconde activité a été enregistrée.
1.6.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 n'est actuellement pas géré :
![]() |
Nous la gérons de la façon suivante :
// naviguer vers la vue n° 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// on navigue vers la vue n° 2 en lui passant le nom saisi dans la vue n° 1
// on crée un Intent
Intent intent = new Intent();
// on associe cet Intent à une activité
intent.setClass(this, SecondActivity.class);
// on associe des informations à cet Intent
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// on lance l'activité de type [SecondActivity] en lui passant l'Intent
startActivity(intent);
}
- lignes 2-3 : la méthode [navigateToView2] gère le 'clic' sur le bouton identifié par [R.id.buttonVue2] défini dans la vue [vue1.xml] :
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
Les commentaires décrivent les étapes à réaliser pour le changement de vue :
- ligne 6 : créer un objet de type [Intent]. Cet objet va permettre de préciser et l'activité à lancer et les informations à lui passer ;
- ligne 8 : 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] ;
- ligne 10 : 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 ;
- ligne 12 : lancer l'activité définie par l'objet [Intent].
Exécutez le projet [Exemple-05] et vérifiez que vous obtenez bien la vue n° 2 (vide pour l'instant) :
![]() | ![]() |
1.6.4. Construction de la vue n° 2
![]() | ![]() |
- en [1-2], nous supprimons la vue [main.xml] qui ne nous sert plus, puis nous modifions la vue [vue2.xml] de la façon suivante :
![]() |
Les composants sont les suivants :
Le fichier XML [vue2.xml] est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
Exécutez le projet [Exemple-05] et vérifiez que vous obtenez bien la nouvelle vue en cliquant sur le bouton [Vue n° 2].
1.6.5. L'activité [SecondActivity]
Dans [MainActivity], nous avons écrit le code suivant :
// naviguer vers la vue n° 2
protected void navigateToView2() {
// on navigue vers la vue n° 2 en lui passant le nom saisi dans la vue n° 1
// on crée un Intent
Intent intent = new Intent();
// on associe cet Intent à une activité
intent.setClass(this, SecondActivity.class);
// on associe des informations à cet Intent
intent.putExtra("NOM", edtNom.getText().toString().trim());
// on lance l'activité de type [SecondActivity] en lui passant l'Intent
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] :
![]() |
Le code de [SecondActivity] évolue comme suit :
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// composants de l'interface visuelle
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// on récupère l'intent s'il existe
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// on récupère le nom
String nom = extras.getString("NOM");
if (nom != null) {
// on l'affiche
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- ligne 11 : on utilise l'annotation [@EActivity] pour indiquer que la classe [SecondActivity] est une activité associée à la vue [vue2.xml] ;
- lignes 15-16 : on récupère une référence sur le composant [TextView] identifié par [R.id.textViewBonjour]. Ici, on n'a pas écrit [@ViewById(R.id.textViewBonjour)]. Dans ce cas, AA suppose que l'identifiant du composant est identique au champ annoté, ici le champ [textViewBonjour] ;
- ligne 23 : l'annotation [@AfterViews] annote une méthode qui doit être exécutée après que les champs annotés par [@ViewById] ont été initialisés. Dans la méthode [OnCreate] (ligne 19), on ne peut pas utiliser ces champs car ils n'ont pas encore été initialisés. Dans le projet [Exemple-05], on bascule d'une activité à une autre et il n'était a priori pas clair si la méthode annotée [@AfterViews] allait être exécutée une fois à l'instanciation initiale de l'activité ou à chaque fois que l'activité est démarrée. Les tests ont montré que la seconde hypothèse était vérifiée ;
- ligne 26 : la classe [AppCompatActivity] a une méthode [getIntent] qui rend l'objet [Intent] associé à l'activité ;
- ligne 28 : 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 31 : on récupère le nom placé dans l'objet [Intent] de l'activité ;
- ligne 34 : on l'affiche.
Rappel : les champs annotés par l'annotation [@ViewById] ne doivent pas comporter de caractères accentués.
Revenons sur la classe [SecondActivity]. Parce que nous avons écrit :
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
AA va générer une classe [SecondActivity_] dérivée de [SecondActivity] et c'est cette classe qui sera la véritable activité. Cela nous amène à faire des modifications dans :
[MainActivity]
// naviguer vers la vue n° 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// on associe cet Intent à une activité
intent.setClass(this, SecondActivity_.class);
...
}
- ligne 6, on doit remplacer [SecondActivity] par [SecondActivity_] ;
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- ligne 20, on doit remplacer [SecondActivity] par [SecondActivity_] ;
Testez cette nouvelle version. Tapez un nom dans la vue n° 1 et vérifiez que la vue n° 2 l'affiche bien.
![]() | ![]() |
1.6.6. Navigation de la vue n° 2 vers la vue n° 1
Pour naviguer de la vue n° 2 à la vue n° 1 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 [@AfterViews] dans l'activité [MainActivity] qui affiche la vue n° 1 ;
Le code de [SecondActivity] évolue comme suit :
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// on crée un Intent pour l'activité [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// on récupère l'Intent de l'activité courante [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// on met le nom dans l'Intent de [MainActivity]
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// on lance [MainActivity]
startActivity(intent1);
}
}
- lignes 1-2 : on associe la méthode [navigateToView1] au clic sur le bouton [btn_vue1] ;
- ligne 4 : on crée un nouvel [Intent] ;
- ligne 5 : associé à l'activité [MainActivity_] ;
- ligne 7 : on récupère l'Intent associé à [SecondActivity] ;
- ligne 9 : on récupère les informations de cet Intent ;
- ligne 12 : la clé [NOM] est récupérée dans [intent2] pour être mise dans [intent1] avec la même valeur associée ;
- ligne 15 : l'activité [MainActivity_] est lancée.
Dans le code de [MainActivity] on ajoute la méthode [@AfterViews] suivante :
@AfterViews
protected void afterViews() {
// on récupère l'intent s'il existe
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// on récupère le nom
String nom = extras.getString("NOM");
if (nom != null) {
// on l'affiche
editTextNom.setText(nom);
}
}
}
}
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.
![]() | ![]() |
1.6.7. Cycle de vie des activités
Nous avons présenté au paragraphe 1.3.5, le cycle de vie d'une activité. Nous avons ici deux activités et on bascule de l'une à l'autre pendant l'exécution. Ces activités contiennent deux méthodes dont on se sait pas très bien quand elles sont appelées l'une par rapport à l'autre : [onCreate] et [afterViews]. Il est important de le savoir. Pour cela, nous ajoutons des logs dans les deux activités :
Ainsi dans la classe [MainActivity], nous écrivons :
// constructeur
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- lignes 2-4 : nous voulons savoir si la classe [MainActivity] est instanciée une ou plusieurs fois ;
- ligne 8 : nous voulons savoir si la méthode [onCreate] est appelée une ou plusieurs fois ;
- ligne 14 : nous voulons savoir si la méthode [afterViews] est appelée une ou plusieurs fois ;
Nous faisons exactement de même dans la classe [SecondActivity].
Au démarrage de l'application, nous avons les logs suivants :
Les méthodes [onCreate, afterViews] de la première activité ont été exécutées dans cet ordre. Lorsqu'on clique sur le bouton [Vue n° 2], les nouveaux logs sont les suivants :
Les méthodes [onCreate, afterViews] de la seconde activité ont été exécutées dans cet ordre. Lorsqu'on clique sur le bouton [Vue n° 1], les nouveaux logs sont les suivants :
La classe [MainActivity] est donc de nouveau instanciée. Lorsqu'on clique sur le bouton [Vue n° 2], les nouveaux logs sont les suivants :
La classe [SecondActivity] est donc de nouveau instanciée.
Les deux activités sont donc systématiquement recréées lorsqu'on change d'activité.
Nous allons découvrir maintenant une architecture avec une unique activité pouvant gérer plusieurs vues appelées fragments. L'activité et les vues ne seront instanciées qu'une fois contrairement à la méthode précédente où une activité pouvait être instanciée plusieurs fois.
1.7. Exemple-06 : navigation par onglets
Nous allons ici explorer les interfaces à onglets. L'exemple est complexe mais introduit tous les éléments que nous allons utiliser par la suite : activité unique, gestionnaire de fragments (vues), conteneur de fragments, navigation entre fragments. La notion d'onglets est différente de celle des fragments et est mineure dans ce qu'on veut montrer dans cet exemple.
1.7.1. Création du projet
Nous créons un nouveau projet :
![]() | ![]() |
![]() |
![]() |
- en [7], on choisit une activité avec onglets (Tabbed Activity) ;
![]() |
- en [10-14], on garde les valeurs proposées par défaut ;
- en [15], on choisit des onglets avec une barre de titres ;
Le projet créé est alors le suivant :
![]() | ![]() |
- en [1], l'activité ;
- en [2], les vues ;
Une configuration d'exécution [app], du nom du module, a été automatiquement créée [2b] :
![]() |
On peut l'exécuter. S'affiche alors une fenêtre avec trois onglets [3-6] :

1.7.2. Configuration Gradle
Le projet [Exemple-06] a été généré avec le fichier [build.gradle] suivant :
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
Il y a une nouveauté par rapport à ce qui a déjà été rencontré : la ligne 25. Cette bibliothèque est nécessaire aux nouveaux composants utilisés par l'application générée.
1.7.3. La vue [activity_main]
![]() |
La vue [activity_main] est la vue associée à l'activité [MainActivity] du projet. En mode [design], la vue est la suivante :

Elle contient les composants suivants :
![]() |
- [main_content] est la totalité de la vue ;
- [appbar] (encadré rouge, 1) est la barre d'application. Elle contient deux composants :
- [toolbar] (encadré jaune 4) est la barre d'outils ;
- [tabs] (encadré orange 5) est la barre de titre des onglets ;
- [container] (encadré vert, 2) peut accueillir divers fragments. Un fragment est une vue. Ainsi, la même activité va pouvoir afficher plusieurs vues (fragments) dans ce conteneur ;
- [fab] (composant 3) est appelé composant flottant ;
En mode [text], le code est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="exemples.android.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
On retrouve les éléments décrits précédemment :
- lignes 2-49 : la définition du composant [main_content] (ligne 5) qui est la totalité de la vue. On voit que c'est un layout (gestionnaire de disposition de composants) de type [CoordinatorLayout] (ligne 2) ;
- lignes 11-33 : le conteneur [appbar] (ligne 12). C'est un layout de type [AppBarLayout] (ligne 11) ;
- lignes 18-24 : le composant [toolbar] (ligne 19) de type [Toolbar] (ligne 18) ;
- lignes 28-31 : le conteneur [tabs] (ligne 29). C'est un layout de type [TabLayout] (ligne 28). Il va afficher les titres des onglets ;
- lignes 35-39 : le composant [container] (ligne 36). C'est ce conteneur qui affiche les différentes vues de l'activité ;
- lignes 41-47 : le composant [fab] (ligne 42) de type [FloatingActionButton] (ligne 41). C'est un bouton sur lequel on peut cliquer. Il se place par défaut en bas à droite de la vue totale ;
Nous n'essaierons pas de comprendre la signification de tous les attributs de ces composants. Nous allons les utiliser tels quels. C'est avec l'expérience et souvent en mode [design] que l'on découvre leur rôle. Dans ce mode, on découvre que les composants ont plusieurs dizaines d'attributs. En général, seuls certains sont initialisés, les autres gardant une valeur par défaut.
Précisons néanmoins quelques points. La plupart des valeurs configurant les différentes vues sont rassemblées dans le dossier [res / values] :
![]() |
Ces valeurs sont référencées aux lignes 15-16, 23, 39, 46 du fichier [activity_main.xml]. Prenons un exemple :
- ligne 15 :
android:paddingTop="@dimen/appbar_padding_top"
L'annotation [@dimen] fait référence au fichier [res / values / dimens.xml] :
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
La ligne 15 du fichier [activity_main.xml] fait référence à la ligne (f) ci-dessus ;
De façon analogue, l'annotation :
- [@string] fait référence au fichier de ressources [res / values / strings.xml] ;
- [@color] fait référence au fichier de ressources [res / values / colors.xml] ;
- [@style] fait référence au fichier de ressources [res / values / styles.xml] ;
1.7.4. L'activité
![]() |
Le code généré pour l'activité est à la hauteur de la vue décrite précédemment : il est complexe. Nous allons l'analyser en plusieurs étapes.
1.7.4.1. La gestion des fragments et des onglets
Le code de [MainActivity] relatif aux fragments et onglets est le suivant :
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
// le conteneur de fragments
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// vue
setContentView(R.layout.activity_main);
// barre d'outils
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// le gestionnaire de fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// le conteneur de fragments est associé au gestionnaire de fragments
// ç-a-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// la barre d'onglets est également associée au conteneur de fragments
// ç-à-d que l'onglet n° i affiche le fragment n° i du conteneur
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// un fragment
public static class PlaceholderFragment extends Fragment {
...
}
// le gestionnaire de fragments
// c'est à lui qu'on demande les fragments à afficher dans la vue principale
// doit définir les méthodes [getItem] et [getCount] - les autres sont facultatives
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- ligne 28 : Android fournit un conteneur de vues de type [android.support.v4.view.ViewPager] (ligne 12). Il faut fournir à ce conteneur un gestionnaire de vues ou fragments. C'est le développeur qui le fournit ;
- ligne 25 : le gestionnaire de fragments utilisé dans cet exemple. Son implémentation est aux lignes 61-63 ;
- ligne 31 : la méthode exécutée à la création de l'activité ;
- ligne 35 : la vue [activity_main.xml] est associée à l'activité ;
- ligne 37 : on récupère la référence du composant [toolbar] de la vue via son identifiant ;
- ligne 38 : cette barre d'outils devient la barre d'action (une notion Android) de l'activité ;
- ligne 40 : le gestionnaire de fragments est instancié. Le paramètre du constructeur est la classe Android [android.support.v4.app.FragmentManager] (ligne 10) ;
- ligne 44 : on récupère dans la vue [activity_main.xml] la référence du conteneur de fragments via son identifiant ;
- ligne 45 : le gestionnaire de fragments est lié au conteneur de fragments. Cela signifie que lorsqu'on demandera au conteneur de fragments d'afficher le fragment n° i, celui sera demandé au gestionnaire de fragments ;
- ligne 48 : on récupère une référence sur la barre d'onglets via son identifiant ;
- ligne 49 : le gestionnaire d'onglets est associé au conteneur de fragments. Cela signifie que lorsqu'on cliquera sur l'onglet n° i, le conteneur affichera le fragment n° i. L'association faite entre le gestionnaire d'onglets et le conteneur de fragments nous évite toute gestion des onglets. Ainsi n'avons-nous pas à définir de gestionnaire d'événement pour le clic sur un onglet. L'association avec le conteneur de fragments le fournit par défaut. Nous verrons un exemple où il y aura plus de fragments que d'onglets. Dans ce cas, on ne fait pas cette association.
Le gestionnaire de fragments [SectionsPagerAdapter] est le suivant :
// le gestionnaire de fragments
// c'est à lui qu'on demande les fragments à afficher dans la vue principale
// doit définir les méthodes [getItem] et [getCount] - les autres sont facultatives
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// on instancie un fragment [PlaceHolder] et on le rend
return PlaceholderFragment.newInstance(position + 1);
}
// rend le nombre de fragments gérés
@Override
public int getCount() {
return 3;
}
// facultatif - donne un titre aux fragments gérés
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- les fragments affichés par une application dépendent de celle-ci. Le gestionnaire de fragments est défini par le développeur ;
- ligne 5 : le gestionnaire de fragments étend la classe Android [android.support.v4.app.FragmentPagerAdapter]. Le constructeur nous est imposé. Nous devons définir au moins les deux méthodes suivantes :
- int getCount() : rend le nombre de fragments à gérer ;
- Fragment getItem(i) : rend le fragment n° i ;
La méthode CharSequence getPageTitle(i) qui rend le titre du fragment n° i est facultative. Parce que le gestionnaire d'onglets a été associé au gestionnaire de fragments, le titre de l'onglet n° i sera le titre du fragment n° i. Ainsi, les titres des lignes 27-33 vont-ils être les titres des onglets ;
- lignes 18-21 : getCount rend le nombre de fragments gérés, ici trois ;
- lignes 11-15 : getItem(i) rend le fragment n° i. Ici tous les fragments seront identiques de type [PlaceholderFragment] ;
- lignes 24-35 : getPageTitle(int i) rend le titre du fragment n° i ;
1.7.4.2. Les fragments affichés
![]() |
Les fragments de l'activité ont ici tous le même type et sont tous associés à la vue XML [fragment_main] suivante :
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lignes 1-16 : un layout de type [RelativeLayout] ;
- lignes 11-14 : l'unique composant de la vue (fragment) : un [TextView] identifié par [section_label] ;
Dans [MainActivity], les fragments gérés sont du type [PlaceholderFragment] suivant :
// un fragment
public static class PlaceholderFragment extends Fragment {
// un texte affiché dans le fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// rend un fragment avec une information : le n° du fragment passé en paramètre
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// info embarquée
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// résultat
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// vue [fragment_main] est instanciée
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// le [TextView] est retrouvé
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// son contenu est modifié
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// on retourne la vue
return rootView;
}
}
- ligne 2 : la classe [PlaceholderFragment] étend la classe Android [Fragment]. C'est en général toujours le cas ;
- ligne 2 : la classe [PlaceholderFragment] est statique. Sa méthode [newInstance] (ligne 10) permet d'obtenir des instances de type [PlaceholderFragment] ;
- lignes 10-19 : la méthode [newInstance] crée et retourne un objet de type [PlaceholderFragment] ;
- lignes 14-16 : le fragment est créé avec un argument ;
Un fragment doit définir la méthode [onCreateView] de la ligne 22. Cette méthode doit rendre la vue associée au fragment.
- ligne 25 : la vue [fragment_main.xml] est associée au fragment ;
- ligne 27 : cette vue contient un composant [TextView] dont on récupère la référence via son identifiant ;
- ligne 29 : on affiche un texte dans le [TextView] ;
- [getString] est une méthode de la classe parent [AppCompatActivity] ;
- le 1er argument est un n° de composant. [R.string.section_format] désigne le n° du composant identifié par [section_format] dans le fichier [res / values / strings.xml] (ligne 4 ci-dessous) :
<div class="odt-code-rich" data-linenums="true" style="counter-reset: odtline 0;"><pre><code class="language-python">
<span class="odt-code-line"><span style="color:#008080"><</span><span style="color:#3f7f7f">resources</span><span style="color:#008080">></span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#008080"><</span><span style="color:#3f7f7f">string</span> <span style="color:#7f007f">name</span><span style="color:#000000">=</span><span style="font-style:italic;color:#2a00ff">"app_name"</span><span style="color:#008080">></span><span style="color:#000000">Exemple-06</span><span style="color:#008080"></</span><span style="color:#3f7f7f">string</span><span style="color:#008080">></span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#008080"><</span><span style="color:#3f7f7f">string</span> <span style="color:#7f007f">name</span><span style="color:#000000">=</span><span style="font-style:italic;color:#2a00ff">"action_settings"</span><span style="color:#008080">></span><span style="color:#000000">Settings</span><span style="color:#008080"></</span><span style="color:#3f7f7f">string</span><span style="color:#008080">></span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#008080"><</span><span style="color:#3f7f7f">string</span> <span style="color:#7f007f">name</span><span style="color:#000000">=</span><span style="font-style:italic;color:#2a00ff">"section_format"</span><span style="color:#008080">></span><span style="color:#000000">Hello World from section: %1$d</span><span style="color:#008080"></</span><span style="color:#3f7f7f">string</span><span style="color:#008080">></span></span>
<span class="odt-code-line"><span style="color:#008080"></</span><span style="color:#3f7f7f">resources</span><span style="color:#008080">></span></span>
</code></pre></div>
- (suite)
- ligne (d) ci-dessus %1$d indique que l'argument n° 1 (%1) doit être formaté comme un nombre entier ($d) ;
- le second argument de [getString] est la valeur à donner à l'argument $1 de la ligne (d) ci-dessus ;
- [getArguments] donne la référence du bundle des arguments du fragment. Il faut se rappeler ici que chaque argument a été créé avec le bundle suivant (lignes f-h) :
<div class="odt-code-rich" data-linenums="true" style="counter-reset: odtline 0;"><pre><code class="language-text">
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#3f7f5f">// rend un fragment avec une information : le n° du fragment passé en paramètre</span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="font-weight:bold;color:#7f0055">public</span><span style="color:#000000"> </span><span style="font-weight:bold;color:#7f0055">static</span><span style="color:#000000"> PlaceholderFragment newInstance(</span><span style="font-weight:bold;color:#7f0055">int</span><span style="color:#000000"> sectionNumber) {</span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#3f7f5f">// fragment</span></span>
<span class="odt-code-line"><span style="color:#000000"> PlaceholderFragment fragment = </span><span style="font-weight:bold;color:#7f0055">new</span><span style="color:#000000"> PlaceholderFragment();</span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#3f7f5f">// info embarquée</span></span>
<span class="odt-code-line"><span style="color:#000000"> Bundle args = </span><span style="font-weight:bold;color:#7f0055">new</span><span style="color:#000000"> Bundle();</span></span>
<span class="odt-code-line"> args.putInt(ARG_SECTION_NUMBER, sectionNumber);</span>
<span class="odt-code-line"> fragment.setArguments(args);</span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="color:#3f7f5f">// résultat</span></span>
<span class="odt-code-line"><span style="color:#000000"> </span><span style="font-weight:bold;color:#7f0055">return</span><span style="color:#000000"> fragment;</span></span>
<span class="odt-code-line">}</span>
</code></pre></div>
- (suite)
- getArguments().getInt(ARG_SECTION_NUMBER) va donc rendre la valeur [sectionNumber] des lignes (g) et (b) ci-dessus ;
- ligne 31 : on rend la vue ainsi créée ;
1.7.4.3. Gestion du menu
Dans l'application générée, il y a un menu :
![]() |
Le contenu du fichier [menu_main.xml] est le suivant :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- lignes 1-9 : le menu ;
- lignes 5-8 : un élément du menu identifié par [action_settings] (ligne 5) ;
- ligne 6 : le label de l'option de menu. Elle est trouvée dans le fichier [res / values / strings.xml] (ligne (c) ci-dessous :
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
Le code précédent correspond au visuel suivant (le menu est en haut à droite de la fenêtre d'exécution Android) :
![]() | ![]() |
Ce menu est géré de la façon suivante dans l'activité [MainActivity] :
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- lignes 1-6 : cette méthode est appelée lorsque le système est prêt à créer le menu de l'application. Le paramètre d'entrée [Menu menu] est un menu vide n'ayant pas encore d'options ;
- ligne 4 : le fichier [res / menu / menu_main.xml] est exploité. L'objet [Menu menu] passé en paramètre se voit attribuer les options de menu définies dans ce fichier ;
- ligne 5 : on indique que la création du menu a été faite ;
- lignes 8-21 : la méthode [onOptionsItemSelected] est exécutée dès qu'une option du menu est cliquée ;
- ligne 13 : la référence de l'option de menu cliquée ;
- lignes 16-18 : si l'option cliquée est l'option d'identifiant [action_settings], rien n'est fait et on indique que l'événement a été traité (ligne 17) ;
- ligne 20 : on passe l'événement à la classe parent ;
Pour mieux voir ce qui se passe avec ce menu, nous ajoutons des logs dans le code précédent :
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
1.7.4.4. Le bouton flottant
La vue générée a un bouton flottant :
![]() |
Ce composant est défini dans la vue principale [activity-main.xml] :
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
La ligne 7 référence une image fournie par le support Android, celle d'une enveloppe.
Ce composant est géré dans la classe [MainActivity] de la façon suivante :
// bouton flottant
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- ligne 2 : on récupère la référence du bouton flottant dans la vue associée à l'activité (activity_main) ;
- lignes 3-9 : on lui associe un gestionnaire pour gérer le clic sur lui ;
- ligne 6 : la classe [Snackbar] permet d'afficher des messages éphémères sur la vue avec sa méthode [Snackbar.make]. Le 1er argument est une vue à partir de laquelle [Snackbar] va chercher une vue parente dans laquelle afficher le message. Ici [view] est la vue de l'enveloppe cliquée (ligne 5). La vue parente qui sera trouvée sera la vue [activity_main]. Le second argument est le message à afficher. Le troisième argument est la durée d'affichage (SHORT ou LONG) ;
- ligne 7 : on peut cliquer sur le message affiché et déclencher ainsi une action. Ici aucune action n'est associée au clic sur le message. Finalement, la méthode [show] affiche le message ;
Le clic sur le bouton flottant donne le résultat visuel suivant :
![]() |
1.7.5. Exécution du projet
Maintenant que nous avons expliqué les détails du code généré, nous pouvons mieux comprendre son exécution :

Lorsqu'on clique sur l'onglet n° i, le fragment n° i est affiché dans le conteneur de vues. Cela se voit au texte affiché en [4]. On peut remarquer aussi qu'on peut passer d'un onglet à l'autre en tirant la vue à droite ou à gauche avec la souris (swipe). Nous verrons qu'on peut contrôler ce comportement.
Lorsqu'on clique sur l'option de menu en [6], on a les logs suivants :
![]() |
1.7.6. Cycle de vie des fragments
![]() | ![]() |
- en [1], on voit que la méthode [onCreateView] et les suivantes sont exécutées lors du 1er affichage du fragment et à chaque fois que l'activité doit le réafficher ;
Pour suivre le cycle de vie de l'activité et des fragments, nous ajoutons les logs suivants dans le code de [MainActivity] :
// constructeur
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// parent
super.onCreate(savedInstanceState);
...
}
// un fragment
public static class PlaceholderFragment extends Fragment {
// un texte affiché dans le fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// rend un fragment avec une information : le n° du fragment passé en paramètre
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
Nous exécutons de nouveau le projet. Les premiers logs sont les suivants :
- ligne 1 : création de l'activité ;
- ligne 2 : exécution de sa méthode [onCreate] ;
- lignes 3-4 : instanciation du fragment n° 1 ;
- lignes 5-6 : instanciation du fragment n° 2 ;
- ligne 7 : initialisation du fragment n° 2 ;
- ligne 8 : initialisation du fragment n° 1 ;
- ligne 9 : création du menu de l'activité ;
Il faut se rappeler ici le code qui préside à la création des fragments :
// le gestionnaire de fragments
// c'est à lui qu'on demande les fragments à afficher dans la vue principale
// doit définir les méthodes [getItem] et [getCount] - les autres sont facultatives
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// on instancie un fragment [PlaceHolder] et on le rend
return PlaceholderFragment.newInstance(position + 1);
}
...
- lignes 11-15 : un fragment est instancié par [newInstance] à chaque fois que le conteneur de fragments en demande un ;
Les logs ci-dessus montrent que les deux premiers fragments ont été instanciés et initialisés.
Maintenant, cliquons sur l'onglet n° 2. Les nouveaux logs sont les suivants :
- lignes 1-3 : le fragment n° 3 est instancié et initialisé. On rappelle que c'est le fragment n° 2 qui est affiché ;
Maintenant, cliquons sur l'onglet n° 3. Là il n'y a aucun log. Probablement parce que le fragment n° 3 à afficher avait déjà été instancié. Maintenant, revenons à l'onglet n° 1. Les logs sont alors les suivants :
Le fragment n° 1 n'est pas instancié de nouveau mais sa méthode [onCreateView] est de nouveau exécutée. Ce comportement se reproduit pour les deux autres fragments.
De ces logs, on retiendra que :
- l'activité a été instanciée puis initialisée une fois ;
- que chaque fragment a été instancié une fois ;
- que la méthode [onCreateView] de chaque fragment a été exécutée plusieurs fois ;
Ce qu'il faut savoir et ce que confirment les logs est que par défaut, lorsqu'un fragment n° i est affiché, les fragments i-1 et i+1 sont instanciés, s'ils ne le sont pas déjà. C'est ce qui explique par exemple qu'au démarrage, alors qu'il faut afficher le fragment n° 1, ce sont les fragments 1 et 2 qui ont été instanciés et initialisés. Ce que montrent également les logs est que la méthode [getItem(i)] n'est appelée qu'une fois, même si le fragment n° i est lui affiché plusieurs fois. Ainsi il semble que le conteneur de fragments [ViewPager] qui doit afficher le fragment n° i demande celui-ci une fois au gestionnaire de fragments [SectionsPagerAdapter]. Ensuite il ne le redemande plus et continue à utiliser celui qu'il a obtenu.
Enfin, les logs donnent des indications sur la méthode [onCreateView] des fragments :
- au démarrage, les fragments 1 et 2 ont été instanciés et leur méthode [onCreateView] exécutée ;
- lorsqu'on passe du fragment 1 au fragment 2, la méthode [onCreateView] du fragment 2 n'est pas réexécutée. On ne peut donc s'en servir pour mettre à jour le fragment 2. Or l'utilisateur peut, avec le fragment 1, avoir fait une opération dont le résultat devrait être affiché par le fragment 2. On voit que la méthode [onCreateView] ne pourra pas être utilisée pour mettre à jour le fragment 2. Il faudra trouver une autre solution ;
1.8. Exemple-07 : Exemple-06 réécrit avec la bibliothèque [AA]
1.8.1. Création du projet
Nous allons dupliquer le projet [Exemple-06] dans [Exemple-07] pour introduire dans ce dernier les annotations Android. Pour cela, suivez la procédure du paragraphe 1.4. Nous obtenons le résultat suivant :
![]() | ![]() |
1.8.2. Configuration Gradle
![]() |
Nous faisons évoluer le fichier [build.gradle] de la façon suivante :
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
Nous avons ajouté la configuration nécessaire à l'utilisation de la bibliothèque [Android Annotations] (cf paragraphe 1.4).
1.8.3. Ajout des premières annotations AA
Nous allons créer des annotations AA dans [MainActivity] :
![]() |
La classe [MainActivity] évolue de la façon suivante :
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
// le conteneur de fragments
@ViewById(R.id.container)
protected MyPager mViewPager;
// le gestionnaire d'onglets
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// le bouton flottant
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// constructeur
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// barre d'outils
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// le gestionnaire de fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// le conteneur de fragments est associé au gestionnaire de fragments
// ç-à-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
mViewPager.setAdapter(mSectionsPagerAdapter);
// la barre d'onglets est également associée au conteneur de fragments
// ç-à-d que l'onglet n° i affiche le fragment n° i du conteneur
tabLayout.setupWithViewPager(mViewPager);
// bouton flottant
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- ligne 1 : l'annotation [@EActivity] fait de [MainActivity] une classe gérée par AA. Son paramètre [R.layout.activity_main] est l'identifiant de la vue [activity_main.xml] associée à l'activité ;
- lignes 11-12 : le composant identifié par [R.id.tabs] est injecté dans le champ [tabLayout]. C'est le gestionnaire d'onglets ;
- lignes 14-15 : le composant identifié par [R.id.fab] est injecté dans le champ [fab]. C'est le bouton flottant ;
- lignes 23-50 : le code qui était auparavant dans la méthode [onCreate] migre dans une méthode de nom quelconque mais annotée par [@AfterViews] (ligne 23). Dans la méthode ainsi annotée, on est assuré que tous les composants de l'interface visuelle annotés par [@ViewById] ont été initialisés ;
- on a mis par ailleurs des logs pour voir le cycle de vie de l'activité ;
On se rappelle que l'annotation [@EActivity] va générer une classe [MainActivity_] qui sera la véritable activité du projet. Il faut donc modifier le fichier [AndroidManifest.xml] de la façon suivante :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- ligne 12 : la nouvelle activité.
A ce point, exécutez de nouveau le projet et vérifiez que vous obtenez toujours l'interface avec onglets.
1.8.4. Réécriture des fragments
Nous allons revoir la gestion des fragments du projet. Pour l'instant la classe [PlaceholderFragment] est une classe interne statique de l'activité [MainActivity]. Nous allons revenir à un cas d'utilisation plus usuel, celui où les fragments sont définis dans des classes externes. Par ailleurs, nous introduisons les annotations AA pour les fragments.
Le projet [Exemple-07] évolue de la façon suivante :
![]() |
Ci-dessus, on voit apparaître la classe [PlaceholderFragment] qui a été externalisée en-dehors de la classe [MainActivity]. Elle est réécrite de la façon suivante :
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// composant de l'interface visuelle
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onResume();
// affichage
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- ligne 15 : le fragment est annoté avec l'annotation [@EFragment] dont le paramètre est l'identifiant de la vue XML associée au fragment, ici la vue [fragment_main.xml] ;
- lignes 19-20 : injectent dans le champ [textViewInfo] la référence du composant de [fragment_main.xml] identifié par [R.id.section_label] qui est un type [TextView] (ligne (l) ci-dessous) :
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- lignes 42-52 : la méthode [onResume] est exécutée avant l'affichage de la vue associée au fragment. On peut l'utiliser pour mettre à jour l'interface visuelle qui va être affichée ;
- ligne 47 : on doit appeler la méthode de même nom de la classe parent ;
- ligne 49 : il y a une difficulté à savoir si la méthode [onResume] peut être ou non exécutée avant l'initialisation du champ de la ligne 20. Les logs mis pour suivre le cycle de vie du fragment nous le diront. Pour l'instant et par précaution, on fait un test de nullité ;
- ligne 51 : on met à jour l'information du champ [textViewInfo] avec l'argument entier passé au fragment lors de sa création ;
La classe [MainActivity] perd sa classe interne [PlaceholderFragment] et voit son gestionnaire de fragments évoluer de la façon suivante :
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private Fragment[] fragments;
// nombre de fragments
private static final int FRAGMENTS_COUNT = 3;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialisation du tableau des fragments
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
// on crée un fragment
fragments[i] = new PlaceholderFragment_();
// on peut passer des arguments au fragment
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// fragment n° position
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// rend le nombre de fragments gérés
@Override
public int getCount() {
return fragments.length;
}
// facultatif - donne un titre aux fragments gérés
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- ligne 4 : les fragments sont mis dans un tableau ;
- lignes 16-23 : l'initialisation du tableau des fragments se fait dans le constructeur. Ils sont de type [PlaceholderFragment_] (ligne 18) et non [PlaceholderFragment]. La classe [PlaceholderFragment] a en effet été annotée par une annotation AA et va donner naissance à une classe [PlaceholderFragment_] dérivée de [PlaceholderFragment] et c'est cette classe que l'activité doit utiliser. Chaque fragment créé se voit passer un argument entier qui sera affiché par le fragment ;
- lignes 42-45 : on a changé les titres des fragments. Comme ceux-ci sont également les titres des onglets, on devrait voir un changement dans la barre des onglets ;
Compilons [Make] [1] ce projet :
![]() | ![]() |
- en [2], on voit que les classes générées par la bibliothèque AA le sont dans le dossier [app / build / generated / source / apt / debug] (il faut être dans la perspective [Project] pour voir [2]) ;
Exécutez le projet [Exemple-07] et vérifiez qu'il fonctionne toujours.
1.8.5. Examen des logs
Lorsqu'on lance l'application, les logs sont les suivants :
- ligne 1 : construction de l'unique activité ;
- ligne 2 : méthode [afterViews] de l'activité : ses champs annotés par [@ViewById] sont initialisés ;
- lignes 3-5 : construction des trois fragments ;
- lignes 6-7 : le conteneur de fragments [ViewPager] demande les deux premiers fragments ;
- lignes 8-9 : méthodes du fragment 2 ;
- lignes 10-11 : méthodes du fragment 1 ;
- lignes 12-13 : méthode [onResume] du fragment 1 ;
- lignes 14-15 : méthode [onResume] du fragment 2 ;
- ligne 16 : création menu de l'activité ;
On notera qu'on a la réponse à une question posée précédemment : la méthode [onResume] du fragment 1 par exemple (ligne 12) s'exécute après la méthode [afterViews] du fragment (ligne 11). Donc lorsque la méthode [onResume] s'exécute, elle peut utiliser les champs annotés par [@ViewById]. Nous pourrons donc désormais écrire la méthode [onResume] de la façon suivante :
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onResume();
// affichage
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
Maintenant passons de l'onglet 1 à l'onglet 2. Les nouveaux logs sont les suivants :
- ligne 1 : le conteneur de fragments [ViewPager] demande le fragment n° 3 ;
- lignes 2-3 : méthodes du fragment n° 3. On rappelle que ce fragment avait été instancié dès le démarrage de l'application ;
- lignes 4-5 : la méthode [onResume] du fragment n° 3 est exécutée. On rappelle que c'est le fragment n° 2 qui est affiché ;
Maintenant passons de l'onglet 2 à l'onglet 3. Il n'y a aucun log. Donc aucune des méthodes [onCreateView, afterViews, onResume] du fragment n° 3 n'est exécutée. Il affiche correctement le texte [Hello World from section:3] uniquement parce que ce texte avait déjà été créé à l'étape précédente lors de l'affichage du fragment n° 2. On se rappelle en effet qu'à cette étape, la méthode [onResume] du fragment n° 3 avait été exécutée. On se rend compte ici que pas plus que la méthode [onCreateView], la méthode [onResume] ne peut être utilisée pour mettre à jour le fragment 3. S'il avait fallu changer le texte affiché par le fragment, aucune de ces deux méthodes ne pouvait le faire.
Maintenant, revenons de l'onglet n° 3 à l'onglet n° 1. Les logs sont alors les suivants :
On voit que toutes les méthodes du fragment 1 ont été exécutées. On voit que la méthode getItem n'a pas été appelée. On l'a dit, cette méthode n'est appelée qu'une fois pour chaque fragment ;
Maintenant, passons de l'onglet 1 à l'onglet adjacent 2. On a les logs suivants :
Etonnant non ? Toutes les méthodes du fragment n° 3 sont réexécutées.
Pour comprendre ces phénomènes, il faut se rappeler que par défaut, lorsque le conteneur de fragments va afficher le fragment i, il initialise les fragments i-1, i , i+1. Relisons les logs à la lumière de cette information.
Tout d'abord les logs au démarrage de l'application :
Parce que le conteneur de fragments va afficher le fragment 1, les fragments 1 et 2 sont initialisés (lignes 8-15).
On passe maintenant de l'onglet 1 à l'onglet 2 :
Parce que le conteneur de fragments va afficher le fragment 2, les fragments 1, 2 et 3 doivent être initialisés. Les fragments 1 et 2 le sont déjà depuis l'étape précédente. Le fragment 3 est initialisé au lignes 2-5.
On passe de l'onglet 2 à l'onglet 3. Il n'y a pas de logs. Parce que le conteneur de fragments va afficher le fragment 3, les fragments 2 et 3 doivent être initialisés. Or depuis l'étape précédente, ils le sont déjà. Ce qu'on ne voit pas ici, c'est que le fragment 1 qui n'est pas adjacent au fragment 3 perd son état qui n'est pas conservé en mémoire.
On passe de l'onglet 3 à l'onglet 1. Les logs sont les suivants :
Parce que le conteneur de fragments va afficher le fragment 1, le fragment 2 doit lui aussi être initialisé. Il l'est depuis l'étape précédente. Dans cette même étape, l'état du fragment 1 avait été perdu. Il est donc réinitialisé aux lignes 1-4. Ce qu'on ne voit pas ici, c'est que le fragment 3 qui n'est pas adjacent au fragment 1 perd son état qui n'est alors pas conservé en mémoire.
Lorsqu'on passe de l'onglet 1 à l'onglet adjacent 2, on a les logs suivants :
Parce que le conteneur de fragments va afficher le fragment 2, les fragments 1, 2 et 3 doivent être initialisés. Les fragments 1 et 2 le sont déjà depuis l'étape précédente. Le fragment 3 est initialisé au lignes 1-4.
Qu'avons-nous appris ?
- que la gestion par défaut des fragments est très particulière et qu'il faut la connaître si on ne veut pas perdre ses cheveux. On peut changer ce mode de gestion et nous le ferons un peu plus loin ;
- qu'avec cette gestion par défaut, aucune des méthodes [onCreateView, onResume] ne peut être utilisée pour mettre à jour le fragment qui va être affiché car on n'est pas sûr qu'elles vont être exécutées ;
1.8.6. onDestroyView
La méthode [onDestroyView] fait partie du cycle de vie des fragments (cf paragraphe 1.7.6) :
![]() | ![]() |
On voit que dans le cycle de vie d'un fragment :
- la méthode [onCreateView] peut être exécutée plusieurs fois ;
- avant de retourner à la méthode [onCreateView] ultérieurement, il y a forcément un passage par la méthode [onDestroyView] [2] ;
Nous allons insérer ces méthodes dans les fragments pour mieux suivre leur cycle de vie. Le code du fragment devient le suivant :
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
super.onDestroyView();
}
}
Exécutons l'application. Les premiers logs sont les suivants :
- ligne 1 : construction de l'unique activité ;
- ligne 2 : méthode [afterViews] de l'activité : ses champs annotés par [@ViewById] sont initialisés ;
- lignes 3-5 : construction des trois fragments ;
- lignes 6-7 : le conteneur de fragments [ViewPager] demande les deux premiers fragments ;
- lignes 8-9 : la vue du fragment 2 est créée (pas forcément rendue visible) ;
- lignes 10-11 : la vue du fragment 1 est créée (pas forcément rendue visible) ;
- lignes 12-13 : méthode [onResume] du fragment 1 ;
- lignes 14-15 : méthode [onResume] du fragment 2 ;
- ligne 16 : création menu de l'activité ;
Passons de l'onglet 1 à l'onglet 3 :
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- ligne 1 : le conteneur de fragments demande le 3ième fragment ;
- lignes 2-3 : la vue du fragment 3 est créée (pas forcément affichée) ;
- lignes 4-5 : la méthode [onResume] du fragment 3 est exécutée ;
- ligne 6 : la méthode [onDestroyView] du fragment 1 est exécutée. Cela implique que lorsque l'utilisateur va revenir au fragment 1 ou à un fragment adjacent, le cycle de vie de ce fragment va être réexécuté ;
On revient de l'onglet 3 à l'onglet 1 :
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- lignes 1-4 : le cycle de vie du fragment 1 est réexécuté parce qu'il avait subi un [onDestroyView] ;
- ligne 5 : c'est maintenant le fragment 3 qui voit sa méthode [onDestroyView] exécutée. Là encore, lorsque l'utilisateur va revenir au fragment 3 ou à un fragment adjacent, le cycle de vie de ce fragment va être réexécuté ;
1.8.7. setUserVisibleHint
La méthode [onCreateView] du cycle de vie instancie la vue associée au fragment mais ne la rend pas forcément visible. C'est ce que nous allons voir maintenant. La méthode [Fragment.setUserVisibleHint] est exécutée à chaque fois que la visibilité du fragment change. On ajoute cette méthode au code du fragment :
package exemples.android;
....
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// composant de l'interface visuelle
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
Au démarrage, les logs sont les suivants :
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- les logs des lignes 7, 9-10 montrent que seul le fragment 1 devient visible. On voit également qu'il devient visible avant exécution de sa méthode [onCreateView] ;
Passons de l'onglet 1 à l'onglet 2 :
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- le fragment 1 est caché (ligne 3), le fragment 2 est affiché (ligne 4) ;
Passons de l'onglet 2 à l'onglet 3 :
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- le fragment 2 est caché (ligne 1), le fragment 3 est affiché (ligne 2) ;
Revenons à l'onglet 1 :
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- le fragment 3 est caché (ligne 2), le fragment 1 est affiché (ligne 3) ;
Qu'avons-nous appris ?
- la méthode [setUserVisibleHint] est exécutée une fois avec la propriété [isVisibleToUser] à true, pour le fragment qui va être affiché ;
- on ne peut pas situer quand va être exécutée cette méthode par rapport au cycle de vie du fragment. Ainsi, pour le fragment 1, la méthode [setUserVisibleHint, true] a été exécutée avant la méthode [onCreateView] du début du cycle de vie de ce fragment, alors que pour les fragments 2 et 3 c'est l'inverse qui s'est produit ;
1.8.8. setOffscreenPageLimit
Les logs précédents montrent que lorque le conteneur de fragments [ViewPager] s'apprête à afficher le fragment n° i, il exécute, si ce n'est déjà fait, le cycle de vie des fragments adjacents i-1 et i+1. Ce fonctionnement peut être contrôlé par la méthode [ViewPager].setOffscreenPageLimit :
Avec l'instruction ci-dessus,
- lorque le conteneur de fragments [ViewPager] s'apprête à afficher le fragment n° i, il exécute, si ce n'est déjà fait, le cycle de vie des fragments adjacents de l'intervalle [i-n, i+n] ;
- si on affiche ensuite le fragment j :
- le même phénomène se reproduit pour les fragments adjacents de l'intervalle [j-n, j+n] ;
- les fragments initialisés lors de l'étape 1 et qui ne sont plus dans l'adjacence [j-n, j+n] du nouveau fragment, peuvent alors subir une opération [onDestroyView]. Néanmoins, j'ai pu observer sur d'autres applications, notamment celle du chapitre 3, que ce n'était pas systématiquement le cas ;
Nous modifions la méthode [MainActivity.afterViews] de la façon suivante :
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// barre d'outils
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// le gestionnaire de fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// le conteneur de fragments est associé au gestionnaire de fragments
// ç-à-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
mViewPager.setAdapter(mSectionsPagerAdapter);
// on inhibe le swipe entre fragments
mViewPager.setSwipeEnabled(false);
// offset des fragments
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// la barre d'onglets est également associée au conteneur de fragments
// ç-à-d que l'onglet n° i affiche le fragment n° i du conteneur
tabLayout.setupWithViewPager(mViewPager);
// bouton flottant
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- ligne 20 : nous mettons le nombre de fragments adjacents à initialiser, au nombre total de fragments -1. Ainsi, au démarrage, lorsque le conteneur de fragments va afficher le fragment n° 1, il va en même temps initialiser les fragments 2, 3, ..., n avec n=1+mSectionsPagerAdapter.getCount() - 1=mSectionsPagerAdapter.getCount(). Ce sont donc tous les fragments qui vont être initialisés. Lorsque la fenêtre d'affichage va se déplacer sur un autre fragment, le conteneur de fragments :
- va découvrir que tous les fragments adjacents du nouveau fragment sont déjà initialisés et il ne fera donc pas leur initialisation ;
- comme l'adjacence du nouveau fragment couvre elle-aussi la totalité des fragments, aucun ne sera « désinitialisé » par le conteneur de fragments ;
Au total, on devrait voir tous les fragments instanciés et initialisés au démarrage de l'application et puis plus jamais ensuite. C'est ce que nous vérifions maintenant en examinant les logs.
Au démarrage, nous avons les logs suivants :
- lignes 4-6 : construction des trois fragments ;
- lignes 7, 9, 11 : le conteneur de fragments réclame les trois fragments. Dans la version précédente, il en réclamait deux ;
- lignes 14-25 : le cycle de vie des trois fragments s'exécute ;
Passons maintenant de l'onglet 1 à l'onglet 2 :
Passons de l'onglet 2 à l'onglet 3 :
Puis de l'onglet 3 à l'onglet 1 :
Les logs confirment la théorie. Tous les fragments ont été instanciés et initialisés au démarrage. Ensuite les méthodes de leur cycle de vie ne sont plus exécutées. On a là un fonctionnement très prévisible des fragments qui facilite énormément leur utilisation.
Ce que nous voulons trouver, c'est un moyen de mettre à jour un fragment qui va être affiché, ceci quelque soit l'adjacence de fragments choisie par le développeur. Les logs nous ont montré deux choses :
- la méthode [setUserVisibleHint, true] est toujours exécutée pour le fragment qui va être affiché et pas pour les autres ;
- cette événement peut se produire avant ou après le cycle de vie du fragment. Cela dépend de l'adjacence de fragments choisie par le développeur. C'est un problème, car si le cycle de vie n'a pas encore eu lieu, cela signifie que le fragment ne peut pas être mis à jour par la méthode [setUserVisibleHint, true] ;
Les logs au démarrage de l'application lorsque l'adjacence des fragments était 1 ont été les suivants :
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- on voit que lorsque le fragment 1 devient visible, sa vue n'a pas encore été créée. On ne peut donc y toucher. Cela pourra alors se faire au cours du cycle de vie du fragment, par exemple dans les méthodes [onCreateView] (ligne 11) ou [onResume] (lignes 13-14). Comme nous utilisons les annotations AA, nous n'avons normalement pas à écrire la méthode [onCreateView]. C'est donc la méthode [onResume] qui semble la plus adaptée ici pour mettre à jour le fragment 1 ;
Lorsque nous sommes passés de l'onglet 1 à l'onglet 2, les logs ont été les suivants :
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
Cette fois, on n'a que la méthode [setUserVisibleHint, true] de la ligne 4 pour mettre à jour le fragment 2 ;
Lorsque nous sommes passés de l'onglet 2 à l'onglet 3, les logs ont été les suivants :
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
Ici, on n'a que la méthode [setUserVisibleHint, true] de la ligne 2 pour mettre à jour le fragment 3 ;
Lorsque nous sommes passés de l'onglet 3 à l'onglet 1, les logs ont été les suivants :
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
Ici, il faut utiliser la méthode [onResume] du fragment 1 (lignes 6-7) pour mettre à jour le fragment 1.
Donc sur cet exemple, on voit que pour mettre à jour un fragment qui va s'afficher, on dispose de deux méthodes : [setUserVisibleHint] et [onResume].
Nous allons mettre en oeuvre cette solution dans un nouveau projet où chaque fragment devra afficher le nombre de fois où il a été affiché, ce qu'on appellera une visite. Il faudra donc mettre à jour son affichage à chaque fois qu'il sera affiché. C'est bien le problème qu'on cherche à résoudre.
Auparavant, nous examinons la dernière étape de la vie d'une activité ou d'un fragment, celle où il est détruit. Le système peut prendre l'initiative de supprimer une activité si d'autres activités plus prioritaires réclament des ressources indisponibles. Pour libérer celles-ci, le système va prendre l'intiative de supprimer certaines activités. La méthode [onDestroy] de l'activité et des fragments va alors être appelée.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
Nous allons permettre à l'utilisateur de supprimer l'activité au moyen d'une option de menu [5]. Pour cela, nous ajoutons une nouvelle option de menu dans le fichier [menu_main.xml] [1] :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
On fait un simple copier / coller de la 1ère option de menu et on adapte le résultat (lignes 9 et 10). Le libellé de cette nouvelle option est ajouté dans le fichier [strings.xml] [2] :
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
Enfin, dans la classe [MainActivity], on gère le clic sur l'option [Terminate] :
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//on termine l'activité
finish();
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
- lignes 14-19 : on fait un copier / coller des lignes 10-13 et on adapte le code à la nouvelle option ;
- ligne 17 : l'activité est terminée par action logicielle ;
Maintenant exécutons cette nouvelle version, puis dès que la première vue est affichée, cliquons sur l'option de menu [Terminate]. Les logs sont alors les suivants :
- lignes 1-2 : clic sur l'option [Terminate] ;
- ligne 4 : la méthode [onDestroy] de l'activité est appelée ;
- lignes 4-5 : la méthode [onDestroyView] du fragment 1 est appelée puis sa méthode[onDestroy] ;
- lignes 6-9 : cette opération se répète pour les deux autres fragments ;
On se rappellera donc que la méthode [onDestroy] de l'activité et des fragments est appelée lorsque l'activité va être supprimée par le système, le développeur ou l'utilisateur. On peut utiliser cette méthode pour sauvegarder des informations, par exemple localement sur la tablette afin de les retrouver lorsque l'utilisateur relancera de nouveau l'application.
1.9. Exemple-08 : mise à jour d'un fragment avec une adjacence de fragments variable
1.9.1. Création du projet
On duplique le projet [Exemple-07] dans [Exemple-08]. Pour cela, on suivra la procédure décrite pour dupliquer [Exemple-02] dans [Exemple-03] au paragraphe 1.4.
![]() | ![]() |
1.9.2. Réécriture du fragment [PlaceholderFragment]
Le nouveau code du du fragment [PlaceholderFragment] est le suivant. Il fonctionne quelque soit l'adjacence donnée aux fragments (1, partielle, totale) :
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// composant de l'interface visuelle
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// texte initial
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// init done
initDone = true;
}
// affichage texte courant
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// update fragment
public void update() {
// le travail à faire dépend du n° de la visite
if (numVisit > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// texte modifié
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// infos locales pour logs
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- lignes 34-48 : la méthode [@AfterViews] est susceptible d'être exécutée plusieurs fois. On s'en servait pour initialiser le texte du fragment (ligne 42). On le fait toujours mais pour ne le faire qu'une fois, on gère un booléen [initDone] (ligne 44) pour indiquer que l'initialisation a été faite et qu'elle n'est pas à refaire ;
- lignes 56-59 : nous introduisons la méthode [onDestroyView] pour noter le fait que la prochaine fois que le fragment va être réaffiché, son cycle de vie va être réexécuté ;
- les logs ont montré que deux méthodes peuvent s'exécuter après la méthode [@AfterViews] : les méthodes [setUserVisibleHint] et [onResume]. La méthode [onResume] n'est exécutée que lorsque le cycle de vie du fragment est exécuté. La méthode [setUserVisibleHint] elle, n'est pas toujours exécutée après la méthode [@AfterViews]. Les logs ont montré qu'au moins l'une des deux est exécutée après la méthode [@AfterViews]. Les logs n'ont jamais montré que les deux pouvaient être exécutées ensemble après la méthode [@AfterViews]. C'est soit l'une, soit l'autre. Par précaution, on positionnera un booléen [updateDone] lorsqu'une mise à jour a été faite ;
Les méthodes [setUserVisibleHint] et [onResume] sont les suivantes :
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// mémoire
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// nombre de visites
if (isVisibleToUser) {
// incrément
numVisit++;
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// le fragment va être caché
updateDone = false;
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// update
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- ligne 14 : on mémorise l'état visible ou non du fragment ;
- lignes 22-25 : si le fragment est visible et que la méthode [@AfterViews] a été exécutée, la méthode [update] est exécutée et le booléen [updateDone] passé à true ;
- lignes 26-28 : si le fragment va être caché, on remet le booléen [updateDone] à false. Il nous faut en effet un événement pour réinitialiser à false le booléen [updateDone] mis à true dès que la méthode [update] est appelée afin que de nouvelles mises à jour puissent se faire. Nous utilisons le fait que le fragment n'est plus visible pour le faire. Lorsqu'il redeviendra visible, la mise à jour du fragment devra se faire de nouveau ;
- lignes 32-42 : les logs montrent que selon l'adjacence choisie pour les fragments, la méthode [onResume] peut s'exécuter alors que le fragment n'est pas visible. S'il n'est pas visible, on ne fait pas la mise à jour (ligne 39) et comme on l'a fait pour [setMenuVisibility], on gère le booléen [updateDone].
Enfin, la méthode [onDestroyView] est la suivante :
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// mise à jour indicateur
afterViewsDone = false;
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
La méthode [onDestroyView] est exécutée lorsqu'un cycle de vie du fragment prend fin. Un autre cycle pourra reprendre ultérieurement.
- ligne 6 : la méthode [onDestroyView] supprime tout lien avec la vue attachée au fragment. Il sera recréé au prochain cycle de vie du fragment. Pour l'instant, il nous faut mettre le booléen [afterViews] à false, pour indiquer que le lien avec la vue n'existe plus ;
Nous allons exécuter l'application avec 5 fragments ayant une adjacence de 2. Les modifications sont faites dans [MainActivity] :
// nombre de fragments
private final int FRAGMENTS_COUNT = 5;
// adjacence des fragments
private final int OFF_SCREEN_PAGE_LIMIT=2;
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// offset des fragments
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
Les logs au démarrage sont les suivants :
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- lignes 8, 10, 12 : le conteneur de fragments réclame tous les fragments adjacents au fragment 1 ;
- lignes 9, 11, 13 : la méthode [setUserVisibleHint] de ces fragments est exécutée avec [visibleToUser] à false ;
- ligne 14 : la méthode [setUserVisibleHint] du fragment 1 est exécutée avec [visibleToUser] à true ;
- lignes 15-17 : la méthode [afterViews] des 3 segments adjacents est appelée. On voit donc ici un cas où cette méthode est appelée après qu'un fragment soit devenu visible (le fragment 1 ligne 14) ;
- lignes 18-20 : la méthode [onResume] des 3 segments adjacents est appelée ;
On passe de l'onglet 1 à l'onglet 2 :
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- parce que l'adjacence de fragments est décalée d'une position vers la droite, le fragment 4 est réclamé par le conteneur de fragments ;
- ligne 2 : la méthode [setUserVisibleHint] du fragment 4 est appelée avec [visibleToUser] à false ;
- ligne 3 : la méthode [setUserVisibleHint] du fragment 1 est appelée avec [visibleToUser] à false. En effet, le fragment 1 est désormais caché ;
- ligne 4 : la méthode [setUserVisibleHint] du fragment 2 est appelée avec [visibleToUser] à true. Le fragment 2 est désormais visible ;
- lignes 5-6 : le cycle de vie du fragment 4 se poursuit ;
On passe de l'onglet 2 à l'onglet 3 :
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- parce que l'adjacence de fragments est décalée d'une position vers la droite, le fragment 5 est réclamé par le conteneur de fragments ;
- ligne 2 : la méthode [setUserVisibleHint] du fragment 5 est appelée avec [visibleToUser] à false ;
- ligne 3 : la méthode [setUserVisibleHint] du fragment 2 est appelée avec [visibleToUser] à false. En effet, le fragment 2 est désormais caché ;
- ligne 4 : la méthode [setUserVisibleHint] du fragment 3 est appelée avec [visibleToUser] à true. Le fragment 3 est désormais visible ;
- lignes 5-6 : le cycle de vie du fragment 5 se poursuit ;
On passe de l'onglet 3 à l'onglet 4 :
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- ligne 1 : le fragment 3 est désormais caché ;
- ligne 2 : le fragment 4 est désormais visible. On notera qu'il n'y a pas exécution du cycle de vie du fragment 4. Celui-ci a déjà été fait deux étapes précédemment ;
- ligne 3 : le fragment 1 sort de l'adjacence du fragment 4 affiché. Sa méthode [onDestroyView] est exécutée. La prochaine fois qu'il sera affiché, son cycle de vue [onCreateView, afterViews, onResume] sera réexécuté ;
On passe de l'onglet 4 à l'onglet 5 :
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- ligne 1 : le fragment 4 est désormais caché ;
- ligne 2 : le fragment 5 est désormais visible. On notera qu'il n'y a pas exécution du cycle de vie du fragment 5. Celui-ci a déjà été fait 2 étapes précédemment ;
- ligne 3 : le fragment 2 sort de l'adjacence du fragment 5 affiché. Sa méthode [onDestroyView] est exécutée ;
On passe de l'onglet 5 à l'onglet 1 :
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- lignes 1, 4, 5, 6 : le cycle de vie du fragment 1 est réexécuté. En effet, il avait perdu la connexion avec sa vue ;
- lignes 2, 5, 8, 9 : pour la même raison le cycle de vie du fragment 2 est réexécuté ;
- lignes 10-11 : les fragments 4 et 5 sortent de l'adjacence du fragment affiché ;
- ligne 7 : le fragment 1 est mis à jour ;
![]() |
Les logs n'ont jamais montré que les méthodes [setUserVisibleHint] et [onResume] tentaient toutes les deux de mettre à jour le fragment. C'est soit l'une, soit l'autre. Le lecteur est invité à faire d'autres tests et de suivre les logs pour bien comprendre la notion d'adjacence et de cycle de vie des fragments.
Maintenant, mettons une adjacence totale et faisons les mêmes tests.
Dans [MainActivity] :
// nombre de fragments
private final int FRAGMENTS_COUNT = 5;
// adjacence des fragments
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
Les logs au démarrage sont les suivants :
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- les logs montrent que le cycle de vie des 5 fragment est exécuté ;
- le fragment 1 est affiché ligne 18 ;
On passe de l'onglet 1 à l'onglet 2 :
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 1 est caché ;
- ligne 2 : le fragment 2 est affiché ;
On passe de l'onglet 2 à l'onglet 3 :
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 2 est caché ;
- ligne 2 : le fragment 3 est affiché ;
On passe de l'onglet 3 à l'onglet 4 :
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 3 est caché ;
- ligne 2 : le fragment 4 est affiché ;
On passe de l'onglet 4 à l'onglet 5 :
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 4 est caché ;
- ligne 2 : le fragment 5 est affiché ;
On passe de l'onglet 5 à l'onglet 1 :
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 5 est caché ;
- ligne 2 : le fragment 1 est affiché ;
- ligne 3 : le fragment 1 est mis à jour ;
On passe de l'onglet 1 à l'onglet 4 :
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- ligne 1 : le fragment 1 est caché ;
- ligne 2 : le fragment 4 est affiché ;
- ligne 3 : le fragment 4 est mis à jour ;
On remarque, qu'avec l'adjacence totale, le comportement des fragments est beaucoup plus prévisible.
Maintenant, mettons une adjacence nulle et voyons ce qui se passe. La classe [MainActivity] évolue comme suit :
// nombre de fragments
private final int FRAGMENTS_COUNT = 5;
// adjacence des fragments
private final int OFF_SCREEN_PAGE_LIMIT = 0;
Les logs au démarrage sont les suivants :
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- lignes 8 et 10, on voit que le conteneur de fragments a réclamé 2 fragments, les n°s 1 et 2. Tout se passe donc comme s'il y avait une adjacence de 1. L'adjacence de 0 a donc été ignorée.
1.9.3. Communication inter-fragments
Dans l'architecture précédente, nous avons une activité et n fragments. L'utilisateur interagit avec les différents fragments. Ces interactions modifient l'état de l'application. On appelle ici état de l'application, l'ensemble des informations qu'elle mémorise tout au long de sa vie. Le problème suivant se pose alors :
- lorsque l'utilisateur interagit avec le fragment i, l'application passe d'un état E1 à un état E2 ;
- une action de l'utilisateur sur le fragment i fait afficher le fragment j ;
- comment mettre à jour le fragment j avec l'état actuel E2 de l'application ;
Des exemples précédents, nous savons comment mettre à jour le fragment j. Mais où trouver l'état E2 de l'application pour le mettre à jour ?
Il y a différentes solutions à ce problème. Nous en avons vu une : le fragment i peut transmettre l'état E2 de l'application au fragment j via des arguments. Nous avons rencontré cette méthode dans la classe [MainActivity] lors de la création des fragments :
for (int i = 0; i < fragments.length; i++) {
// on crée un fragment
fragments[i] = new PlaceholderFragment_();
// on peut passer des arguments au fragment
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
Cette solution n'est pas immédiatement utilisable ici. En effet, lorsque l'utilisateur clique sur l'onglet j qui va faire apparaître le fragment j, notre code n'est pas appelé. C'est uniquement du code système qui s'exécute. Nous verrons dans un prochain projet comment intercepter le clic sur un onglet, mais pour l'instant nous allons adopter une autre voie.
Nous avons parlé d'état de l'application : l'ensemble des données gérées par l'application au fil du temps. Ici l'application est constituée d'une activité et de n fragments tous instanciés une unique fois au démarrage de l'application et dont la durée de vie est celle de l'application. Donc chacun de ces éléments ou plusieurs ensemble peuvent être candidats pour stocker l'état de l'application. Chaque fragment a accès par la méthode [Fragment.getActivity()], à l'activité qui l'a créé . Tous les fragments ayant accès à l'activité, il semble naturel de stocker l'état de l'application dans celle-ci.
Cependant le résultat de la méthode [Fragment.getActivity()] dépend du moment où elle est appelée dans le cycle de vie. Nous illustrons ce point en ajoutant quelques logs dans la classe [PlaceholderFragment] :
// update fragment
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// le travail à faire dépend du n° de la visite
if (numVisit > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// texte modifié
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// infos locales pour logs
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- lignes 14-16 : la méthode [getInfos] affiche partie de l'état de l'application ;
Nous lançons l'application avec une adjacence de fragments de 2. Les logs au démarrage de l'application :
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
- lignes 9, 10, 13, 14 : on voit que dans les méthodes [setUserVisibleHint], on a [getActivity()==null] si le fragment n'est pas encore visible (isVisibleToUser==false) ;
- ligne 19 : on voit que lorsque le flux d'exécution arrive à la méthode [update] du fragment 1, la méthode [getActivity] rend bien l'activité ;
Lorsqu'on met l'adjacence de fragments à 4 (adjacence totale), les logs sont les suivants :
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepting and dropping
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours
On a les mêmes résultats. On en déduit que dès que le fragment est visible la méthode [getActivity] rend l'activité du fragment. On remarque également que lorsque l'exécution atteint la méthode [update] du fragment qui va s'afficher, la méthode [getActivity] rend bien une valeur.
Pour illustrer la communication inter-fragments nous construisons un nouveau projet.
1.10. Exemple-09 : communication inter-fragments, swipe et scrolling
1.10.1. Création du projet
On duplique le projet [Exemple-07] dans [Exemple-08]. Pour cela, on suivra la procédure décrite pour dupliquer [Exemple-02] dans [Exemple-03] au paragraphe 1.4.
![]() | ![]() |
1.10.2. La session
Dans ce nouveau projet, nous voulons que les fragments affichent le nombre total de fragments affichés par l'utilisateur. Il faut ici entretenir un compteur qui soit accessible à tous les fragments. Nous appellerons session l'objet encapsulant les données partagées par les fragments. Cette terminologie vient du développement web, où on met dans une session les données à partager par différentes vues demandées par un même utilisateur. Le fait d'encapsuler les informations partagées par les différents fragments dans un même objet rend les choses plus lisibles.
La classe [Session] sera la suivante :
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nombre de fragments visités
private int numVisit;
// getters et setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- ligne 8 : la session encapsulera le nombre de fragments visités ;
- ligne 5 : l'annotation [EBean] est une annotation AA. L'attribut [scope] désigne la portée (ou durée de vie) de la classe ainsi annotée. Ici l'attribut [scope = EBean.Scope.Singleton] fait que la classe [Session] est un singleton : elle va être instanciée une fois et une fois seulement au démarrage de l'application. La référence d'une classe annotée [EBean] peut ensuite être injectée dans une autre classe. C'est la notion d'injection de dépendances ;
1.10.3. L'activité [MainActivity]
L'activité [MainActivity] évolue de la façon suivante :
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// injection session
@Bean(Session.class)
protected Session session;
// nombre de fragments
private final int FRAGMENTS_COUNT = 5;
// adjacence des fragments
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// initialisation session
session.setNumVisit(0);
}
...
- lignes 7-8 : injection de la référence au singleton de la session grâce à l'annotation [@Bean]. Le paramètre de l'annotation est la classe du bean à injecter. Le champ ainsi annoté ne peut pas avoir la portée [private] ;
- ligne 15 : l'annotation [@AfterInject] sert à désigner une méthode à appeler lorsque toutes les injections de la classe auront été faites. Ainsi lorsqu'on entre dans la méthode [afterInject] de la ligne 16, le référence de la ligne 8 a été initialisée ;
- ligne 20 : on met le compteur de visites à zéro ;
1.10.4. Le fragment [PlaceholderFragment]
Le fragment [PlaceholderFragment] évolue de la façon suivante :
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// session
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// mémoire
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// nombre de visites
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// le fragment va être caché
updateDone = false;
}
}
// update fragment
public void update() {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// session
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texte modifié
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- ligne 7 : la session ;
- lignes 35-37 : nous savons que lorsqu'on arrive dans la méthode [update], la méthode [getActivity] rend bien l'activité. On en profite pour récupérer la session et la mémoriser localement (ligne 36) ;
- lignes 39-41 : pour incrémenter le n° de la visite, on va chercher celui-ci dans la session. On aurait pu mettre ce code dans la méthode [setUserVisibleHint] à partir de la ligne 19 car on sait qu'alors la méthode [getActivity] rend l'activité. On décide ici de ne pas faire jouer de rôle particulier à cette méthode et de faire migrer le code spécifique à un fragment dans la méthode [update] de celui-ci qui est faite pour cela ;
- ligne 43 : affiche le n° de la visite ;
Lorsqu'on exécute cette application avec 5 fragments, une adjacence de 2 fragments, les premiers logs sont les suivants :
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- lignes 2-3 : on voit que la méthode [afterInject] de l'activité est exécutée avant sa méthode [afterViews] ;
Le lecteur est invité à tester cette nouvelle application.
1.10.5. Désactiver le Swipe ou Balayage
Dans l'application précédente, lorsqu'on balaie l'émulateur Android avec la souris vers la gauche ou la droite, la vue courante laisse alors place à la vue de droite ou de gauche selon les cas. Ce comportement par défaut n'est pas toujours souhaitable. Nous allons apprendre à désactiver le balayage des vues (swipe).
Revenons sur la vue XML principale [activity_main] :
![]() |
Dans le code XML de la vue on trouve celui du conteneur de fragments :
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
La ligne 1 désigne la classe qui gère les pages de l'activité. On retrouve cette classe dans l'activité [MainActivity] :
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
// le conteneur de fragments
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
Ligne 12, le conteneur de fragments est de type [android.support.v4.view.ViewPager] (ligne 1). Pour désactiver le balayage, on est amené à dériver cette classe de la façon suivante :
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// contrôle le swipe
private boolean isSwipeEnabled;
// constructeurs
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// méthodes à redéfinir pour gérer le swipe
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe autorisé ?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe autorisé ?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- ligne 8 : la classe [MyPager] étend la classe Android [ViewPager] (ligne 4) ;
- sur un balayage de la main, les gestionnaires d'événements des lignes 24 et 34 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.xml] et dans l'activité principale [MainActivity]. Dans [activity_main.xml] on écrit :
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Ligne 1, on utilise la nouvelle classe. Dans [MainActivity], le code évolue comme suit :
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
// le conteneur de fragments
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// le conteneur de fragments est associé au gestionnaire de fragments
// ç-à-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
mViewPager.setAdapter(mSectionsPagerAdapter);
// on inhibe le swipe entre fragments
mViewPager.setSwipeEnabled(false);
// la barre d'onglets est également associée au conteneur de fragments
...
- ligne 12 : le gestionnaire de pages a désormais le type [MyPager] ;
- ligne 23 : 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 lorsque vous les tirez à droite ou à gauche avec la souris. Dans toutes les applications à venir, le balayage sera inhibé. Nous ne le rappellerons pas.
1.10.6. Désactiver le scrolling entre fragments
Continuons avec une amélioration du gestionnaire d'onglets. Lorsqu'on passe de l'onglet 1 à l'onglet 4, on voit défiler les deux onglets intermédiaires 2 et 3. Cela s'appelle en jargon Android le smoothScrolling. Ce comportement peut devenir gênant s'il y a beaucoup d'onglets. Il peut être inhibé en ajoutant le code suivant dans le gestionnaire de fragments [MyPager] :
// contrôle le swipe
private boolean isSwipeEnabled;
// contrôle le scrolling
private boolean isScrollingEnabled;
...
// scrolling
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
Parce que le gestionnaire d'onglets a été associé au gestionnaire de fragments [MyPager], lorsqu'on clique sur l'onglet n° i, le fragment n° i est affiché par le conteneurs de fragments par la méthode [setCurrentItem] ci-dessus (ligne 9). [position] est le n° du fragment à afficher ;
- ligne 10 : on fait appel à la méthode [setCurrentItem] de la classe parent. Le second argument à [false] demande à ce qu'il y ait une transition immédiate entre l'ancien et le nouveau fragment (pas de scrolling), à [true] qu'il y ait une transition par scrolling. Ici, le second argument est la valeur du champ de la ligne 4, champ que le développeur peut fixer avec la méthode des lignes 16-18 ;
Si on veut désactiver le scrolling, la classe [MainActivity] sera la suivante :
...
// offset des fragments
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// on inhibe le swipe entre fragments
mViewPager.setSwipeEnabled(false);
// pas de scrolling
mViewPager.setScrollingEnabled(false);
...
Exécutez de nouveau le projet et vérifiez qu'il n'y a plus de scrolling entre les onglets 1 et 4 par exemple. Dans la suite, nous inhiberons toujours le scrolling. Nous ne le rappellerons pas.
1.10.7. Un nouveau fragment
Dans notre exemple, tous les fragments sont du même type [PlaceHolderFragment]. Nous allons maintenant apprendre à créer un nouveau fragment et à l'afficher.
Tout d'abord copions la vue [vue1.xml] du projet [Exemple-04] dans le projet [Exemple-09] [1] :
![]() | ![]() |
- en [1], la vue [vue1.xml] ;
- en [3], la vue présente des erreurs provenant de textes absents dans le fichier [res/values/strings.xml] ;
En [2], on rajoute les textes manquants en les prenant dans le fichier [res/values/strings.xml] du projet [Exemple-04]
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- ci-dessus, on a ajouté les lignes 6-9 ;
Maintenant, nous créons la classe [Vue1Fragment] qui va être le fragment chargé d'afficher la vue [vue1.xml] :
![]() |
La classe [Vue1Fragment] sera la suivante :
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gestionnaire d'évt
@Click(R.id.buttonValider)
protected void doValider() {
// on affiche le nom saisi
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- ligne 10 : l'annotation [@EFragment] fait que le fragment utilisé par l'activité sera en réalité la classe [Vue1Fragment_]. Il faut s'en souvenir. Le fragment est associé à la vue [vue1.xml] ;
- lignes 14-15 : le composant identifié par [R.id.editTextNom] est injecté dans le champ [editTextNom] de la ligne 15 ;
- lignes 18-20 : la méthode [doValider] gère l'événement 'click' sur le bouton identifié par [R.id.buttonValider] ;
- ligne 21 : 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 ;
Dans la classe [MainActivity], le gestionnaire de fragments évolue de la façon suivante :
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private Fragment[] fragments;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialisation du tableau des fragments
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
// on crée un fragment
fragments[i] = new PlaceholderFragment_();
// on peut passer des arguments au fragment
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// un fragment de +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- ligne 13 : il y a [FRAGMENTS_COUNT] fragments : [FRAGMENTS_COUNT-1] fragments de type [PlaceholderFragment] (lignes 14-21) et un fragment de type [Vue1Fragment_], ligne 23 (attention à l'underscore) ;
Compilez puis exécutez le projet [Exemple-09]. L'onglet n° 5 doit être différent :
![]() |
1.10.8. Faire dériver tous les fragments d'une même classe abstraite
Le nouveau fragment [Vue1Fragment] a besoin lui aussi de se mettre à jour lorsqu'il est affiché. Pour ce faire, on va devoir créer un code similaire à celui créé pour le fragment [PlaceholderFragment]. Pour éviter de se répéter, on va factoriser ce qui peut l'être dans une classe abstraite dont tous les fragments de l'application hériteront.
Pour cela nous créons un nouveau projet.
1.11. Exemple-10 : faire dériver tous les fragments d'une classe abstraite
1.11.1. Création du projet
On duplique le projet [Exemple-09] dans [Exemple-10] :
![]() | ![]() |
1.11.2. Gestion du mode debug
Nous ajoutons au projet la possibilité d'afficher ou non les logs du mode debug. Pour cela, nous ajoutons une constante statique à la classe [MainActivity] :
// mode debug
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. La classe abstraite parente de tous les fragments
![]() |
La classe [AbstractFragment] est la suivante :
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// données privées
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// données accessibles aux classes filles
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// activité
protected MainActivity activity;
// session
protected Session session;
// constructeur
public AbstractFragment() {
// init
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
...
}
@Override
public void onResume() {
// parent
super.onResume();
...
}
// infos locales
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// update fragment
protected void update() {
...
// on demande à la classe fille de se mettre à jour
updateFragment();
}
protected abstract void updateFragment();
}
- ligne 7 : la classe [AbstractFragment] étend la classe Android [Fragment] ;
- tout fragment doit pouvoir se mettre à jour. C'est pourquoi la classe parent [AbstractFragment] impose la présence à ses classes filles d'une méthode [updateFragment] (ligne 68) qu'elle appelle (ligne 65) ;
- ligne 19 : la classe stockera une référence sur l'activité de l'application ;
- ligne 22 : la classe stockera une référence sur la session où sont rassemblées les données partagées par les fragments et l'activité ;
- lignes 25-33 : le constructeur de la classe abstraite ;
- ligne 27 : création d'une copie de la constante [MainActivity.IS_DEBUG_ENABLED] dans le champ de la ligne 16 ;
- ligne 28 : on mémorise le nom de la classe instanciée, donc le nom d'une classe fille ;
- lignes 15-22 : ces champs ont l'attribut [protected] pour que les classes filles y aient accès. On remarquera que les classes filles ignorent l'existence des booléens [isVisibleToUser] et [updateDone] (lignes 10-11) ;
- ligne 57 : la méthode [getParentInfos] a l'attribut [protected] pour que les classes filles puissent l'appeler ;
Les méthodes [setUserVisibleHint, onDestroyView, onResume] restent analogues à ce qu'elles étaient dans la classe [PlaceholderFragment] du projet précédent :
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// mémoire
this.isVisibleToUser = isVisibleToUser;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// cas où le fragment va devenir visible
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// on quitte le fragment
updateDone = false;
}
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// mise à jour indicateur
afterViewsDone = false;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// update
if (!updateDone) {
update();
updateDone = true;
}
}
}
La méthode [update] est la suivante :
// update fragment
protected void update() {
// on récupère l'activité et la session
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// on demande à la classe fille de se mettre à jour
updateFragment();
}
D'après le code ci-dessus, lorsque la méthode [update] d'un fragment s'exécute, celui-ci est visible. Cela est important parce que cela signifie que la méthode [Fragment.getActivity] rend alors une référence sur l'activité de l'application (cf paragraphe 1.10.8), ce qui donne ensuite accès à la session.
- lignes 4-10 : on initialise l'activité et la session si ce n'a pas déjà été fait ;
- ligne 12 : on appelle la méthode [updateFragment] de la classe fille. Lorsque celle-ci s'exécutera, les champs [activity] et [session] auxquels elle a accès autont été initialisés ;
1.11.4. La classe [PlaceholderFragment]
![]() |
La classe [PlaceholderFragment] évolue de la façon suivante :
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// composant de l'interface visuelle
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean initDone;
// data
private String text;
private int numVisit;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
...
}
// update fragment
public void updateFragment() {
...
}
}
- ligne 10 : la classe [PlaceholderFragment] étend la classe [AbstractFragment]. Avec cette architecture, l'écriture d'un fragment consiste :
- à écrire la méthode [@AfterViews] qui sert à initialiser le fragment lors de son premier cycle de vie ou à le réinitialiser s'il y a eu un [onDestroyView] auparavant. La ligne 39 est obligatoire pour gérer correctement le cycle de vie du fragment ;
- à écrire la méthode [updateFragment] qui va mettre à jour le fragment juste avant son affichage. Cette méthode peut utiliser la session de sa classe parent ;
- à écrire les gestionnaires d'événements du fragment. C'est ce que nous allons faire dans de futurs projets ;
Les méthodes [@AfterViews] et [updateFragment] restent analogues à ce qu'elles étaient dans le projet précédent :
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// texte initial
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// init done
initDone = true;
}
// affichage texte courant
textViewInfo.setText(text);
}
// update fragment
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texte modifié
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// infos locales pour logs
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- ligne 7 et 23 : dans les logs, on affiche les informations de la classe parent avec la méthode héritée [getParentInfos] ;
1.11.5. La classe [Vue1Fragment]
![]() |
La classe [Vue1Fragment] présente la même structure que la classe [PlaceholderFragment] :
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private int numVisit;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// gestionnaire d'évt
@Click(R.id.buttonValider)
protected void doValider() {
// on affiche le nom saisi
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// infos locales pour logs
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// mise à jour fragment
@Override
protected void updateFragment() {
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// on affiche le n° de la visite
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- ligne 9 : la classe [Vue1Fragment] étend la classe [AbstractFragment] ;
- lignes 18-26 : la méthode [@AfterViews] n'a rien d'intéressant à faire. Il faut quand même l'écrire pour mettre le booléen [afterViewsDone] à true, car cette information est utilisée par la classe parent ;
- lignes 42-49 : la méthode [updateFragment] consiste à afficher un message court montrant le n° de la visite (ligne 48) et à incrémenter ce n° dans la session (lignes 44-46) ;
Le lecteur est invité à tester ce nouveau projet.
Nous reprendrons dans tous les futurs projets cette architecture :
- une activité et n fragments ;
- tous les fragments étendent la classe [AbstractFragment] ;
- les données à partager entre fragments et entre fragments et activité sont placées dans la classe [Session] ;
1.11.6. Assocation onglets / fragments
Dans la classe [MainActivity] qui gère les onglets il est écrit :
// la barre d'onglets est également associée au conteneur de fragments
// ç-à-d que l'onglet n° i affiche le fragment n° i du conteneur
tabLayout.setupWithViewPager(mViewPager);
La ligne 3 associe le gestionnaire d'onglets au conteneur de fragment. On a vu une conséquence de cette association : lorsque l'utilisateur clique sur l'onglet n° i, le conteneur de fragments fait afficher le fragment n° i. On n'a pas vu l'inverse : lorsqu'on demande au conteneur de fragments d'afficher le fragment n° i, alors l'onglet n° i est automatiquement sélectionné.
Pour illustrer ce comportement, nous allons ajouter les options [Fragment 1, Fragment 2, ...] au menu actuel. Lorsque l'utilisateur cliquera l'option [Fragment i], on demandera au conteneur de fragments d'afficher le fragment n° i. On verra alors si l'onglet n° i a été sélectionné ou non.
Cette étape commence par le changement du menu de l'application :
![]() | ![]() |
Le contenu du fichier [res / menu / menu_main.xml] évolue de la façon suivante :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- lignes 9-28 : les cinq nouvelles options du menu ;
- les libellés des options (lignes 10, 14, 18, 22, 26) sont définis dans le fichier [res / values / strings.xml] [2] :
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
<!-- menu -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
Le résultat visuel est le suivant :
![]() |
La gestion du clic sur ces options de menu se fait dans la classe [MainActivity] :
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// traitement des options de menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// item traité
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// on change le fragment affiché
mViewPager.setCurrentItem(i);
}
}
- ligne 2 : la méthode [onOptionsItemSelected] est appelée lorsque se produit un clic sur l'une des options de menu ;
- ligne 8 : on récupère l'identifiant de l'option cliquée ;
- lignes 9-36 : les différents cas sont traités par un switch ;
- lignes 16-36 : le clic sur l'option [Fragment i] renvoie à la méthode [showFragment(i-1)] des lignes 41-45 ;
- ligne 43 : on demande au conteneur de fragments d'afficher le fragment demandé ;
- ligne 42 : on vérifie auparavant qu'on peut le faire (condition 1) et que c'est nécessaire (condition 2) ;
Le lecteur est invité à tester cette nouvelle version. On constate que lorsqu'on demande l'affichage du fragment n° i, celui-ci est bien affiché et l'onglet n° i est lui-même sélectionné.
Maintenant que nous avons vu comment fonctionnait l'association onglets / fragments, nous allons nous intéresser à un autre cas : celui où la gestion des onglets est dissociée de celle des fragments. C'est le cas par exemple lorsqu'il y a moins d'onglets que de fragments. Pour illustrer ce nouveau cas d'utilisation, nous construisons un nouveau projet.
1.12. Exemple-11 : onglets dissociés des fragments
1.12.1. Création du projet
On duplique le projet [Exemple-10] dans [Exemple-11] :
![]() | ![]() |
1.12.2. Objectifs
La nouvelle application aura deux onglets :
- le 1er onglet affichera toujours le fragment [Vue1] ;
- le second onglet affichera un fragment choisi dans le menu ;

- en [1], le fragment [Vue1] ;
- en [2], le fragment de type [PlaceholderFragment] choisi par l'utilisateur ;
- en [3], on continue à compter les visites ;
1.12.3. La session
![]() |
La nouvelle session sera la suivante :
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nombre de fragments visités
private int numVisit;
// n° fragment de type [PlaceholderFragment] affiché dans second onglet
private int numFragment;
// getters et setters
...
}
- ligne 10 : nous allons gérer nous-mêmes le clic sur les onglets. Lorsqu'on clique sur un onglet, il faut restituer le fragment qu'il affichait la dernière fois qu'il était sélectionné. Le champ [numFragment] mémorisera le n° de celui-ci pour l'onglet n° 2, un nombre dans [0, Fragments_COUNT-2]. Lorsque l'onglet n° 2 sera cliqué, on ira chercher dans la session, le n° du fragment à afficher ;
1.12.4. Le menu
![]() |
Le menu [res / menu / menu_main.xml] évolue de la façon suivante :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
L'onglet n° 2 affichera l'un des quatre fragments des lignes 9-24. Le 5ième fragment est le fragment [Vue1Fragment] qui sera lui toujours affiché dans l'onglet n° 1.
1.12.5. La classe [MainActivity]
La classe [MainActivity] doit désormais gérer les onglets et la navigation entre eux, ce qu'elle ne faisait pas jusqu'à maintenant. Son code évolue de la façon suivante :
// le gestionnaire d'onglets
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// pas de scrolling
mViewPager.setScrollingEnabled(false);
// affichage Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// au départ on n'a qu'un seul onglet
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// gestionnaire d'évt
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- ligne 17 : le 1er fragment affiché par le conteneur de fragments sera le fragment [Vue1Fragment]. Par construction, ce sera le dernier fragment du conteneur ;
- lignes 20-22 : parce qu'on n'a pas fait d'association entre onglets et conteneur de fragments, nous devons gérer les onglets nous-mêmes. Au départ, la barre d'onglets [tabLayout] de la ligne 3 n'a aucun onglet ;
- ligne 20 : on crée le 1er onglet ;
- ligne 21 : on lui donne un titre. Dans les exemples précédents, le titre des onglets était le titre des fragments. C'est désormais fini. Du coup, on retire la méthode [getPageTitle] du gestionnaire de fragments. On n'en a plus besoin :
// facultatif - donne un titre aux fragments gérés
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- ligne 22 : l'onglet créé est ajouté à la barre d'onglets. Notre barre d'onglets a désormais un onglet. Qu'affiche cet onglet ? Il faut comprendre que les onglets et les fragments sont deux notions indépendantes. Le fragment affiché est toujours celui choisi par le conteneur de fragments. Si on change d'onglet et qu'on ne demande pas au conteneur de changer le fragment affiché, rien ne se passe : c'est toujours le même fragment qui est affiché mais l'onglet sélectionné a lui changé. Donc ici, le fragment affiché est celui choisi ligne 17 : le fragment [Vue1Fragment] ;
- lignes 26-30 : la méthode à écrire pour gérer le changement d'onglet par l'utilisateur ;
La méthode [onTabSelected] des lignes 26-30 est déclenchée dès qu'il y a changement d'onglet (si l'utilisateur clique sur un onglet déjà sélectionné, il ne se passe rien). Son code est le suivant :
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
// position de l'onglet
int position = tab.getPosition();
// n° du fragment à afficher
int numFragment;
switch (position) {
case 0:
// n° fragment [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// n° fragment [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// affichage fragment
mViewPager.setCurrentItem(numFragment);
}
- ligne 8 : on récupère la position de l'onglet qui a été cliqué. On va récupérer ici un nombre 0 ou 1 ;
- lignes 12-15 : si c'est le premier onglet qui a été cliqué, on se prépare à afficher le fragment [Vue1Fragment] ;
- lignes 16-18 : dans les autres cas (onglet n° 2 cliqué), on se prépare à réafficher le fragment qui était affiché la dernière fois que l'onglet n° 2 était sélectionné. Le n° de celui-ci avait alors été mis dans la session de l'application ;
- ligne 21 : on demande au conteneur de fragments d'afficher le fragment désiré ;
Voyons maintenant la gestion des options de menu (toujours dans [MainActivity]) :
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// traitement des options de menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item traité
return true;
}
- lignes 16-31 : gestion des 4 options du menu. Chaque gestionnaire appelle la méthode [showFragment] avec le n° du fragment à afficher ;
La méthode [showFragment] est la suivante :
// l'onglet n° 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// si le 2ième onglet n'existe pas encore, on le crée
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// on fixe le titre du second onglet
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// on change le fragment affiché
mViewPager.setCurrentItem(i);
// le n° du fragment affiché est mis en session
session.setNumFragment(i);
// on sélectionne l'onglet 2 - ne fait rien si celui-ci est déjà sélectionné
tab2.select();
}
}
- on se rappelle qu'au départ de l'application, on n'a qu'un onglet ;
- ligne 2 : une référence sur l'onglet n° 2, null au départ ;
- ligne 5 : les conditions d'affichage n'ont pas changé par rapport à la version précédente ;
- lignes 7-10 : si l'onglet n° 2 n'existe pas encore, il est créé (ligne 8) et ajouté à la barre d'onglets (ligne 9) ;
- ligne 12 : on met dans le titre du second onglet le n° du fragment qui va être affiché avec une numérotation commençant à 1 ;
- ligne 14 : le fragment désiré est affiché ;
- ligne 16 : son n° est mis dans la session ;
- ligne 18 : l'onglet n° 2 est sélectionné. S'il était déjà sélectionné, rien ne se passera : la méthode [onTabSelected] ne sera pas exécutée. S'il n'était pas déjà sélectionné, la méthode [onTabSelected] va se déclencher. Cette méthode demande alors au conteneur de fragments d'afficher le fragment déjà affiché ligne 14. Un simple test dans la méthode [onTabSelected] évite ce cas :
// affichage fragment seulement si c'est nécessaire
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
Le lecteur est invité à tester cette nouvelle version.
1.12.6. Améliorations
Nous avons désormais une bonne compréhension des fragments, de leur cycle de vie, de la notion d'adjacence de fragments et de leur relation avec la barre d'onglets. Nous avons par ailleurs une architecture robuste qui vient de passer le test de l'exemple 11 :
- une activité et n fragments ;
- tous les fragments étendent la classe [AbstractFragment] ;
- les données à partager entre fragments et entre fragments et activité sont placées dans la classe [Session] ;
Nous allons dans un nouveau projet préciser les relations entre activité et fragments par l'ajout d'une interface.
1.13. Exemple-12 : codifier les relations entre activité et fragments
Dans cet exemple, nous voulons définir les relations minimales entre activité et fragments. Pour cela, nous utiliserons :
- une interface [IMainActivity] qui définira ce que les fragments peuvent demander à l'activité ;
- une classe abstraite [AbstractFragment] qui définira l'état et les méthodes que devrait avoir tout fragment ;
1.13.1. Création du projet
Nous dupliquons le projet [Exemple-11] dans [Exemple-12] en suivant la procédure du paragraphe 1.4. Nous obtenons le résultat suivant :
![]() | ![]() |
1.13.2. L'interface [IMainActivity]
Des exemples précédents, il apparaît que les fragments ont besoin d'avoir accès à la session instanciée par l'activité. Par ailleurs, pas visible dans ces exemples, mais prévisible : les gestionnaires des événements des fragments se terminent parfois par un changement de vue. On demandera à l'activité d'opérer ce changement. L'interface [IMainActivity] pourrait être alors la suivante :
![]() |
package exemples.android;
public interface IMainActivity {
// accès à la session
Session getSession();
// changement de vue
void navigateToView(int position);
// mode debug
boolean IS_DEBUG_ENABLED = true;
}
Ligne 12, on notera la présence d'une constante qui était auparavant dans la classe [MainActivity]. On veut réduire le couplage entre les fragments et l'activité et le réduire à un couplage entre [AbstractFragment] et [IMainActivity]. L'activité pourra alors s'appeler autrement que [MainActivity]. La constante [IS_DEBUG_ENABLED] étant utilisée dans les fragments, elle est déplacée dans l'interface [IMainActivity].
1.13.3. La classe abstraite [AbstractFragment]
La classe abstraite [AbstractFragment] évolue très peu :
// données accessibles aux classes filles
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// activité
protected IMainActivity mainActivity;
protected Activity activity;
...
// update fragment
protected void update() {
// on récupère l'activité et la session
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// on demande à la classe fille de se mettre à jour
updateFragment();
}
- lignes 6 et 7 : on entretient deux types de référence sur l'activité :
- ligne 6 : une référence sur l'activité implémentant l'interface [IMainActivity] ;
- ligne 7 : une référence sur l'activité héritant de la classe Android [Activity]. C'est le cas de toute activité ;
Ces deux références pointent bien sûr sur le même objet. Mais celui-ci est vu avec deux types différents. Cela nous évitera des transtypages à l'exécution ;
- ligne 14 : on récupère une référence sur l'activité au moyen de la méthode [getActivity] ;
- ligne 15 : si celle-ci est non nulle, alors on peut avoir accès à la session ;
- lignes 16-17 : on mémorise l'activité en tant qu'implémentant l'interface [IMainActivity] et la session ;
1.13.4. Modification du gestionnaire de fragments
Le gestionnaire de fragments [SectionsPagerAdapter] dans la classe [MainActivity] est modifié en un seul point : au lieu de gérer des fragments de type [Fragment], il gère désormais des fragments de type [AbstractFragment] :
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private AbstractFragment[] fragments;
// n° de fragment
private static final String ARG_SECTION_NUMBER = "section_number";
// constructeur
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialisation du tableau des fragments
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// un fragment de +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
...
}
// rend le nombre de fragments gérés
@Override
public int getCount() {
...
}
}
1.13.5. Modification de la classe [MainActivity]
La classe [MainActivity] doit implémenter l'interface [IMainActivity] :
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// injection session
@Bean(Session.class)
protected Session session;
...
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// on affiche la vue position
if(mViewPager.getCurrentItem()!=position){
// affichage fragment
mViewPager.setCurrentItem(position);
}
}
- lignes 10-12 : la méthode [getSession] existait déjà ;
- lignes 15-22 : la méthode [navigateToView] fait afficher le fragment n° [position] ;
- ligne 17 : on regarde s'il y a quelque chose à faire ;
- ligne 19 : le fragment n° [position] est affiché ;
A ce stade, exécutez l'application. Elle doit fonctionner.
1.13.6. Modification de l'affichage des fragments dans [MainActivity]
Actuellement, la classe [MainActivity] affiche un fragment par l'instruction :
// affichage Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
Comme la méthode [navigateToView] fait la même chose, on remplace ce type d'instruction partout (2 endroits) par :
Exécutez ensuite l'application. Elle doit toujours fonctionner.
1.13.7. Conclusion
A partir de maintenant, nous utiliserons toujours l'architecture précédente :
- une activité implémentant l'interface [IMainActivity] ;
- des fragments étendant la classe [AbstractFragment], ce qui leur impose d'implémenter la méthode [updateFragment]. Ceux-ci doivent avoir également une méthode [@AfterViews] dans laquelle elles mettent le booléen [afterViewsDone] à true ;
- une session encapsulant les données à partager entre fragments et activité ;
1.14. Exemple-13 : Exemple-05 avec des fragments
Dans le projet [Exemple-05] 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 une seule activité avec plusieurs vues de type [AbstractFragment].
1.14.1. Création du projet
Nous dupliquons le projet précédent [Exemple-12] dans [Exemple-13] en suivant la procédure du paragraphe 1.4. Nous obtenons le résultat suivant :
![]() | ![]() |
1.14.2. Structuration du projet
Nous allons commencer à utiliser des packages pour organiser le code. Pour l'instant, nous pouvons distinguer deux domaines distincts :
- la gestion de l'activité ;
- la gestion des fragments ;
Nous créons pour eux deux packages [exemples.android.activity] et [exemples.android.fragments] :
![]() |
![]() | ![]() |
On fait de même pour créer le package [exemples.android.fragments] :
![]() | ![]() |
En [8], on crée un troisième package appelé [architecture] dans lequel nous mettrons les entités [IMainActivity, AbstractFragment, Session, MyPager] qui sont les éléments de base de l'architecture de notre application. Ceci pour nous rappeler qu'on a fait un choix d'architecture précis. Ensuite, déplacez les éléments existants du projet comme indiqué en [9]. Chaque déplacement doit être validé par un clic sur le bouton [Refactor].
A ce stade, compilez l'application. On a les erreurs suivantes dans [MainActivity :
![]() |
Lors des déplacements de classes vers les packages, Android Studio a fait les changements nécessaires dans les codes de l'application (lignes 18-21, par exemple). Les classes concernées par les lignes 15 et 17 n'ont pas été déplacées. Elles sont générées par la bibliothèque Android Annotations. Pour ces classes, il faut changer les imports à la main. Ces lignes deviennent donc :
![]() |
Ceci fait, il n'y a plus d'erreurs de compilation. Exécutez l'application. On a alors l'erreur suivante :
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
Cette erreur provient du manifeste de l'application :
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Les lignes 3 et 12 font que l'activité désignée est [exemples.android.MainActivity_]. Or du fait que l'activité a migré dans le package [activity], la ligne 12 doit être désormais :
android:name=".activity.MainActivity_"
Attention au . devant [activity]. De nouveau, Android Sudio n'a pu mettre à jour le manifeste parce que celui-ci référence une classe Android Annotations qui elle n'a pas subi de déplacement. L'utilisation de la bibliothèque AA amène donc avec elle un certain nombre de désagréments.
1.14.3. Nettoyage du projet
Dans le nouveau projet :
- il n'y a plus d'onglets, de bouton flottant, de menu ;
- les fragments [PlaceholderFragment] disparaissent. L'application va gérer deux fragments [Vue1Fragment] qu'on a déjà et [Vue2Fragment] qu'il faudra créer ;
- la session n'est plus la même ;
1.14.3.1. Nettoyage des fragments
Supprimez la classe [PlaceHolderFragment] [1] :
![]() | ![]() |
De même supprimez la vue [res / layout / fragment_main.xml] associée à ce fragment [2].
1.14.3.2. Nettoyage de la session
La session est actuellement la suivante :
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nombre de fragments visités
private int numVisit;
// n° fragment de type [PlaceholderFragment] affiché dans second onglet
private int numFragment;
// getters et setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
Nous ne gardons rien de cette session.
Compilez le projet. Les lignes erronées sont celles qui utilisaient le contenu de la session. Supprimez-les. Dans la classe [Vue1Fragment] on supprime également la variable [numVisit] du code qui devient le suivant :
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaire d'évt
@Click(R.id.buttonValider)
protected void doValider() {
// on affiche le nom saisi
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// mise à jour fragment
@Override
protected void updateFragment() {
}
}
1.14.3.3. Suppression des onglets, du bouton flottant et du menu
La suppression des onglets et du bouton flottant se fait à deux endroits :
- dans la vue [res / layout / activity-main.xml] qui définit ces éléments et leur emplacement dans la vue ;
- dans le code de l'activité [MainActivity] ;
La suppression du menu se fait également à deux endroits :
- dans la vue [res / menu / menu-main.xml] qui définit les options du menu ;
- dans le code de l'activité [MainActivity] ;
Le code de la vue la vue [res / layout / activity-main.xml] est actuellement le suivant :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- on supprime les lignes [28-31, 41-47] ;
- on supprime également la barre d'outils des lignes 18-24 ;
Le code du menu [res / menu / menu_main.xml] est actuellement le suivant :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- on supprimera les lignes 9-24. On laisse ainsi une option qu'on n'utilisera pas. Simplement pour avoir un exemple de déclaration d'une option de menu qu'on pourra reproduire par copier / coller ;
Dans la classe [MainActivity] on supprime tout ce qui fait référence aux onglets, au bouton flottant, à la barre d'outils et au menu. Pour trouver ces références, le plus simple est de supprimer leur déclaration :
// le gestionnaire d'onglets
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// le bouton flottant
@ViewById(R.id.fab)
protected FloatingActionButton fab;
et de recompiler l'application. Les lignes erronées sont celles qui font référence aux éléments disparus. Supprimez alors toutes ces lignes. Par ailleurs, modifiez le gestionnaire de fragments pour qu'il ne fasse plus référence au fragment [PlaceholderFragment] que nous avons supprimé :
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private AbstractFragment[] fragments;
// constructeur
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// rend le nombre de fragments gérés
@Override
public int getCount() {
return fragments.length;
}
}
- lignes 7-10 : on a supprimé toute la génération des fragments ;
A ce stade, il ne doit plus y avoir d'erreur de compilation. Dans la classe [MainActivity], on est arrivé au code intermédiaire suivant :
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// le conteneur de fragments
@ViewById(R.id.container)
protected MyPager mViewPager;
// la barre d'outils
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// injection session
@Bean(Session.class)
protected Session session;
// nombre de fragments
private final int FRAGMENTS_COUNT = 5;
// adjacence des fragments
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// mode debug
public static final boolean IS_DEBUG_ENABLED = true;
// le gestionnaire de fragments
private SectionsPagerAdapter mSectionsPagerAdapter;
// constructeur
public MainActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// barre d'outils - c'est là qu'est affiché le nom de l'application
setSupportActionBar(toolbar);
// le gestionnaire de fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// le conteneur de fragments est associé au gestionnaire de fragments
// ç-à-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
mViewPager.setAdapter(mSectionsPagerAdapter);
// offset des fragments
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// on inhibe le swipe entre fragments
mViewPager.setSwipeEnabled(false);
// pas de scrolling
mViewPager.setScrollingEnabled(false);
// affichage Vue1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// on affiche la vue position
if (mViewPager.getCurrentItem() != position) {
// affichage fragment
mViewPager.setCurrentItem(position);
}
}
// le gestionnaire de fragments
// c'est à lui qu'on demande les fragments à afficher dans la vue principale
// doit définir les méthodes [getItem] et [getCount] - les autres sont facultatives
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private AbstractFragment[] fragments;
// constructeur
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// rend le nombre de fragments gérés
@Override
public int getCount() {
return fragments.length;
}
}
}
Il reste quelques modifications à faire :
- supprimez la ligne 31 qui n'a plus lieu d'être ;
- ligne 33 : mettez 1 comme adjacence de fragments ;
- ligne 76 : naviguez vers la vue 0. Ce sera elle qui sera affichée la première ;
- ligne 108 : initialisez le tableau avec le fragment [Vue1Fragment_] :
// les fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
On n'a donc qu'un seul fragment. Exécutez l'application. Vous devez obtenir le résultat suivant :

Le bouton [Valider] doit fonctionner.
1.14.4. Création des fragment et des vues associées
L'application aura deux vues, celles du projet [Exemple-05]. Nous avons déjà la vue [vue1.xml] dans le projet présent. Nous dupliquons maintenant [vue2.xml] de [Exemple-05] vers [Exemple-12] (ouvrez les deux projets et faites du copier / coller) entre eux.
![]() | ![]() |
- en [1], la nouvelle vue. Lorsqu'on essaie de l'éditer, des erreurs apparaissent [2]. Il nous faut modifier le fichier [strings.xml] [3] pour y ajouter les chaînes référencées par cette nouvelle vue :
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- vue 2 -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
Nous dupliquons la classe [Vue1Fragment] dans [Vue2Fragment] :
![]() |
et nous modifions le code copié de la façon suivante :
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// mise à jour fragment
@Override
protected void updateFragment() {
}
}
- ligne 9 : le fragment est associé à la vue [res / layout / vue2.xml] ;
- ligne 10 : la classe étend la classe abstraite [AbstractFragment] ;
- lignes 12-20 : la méthode [@AfterViews] obligatoire ;
- lignes 23-25 : la méthode [updateFragment] obligatoire ;
1.14.5. Mise en place des fragments et de la navigation entre-eux
L'activité va gérer désormais deux fragments. Sa classe [SectionsPagerAdapter] évolue comme suit :
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
L'interface [IMainActivity] assure la navigation entre vues avec sa méthode [navigateToView]. Nous allons gérer le clic sur le bouton [Vue n° 2] du fragment [Vue1Fragment] :
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// on affiche le nom saisi
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// mise à jour fragment
@Override
protected void updateFragment() {
}
}
- lignes 37-40 : la méthode [showVue2] gère l'événement 'clic' sur le bouton [Vue n° 2] ;
- ligne 39 : on navigue avec la méthode [navigateToView] de l'activité. On rappelle ici que l'activité a été mémorisée dans la classe parent sous la forme :
// activité
protected IMainActivity mainActivity;
et que cette activité a déjà été initialisée lorsqu'on arrive dans un quelconque gestionnaire d'événement.
- ligne 34 : l'instruction utilise la variable [activity] de la classe parent qui est une référence de l'activité en tant qu'instance du type Android [Activity] ;
protected Activity activity;
On retrouve un code analogue pour le fragment [Vue2Fragment] :
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// mise à jour fragment
@Override
protected void updateFragment() {
}
}
- lignes 24-27 : la méthode [showVue1] gère l'événement 'clic' sur le bouton [Vue n° 1] ;
Exécutez le projet et vérifiez que la navigation entre vues fonctionne.
1.14.6. Définition de la session
Le fonctionnement de l'application est le suivant :
- saisie d'un nom dans la vue n° 1 ;
- affichage de ce nom dans la vue n° 2 ;
Pour que la vue n° 1 puisse communiquer le nom saisi à la vue n° 2, nous utiliserons la session suivante ;
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nom
private String nom;
// getters et setters
...
}
- ligne 8 : le nom saisi ;
La classe [MainActivity] va initialiser la session de la façon suivante :
// injection session
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// init session
session.setNom("");
}
1.14.7. Ecriture finale des fragments
Dans le fragment [Vue1Fragment] nous modifions le code du gestionnaire du clic sur le bouton [Valider] :
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// gestionnaires d'évts ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// on mémorise le nom saisi
String nom = editTextNom.getText().toString();
// on l'affiche
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// on met le nom saisi dans la session
session.setNom(editTextNom.getText().toString());
// on navigue vers la vue n° 2
mainActivity.navigateToView(1);
}
// mise à jour fragment
@Override
protected void updateFragment() {
}
}
- lignes : 31-37 : on gère le clic sur le bouton [Vue n° 2] ;
- ligne 34 : avant de naviguer vers la vue n° 2, on met le nom saisi dans la session pour que la nouvelle vue y ait accès ;
La vue [Vue2Fragment] évolue de la façon suivante :
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// composants de l'interface visuelle
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// mise à jour fragment
@Override
protected void updateFragment() {
// on récupère le nom saisi dans la session
String nom = session.getNom();
// on l'affiche
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
Lorsque la vue n° 2 s'affiche, il faut afficher le nom saisi dans la vue n° 1. On sait que juste après son affichage, sa méthode [updateFragment] va être exécutée. C'est donc dans cette méthode (lignes 36-42) qu'on peut mettre le code d'affichage du nom.
- lignes 16-17 : déclaration du seul composant visuel de la vue ;
- ligne 39 : le nom saisi dans la vue n°1 est récupéré dans la session ;
- ligne 41 : le libellé [textViewBonjour] est modifié ;
Exécutez le projet et vérifiez qu'il fonctionne.
1.14.8. Gestion du cycle de vie des fragments
Dans le fragment [Vue1Fragment], la méthode [@AfterViews] est la suivante :
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
Cette méthode est incomplète. En effet, il faut toujours prévoir le cas où le fragment est recyclé après une opération [onDestroyView]. Dans ce cas, la vue du fragment 1 est régénérée et le nom qui a pu être saisi précédemment va disparaître de la vue. On ne veut pas ça. Actuellement le nom saisi reste affiché parce que l'adjacence des fragments de 1 fait que le cycle de vie du fragment [Vue1Fragment] n'est exécuté qu'une fois. Il est cependant préférable de prévoir le cas du recyclage du fragment.
Il y a plusieurs façons de résoudre ce problème :
- on peut profiter du fait que la méthode [update] soit exécutée systématiquement à chaque affichage du fragment pour mettre à jour le nom saisi ;
- on peut faire cette mise à jour uniquement lorsque la méthode [@AfterViews] est réexécutée. C'est cette dernière voie que nous prenons ;
On modifie le code de [Vue1Fragment] de la façon suivante :
// les éléments de l'interface visuelle
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private String nom;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// on (ré)initialise le texte affiché
editTextNom.setText(nom);
}
// gestionnaires d'évts ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// on note le nom saisi pour pouvoir le récupérer si le fragment est recyclé
nom = editTextNom.getText().toString();
// on met le nom saisi dans la session
session.setNom(nom);
// on navigue vers la vue n° 2
activity.navigateToView(1);
}
- ligne 27 : alors qu'on s'apprête à quitter la vue 1 pour la vue 2, on mémorise le nom saisi ;
- ligne 17 : à chaque nouvelle exécution du cycle de vie du fragment, le dernier nom saisi est réaffiché ;
Pour le fragment [Vue2Fragment], le code existant suffit :
// composants de l'interface visuelle
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// mise à jour fragment
@Override
protected void updateFragment() {
// on récupère le nom saisi dans la session
String nom = session.getNom();
// on l'affiche
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- le seul composant visuel de la vue (ligne 3) est mis à jour à chaque fois que la vue est affichée (ligne 21). La méthode [@AfterViews] n'a donc rien à ajouter ;
1.14.9. Conclusion
A ce point, nous avons de nouveau montré la pertinence de notre architecture :
- une activité implémentant l'interface [IMainActivity] ;
- des fragments étendant la classe [AbstractFragment], ce qui leur impose d'implémenter la méthode [updateFragment]. Ceux-ci doivent avoir également une méthode [@AfterViews] dans laquelle elles mettent le booléen [afterViewsDone] à true ;
- une session encapsulant les données à partager entre fragments et activité ;
1.15. Exemple-14 : une architecture à deux couches
Nous allons construire une application à une vue ayant l'architecture suivante :
![]() |
1.15.1. Création du projet
Nous dupliquons le projet précédent [Exemple-12] dans [Exemple-13] en suivant la procédure du paragraphe 1.4. Nous obtenons le résultat suivant :
![]() | ![]() |
1.15.2. La vue [vue1]
L'application n'aura qu'une vue [vue1.xml]. Aussi supprimons-nous l'autre vue [vue2.xml] ainsi que son fragment associé :
![]() | ![]() |
Compilez l'application. Des erreurs apparaissent dans [MainActivity] :
![]() |
Corrigez la ligne 4 ci-dessous dans le gestionnaire de fragments [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
La ligne 4 ci-dessus devient :
// les fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Supprimez les imports devenus inutiles [Ctrl-Shift-O]. Il ne doit plus y avoir d'erreurs de compilation. Exécutez le projet : la vue n° 1 doit apparaître. Nous allons maintenant modifier celle-ci.
Nous allons créer la vue [vue1.xml] qui permettra de générer des nombres aléatoires :
![]() |
Ses composants sont les suivants :
Son code XML est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorIntervalle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
La vue précédente utilise des libellés définis dans le fichier [res / values / strings.xml] :
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
Les couleurs utilisées dans [vue1.xml] sont définis dans le fichier [res / values / colors.xml] :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<!-- couleurs appli -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. La session
![]() |
Comme ici, il n'y a qu'un fragment, il n'y a pas à prévoir de communication inter-fragments. La session sera donc vide :
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
A ce stade, compilez l'application. Des erreurs apparaissent sur les lignes qui utilisaient des éléments de la session maintenant vide. Supprimez ces lignes et vérifiez que la compilation ne produit plus d'erreurs.
1.15.4. Le fragment [Vue1Fragment]
![]() |
Nous modifions le fragment [Vue1Fragment] existant de la façon suivante :
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// liste des réponses à une commande
private List<String> reponses = new ArrayList<>();
// adaptateur du listview
private ArrayAdapter<String> adapterReponses;
// les saisies
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// mémoire
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// on cache les messages d'erreur
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// on cache les éventuels msg d'erreur précédents
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// on teste la validité des saisies
if (!isPageValid()) {
return;
}
}
// on vérifie la validité des données saisies
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- il n'y a ici qu'un fragment dont le cycle de vie ne sera exécuté qu'une unique fois, au démarrage de l'application. Pour cette raison, les méthode [@AfterViews] (lignes 46-57) et [udateFragment] (lignes 75-81) ne seront exécutées qu'une fois au démarrage de l'application ;
- lignes 55-56 : on cache les deux messages d'erreur de la vue (représentés ci-dessous) [1-2] ;
![]() |
- lignes 59-60 : la méthode exécutée lors d'un clic sur le bouton [Exécuter] ;
- lignes 71-73 : on vérifie la validité des saisies ;
La méthode [isPageValid] est la suivante :
// les saisies
private int nbAleas;
private int a;
private int b;
...
// on vérifie la validité des données saisies
private boolean isPageValid() {
// saisie du nombre de nombres aléatoires
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// erreur ?
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// saisie de a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// erreur ?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// saisie de b
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// erreur ?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// retour
return (nbErreurs == 0);
}
- lignes 2-4 : ces trois champs sont initialisés par la méthode [isPageValid]. Par ailleurs, cette méthode rend true si toutes les saisies sont valides, false sinon. Si des saisies sont invalides, alors les messages d'erreur associés sont affichés ;
A ce stade, l'application est exécutable. Vérifiez le fonctionnement de la méthode [isPageValid] en entrant des données incorrectes.
1.15.5. La couche [métier]
![]() |
![]() |
La couche [métier] présente l'interface [IMetier] suivante :
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
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 :
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// la liste des objets
List<Object> réponses = new ArrayList<Object>();
// qqs vérifications
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// erreur ?
if (réponses.size() != 0) {
return réponses;
}
// on génère les nombres aléatoires
Random random = new Random();
for (int i = 0; i < n; i++) {
// on génère une exception aléatoire 1 fois / 3
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// sinon on rend un nombre aléatoire entre deux bornes [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// résultat
return réponses;
}
}
- ligne 9 : on utilise l'annotation AA [@EBean] sur la classe [Metier] afin de pouvoir injecter des références de celle-ci dans la couche [Présentation]. L'attribut (scope = EBean.Scope.Singleton) fait que la classe [Metier] ne sera instanciée qu'en un seul exemplaire. C'est donc toujours la même référence qui est injectée si on l'injecte plusieurs fois dans la couche [Présentation] ;
- le reste du code est classique ;
Le type [AleaException] utilisé par la classe [Metier] est la suivante :
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- ligne 3 : la classe [AleaException] étend la classe système [RuntimeException], ce qui en fait une exception non contrôlée : on n'est pas obligé de la gérer dans un try / catch, ni de la mettre dans la signature des méthodes ;
1.15.6. L'activité [MainActivity] revisitée
![]() |
Couche[metier]ActivitéVueUtilisateur
L'activité implémentera l'interface [IMetier] de la couche [métier]. Ainsi le fragment / vue n'aura-t-il que l'activité comme interlocuteur.
L'activité [MainActivity] implémente déjà l'interface [IMainActivity]. Pour qu'elle implémente également l'interface [IMetier], on peut :
- ajouter l'interface [IMetier] aux interfaces implémentées par l'activité ;
- faire en sorte que l'interface [IMainActivity] étende elle-même l'interface [IMetier]. C'est cette voie que nous prenons ;
L'interface [IMainActivity] devient la suivante :
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// accès à la session
Session getSession();
// changement de vue
void navigateToView(int position);
// mode debug
public static final boolean IS_DEBUG_ENABLED = true;
}
- ligne 5 : l'interface [IMainActivity] étend l'interface [IMetier]
La classe [MainActivity] évolue de la façon suivante :
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// injection session
@Bean(Session.class)
protected Session session;
// injection metier
@Bean(Metier.class)
protected IMetier metier;
...
// implémentation IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- lignes 11-12 : la couche [métier] est injectée dans l'activité. On utilise pour cela l'annotation AA [@Bean] dont le paramètre est la classe portant l'annotation AA [@EBean] ;
- ligne 2 : l'activité implémente l'interface [IMainActivity] et donc l'interface [IMetier] de la couche [métier] ;
- lignes 16-19 : implémentation de l'unique méthode de l'interface [IMetier]. On se contente de déléguer l'appel à la couche [métier] ;
1.15.7. Le fragment [Vue1Fragment] revisité
![]() |
Le code de la classe [Vue1Fragment] évolue de la façon suivante :
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// liste des réponses à une commande
private List<String> reponses = new ArrayList<>();
// adaptateur du listview
private ArrayAdapter<String> adapterReponses;
// les saisies
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// on vérifie la validité des données saisies
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// ne sera exécuté qu'une fois au démarrage de l'application
// on crée l'adaptateur du ListView - il faut pour cela que la variable [activity] ait été initialisée
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- lignes 69-70 : on fixe l'adaptateur du composant de type [ListView] ;
Le composant [ListView] sert à afficher une liste d'éléments. Il le fait au moyen d'un adapteur de type [ListAdapter] lui-même relié à la source de données qui doit alimenter le [ListView]. Pour défnir l'adaptateur d'un [ListView], on dispose de la méthode [ListView.setAdapter] suivante :
public void setAdapter (ListAdapter adapter)
[ListAdapter] est une interface. La classe [ArrayAdapter] est une classe implémentant cette interface. Le constructeur utilisé ligne 69 ci-dessus est le suivant :
- [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 [android.R.id.text1]. C'est la méthode utilisée ligne 69 pour créer l'adaptateur du [ListView]. Cet adaptateur n'a besoin d'être défini qu'une fois. Pour permettre sa réutilisation, on l'a défini comme variable d'instance de la classe (ligne 39). Regardons de nouveau, la ligne 69 :
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
Le 1er paramètre du constructeur [ArrayAdapter] est l'activité obtenue dans un fragment par [getActivity] et qui a été ici mémorisée dans la variable [activity] de la classe parent. Ce champ n'a pas toujours de valeur. Ainsi les logs montrent que lorsqu'on arrive dans la méthode [@AfterViews] il n'a pas encore été initialisé et donc on ne peut pas mettre les lignes 69-70 dans cette méthode. Dans la méthode [updateFragment] c'est possible car on sait que lorsque cette méthode est exécutée, on a forcément [activity!=null]. L'adaptateur est ici associé à la source de données [reponses] définie ligne 37 ;
La méthode [doExecuter] traite le clic sur le bouton [Exécuter]. Son code est le suivant :
@Click(R.id.btn_Executer)
void doExecuter() {
// on cache les éventuels msg d'erreur précédents
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// on efface les reponses précédentes
reponses.clear();
adapterReponses.notifyDataSetChanged();
// on teste la validité des saisies
if (!isPageValid()) {
return;
}
// on demande les nombres aléatoires à l'activité
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// on crée une liste de String à partir de ces données
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// refresh listview
adapterReponses.notifyDataSetChanged();
}
- lignes 7-8 : on veut vider le ListView. Pour cela, on vide la source de données [reponses] et on demande à l'adaptateur associé au ListView de se rafraîchir ;
- lignes 10-12 : avant d'exécuter l'action demandée, on vérifie que les valeurs saisies sont correctes ;
- ligne 14 : la liste des nombres aléatoires est demandée à l'activité. On obtient une liste d'objets où chaque objet est de type [Integer] ou [AleaException] ;
- lignes 16-22 : à partir de la liste d'objets obtenue, on met à jour la source de données [reponses] que le ListView affiche ;
- ligne 24 : on demande à l'adaptateur du ListView de se rafraîchir ;
1.15.8. Exécution
Exécutez le projet et vérifiez son bon fonctionnement.
1.16. Exemple-15 : 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 :
![]() |
Les clients interrogent certaines URL de la couche [web / jSON] et reçoivent une réponse texte au format jSON (JavaScript Object Notation). Ici notre service web traitera une unique URL de type [/a/b] qui renverra un nombre aléatoire dans l'intervalle [a,b]. Nous allons décrire l'application dans l'ordre suivant :
Le serveur
- sa couche [métier] ;
- son service [web / jSON] implémenté avec Spring MVC ;
Le client
- sa couche [DAO]. Il n'y aura pas de couche [métier] ;
1.16.1. Le serveur [web / jSON]
Nous voulons construire l'architecture suivante :
![]() |
1.16.1.1. Création du projet
Nous allons construire le service web avec l'écosystème Spring [http://spring.io/]. Nous allons sur le site [http://start.spring.io/] (juin 2016) qui va nous permettre de générer un projet Gradle avec les dépendances nécessaires à notre projet, qui n'est pas un projet Android et pour la construction duquel, Android Studio n'offre alors aucune aide :
![]() |
- en [1] : choisissez un projet Gradle ;
- en [2-3] : les caractéristiques de la dépendance jar générée par le projet (voir ci-dessous) ;
- en [4] : choisir la dépendance web [5] pour que les binaires nécessaires à notre service web soient disponibles ;
- en [6] : générez le projet. Le zip d'un projet Gradle squelette est alors généré et proposé au téléchargement ;
Que mettre en [2-3] ? Nous avons déjà utilisé des dépendances Gradle. Celui du projet précédent était par exemple le suivant :
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- ligne 22 : une dépendance se présente sous la forme [groupId:artifactId:version]. Ce qui est demandé sur le formulaire du site [http://start.spring.io/]:
- en [2] est [groupId] ;
- en [3] est [artifactId] ;
Décompressez dans le dossier des autres projets, le fichier zip obenu :
![]() | ![]() ![]() | ![]() |
Avec Android Studio, ouvrez le projet Gradle [server-01] [1-2]. Le projet ouvert est en [3] (perspective Project).
1.16.1.2. Configuration Gradle
![]() |
Le fichier Gradle généré (juin 2016) est le suivant :
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- les lignes 14 et 34-38 sont pour l'IDE Eclipse. Nous les supprimons ;
- les lignes 1-11 et 15 servent à ajouter un plugin appelé [spring-boot] à notre projet Gradle. Spring Boot est un projet de l'écosystème Spring [http://projects.spring.io/spring-boot/]. Ce plugin définit les versions des dépendances les plus couramment utilisées avec Spring. Cela permet de ne pas préciser leurs versions (lignes 30 et 31). La version est alors celle définie par la version Spring Boot utilisée (ligne 3) ;
- lignes 22-23 : la version de Java à utiliser, ici la version 1.8 ;
- lignes 25-27 : les dépôts de binaires à utiliser pour télécharger les dépendances ;
- ligne 26 : désigne le dépôt Maven central. C'est actuellement le plus grand dépôt de binaires open source disponible ;
- lignes 29-32 : les dépendances nécessaires au projet :
- ligne 30 : cette dépendance amène avec elle tous les binaires nécessaires pour construire une service web Spring ;
- ligne 31 : cette dépendance amène avec elle tous les binaires nécessaires aux tests, notamment aux tests JUnit ;
- une dépendance [compile] indique qu'on a besoin de la dépendance pour faire la compilation du projet. Une dépendance [testCompile] indique qu'on a besoin de la dépendance uniquement pour l'exécution des tests. Elle n'est alors pas incluse dans le binaire du projet ;
Nous faisons un premier nettoyage du fichier Gradle :
// spring boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
// binaire du projet
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
// dépôts Maven
repositories {
mavenLocal()
mavenCentral()
}
// dépendances
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- ligne 30 : nous avons ajouté le dépôt Maven local du poste de développement. Celui-ci est créé lorsqu'on installe Maven (cf paragraphe 6.10). Si la dépendance demandée est déjà dans le dépôt Maven local, elle ne sera pas demandée au dépôt Maven central ;
- lignes 19-22 : une tâche Gradle permettant de générer le binaire du projet. Nous allons l'utiliser pour voir ce qui est fait ;
![]() | ![]() | ![]() |
- en [1-4], exécutez la tâche [jar] définie dans le fichier [build.gradle] ([1] se trouve en haut à droite et sur le côté de l'IDE) ;
L'opération précédente crée l'archive jar du projet et place celui-ci dans le dossier [build / libs] [5] :
![]() |
Le nom de l'archive provient directement des informations données à la tâche [jar] du fichier [build.gradle] (lignes 19-22).
L'ensemble des dépendances du projet peuvent être vues de la façon suivante :
![]() |
On peut voir en [1] que l'unique dépendance du projet [compile('org.springframework.boot:spring-boot-starter-web')] a amené avec elle des dizaines de binaires. Spring Boot pour le web a inclus les dépendances dont une application web Spring MVC aura probablement besoin. Cela veut dire que certaines sont peut être inutiles. Spring Boot est idéal pour un tutoriel :
- il amène les dépendances dont nous aurons probablement besoin ;
- il amène un serveur Tomcat embarqué [1] ce qui nous évite le déploiement de l'application sur un serveur web externe ;
On trouvera de nombreux exemples utilisant Spring Boot sur le site de l'écosystème Spring [http://spring.io/guides].
Nous complétons maintenant le fichier [build.gradle] de la façon suivante :
// spring boot
...
// dépendances
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plugin pour créer un binaire aux normes Maven dans le dépôt Maven local
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// change to point to your repo, e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- ligne 10 : on importe un plugin Gradle appelé [maven-publish] qui permet de publier le binaire du projet dans un dépôt Maven en respectant les normes Maven ;
- ligne 11 : une tâche Gradle appelée [publishing] ;
- lignes 14-15 : les caractéristiques du binaire Maven qui va être créé ;
- ligne 23 : le dépôt Maven sur lequel il sera publié, ici un dépôt Maven local ;
L'ajout du plugin [maven-publish] a créé de nouvelles tâches dans le projet Gradle :
![]() | ![]() |
Si en [2], on exécute la tâche [publish], le binaire du projet est créé et installé dans le dossier indiqué en ligne 23 du fichier [build.gradle] :
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
La tâche [jar] permet de générer le binaire du projet. Ce binaire est sans ses dépendances donc non exécutable. Il est possible de générer un binaire avec toutes ses dépendances et exécutable. Pour cela, nous ajoutons au fichier [build.gradle] le code suivant :
// créer un binaire avec toutes ses dépendances
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- ligne 6 : il faut mettre le nom complet de la classe exécutable du projet :
![]() |
Le code de cette classe sera le suivant :
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
Rafraîchissez le projet Gradle puis exécutez la tâche [fatJar] :
![]() | ![]() |
Le binaire est généré dans le dossier [build / libs] et peut être exécuté [1-7] :
![]() | ![]() |
1.16.1.3. Configuration du projet
La configuration Gradle ne suffit pas. Il nous faut également configurer le projet. Comme ce n'est pas un projet Android généré par l'IDE, cette configuration que nous ne faisions pas jusqu'à maintenant doit ici être faite.
![]() | ![]() |
- en [3-4] : prenez un JDK 1.8 ;
Pour compiler le projet, le bouton disponible pour les projets Android n'est plus présent. On utilisera une option du menu [1-2] :
![]() | ![]() |
Dans la suite, le lecteur est invité à créer le projet qui suit. Nous commentons le code final du projet [3].
1.16.1.4. La couche [métier]
![]() |
![]() |
La couche [métier] reprend l'esprit de la couche [métier] de l'exemple précédent. Elle aura l'interface [IMetier] suivante :
package exemples.android.server.metier;
public interface IMetier {
// nombre aléatoire dans [a,b]
int getAlea(int a, int b);
}
- ligne 5 : la méthode qui génère 1 nombre aléatoire dans [a,b]
Le code de la classe [Metier] implémentant cette interface est le suivant :
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// qqs vérifications
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
// génération résultat
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
Nous ne commentons pas la classe : elle est analogue à celle rencontrée dans l'exemple précédent si ce n'est qu'elle ne lance pas d'exceptions aléatoirement. On notera simplement ligne 8 l'annotation Spring [@Service] qui va faire que Spring va instancier la classe en un unique exemplaire (singleton) et rendre sa référence disponible pour d'autres composants Spring. D'autres annotations Spring auraient pu être utilisées ici pour le même effet. Les composants Spring ont des noms par défaut qui peuvent être précisés comme attribut de l'annotation utilisée. Sans cet attribut, comme ici, le composant Spring porte le nom de la classe avec son premier caractère en minuscule. Ainsi ici, le composant Spring porte ici et par défaut le nom [metier] ;
La classe [Metier] lance des exceptions de type [AleaException] :
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// code d'erreur
private int code;
// constructeurs
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters et setters
....
}
- ligne 3 : [AleaException] étend la classe [RuntimeException]. C'est donc une exception non contrôlée (pas d'obligation de la gérer avec un try / catch) ;
- ligne 6 : on ajoute à la classe [RuntimeException] un code d'erreur ;
1.16.1.5. Le service web / jSON
![]() |
![]() |
Le service web / jSON est implémenté par Spring MVC. Spring MVC implémente le modèle d'architecture dit MVC (Modèle – Vue – Contrôleur) de la façon suivante :
![]() |
Le traitement d'une demande d'un client se déroule de la façon suivante :
- demande - les URL demandées sont de la forme http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] est la classe de Spring qui traite les URL entrantes. Elle "route" l'URL vers l'action qui doit la traiter. Ces actions sont des méthodes de classes particulières appelées [Contrôleurs]. Le C de MVC est ici la chaîne [Dispatcher Servlet, Contrôleur, Action]. Si aucune action n'a été configurée pour traiter l'URL entrante, la servlet [Dispatcher Servlet] répondra que l'URL demandée n'a pas été trouvée (erreur 404 NOT FOUND) ;
- traitement
- l'action choisie peut exploiter les paramètres parami que la servlet [Dispatcher Servlet] lui a transmis. Ceux-ci peuvent provenir de plusieurs sources :
- du chemin [/param1/param2/...] de l'URL,
- des paramètres [p1=v1&p2=v2] de l'URL,
- de paramètres postés par le navigateur avec sa demande ;
- dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [metier] [2b]. Une fois la demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
- une page d'erreur si la demande n'a pu être traitée correctement
- une page de confirmation sinon
- l'action demande à une certaine vue de s'afficher [3]. Cette vue va afficher des données qu'on appelle le modèle de la vue. C'est le M de MVC. L'action va créer ce modèle M [2c] et demander à une vue V de s'afficher [3] ;
- réponse - la vue V choisie utilise le modèle M construit par l'action pour initialiser les parties dynamiques de la réponse HTML qu'elle doit envoyer au client puis envoie cette réponse.
Pour un service web / jSON, l'architecture précédente est légèrement modifiée :
![]() |
- en [4a], le modèle qui est une classe Java est transformé en chaîne jSON par une bibliothèque jSON ;
- en [4b], cette chaîne jSON est envoyée au navigateur ;
Un exemple de sérialisation d'un objet Java en chaîne jSON et de désérialisation d'une chaîne jSON en objet Java est présenté en annexes au paragraphe 6.14.
Revenons à la couche [web] de notre application :
![]() |
Dans notre application, il n'y a qu'un contrôleur :
![]() |
Le service web / jSON enverra à ses clients une réponse de type [Response] suivant :
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- propriétés
// statut de l'opération
private int status;
// les éventuels messages d'erreur
private List<String> messages;
// le corps de la réponse
private T body;
// constructeurs
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters et setters
...
}
- ligne 13 : le champ [T body] est la réponse attendue par le client. On a décidé d'avoir ici une réponse générique de type T, plutôt que le type Integer du nombre aléatoire attendu. Nous voulons pouvoir réutiliser cette classe dans d'autres situations. Lors du traitement pour traiter la demande du client, le serveur peut rencontrer un problème qui est alors résumé dans les deux autres champs ;
- ligne 8 : un code d'état (0 si pas d'erreur) ;
- ligne 9 : si status!=0, une liste de messages d'erreur, usuellement ceux de la pile d'exceptions si exception il y a eu, null si pas d'erreurs ;
Le contrôleur [WebController] est le suivant :
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// couche métier
@Autowired
private IMetier metier;
// mapper JSON
@Autowired
private ObjectMapper mapper;
// nombres aléatoires
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// la réponse
Response<Integer> response = new Response<>();
// on utilise la couche métier
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// on rend la réponse
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// liste des messages
List<String> messages = new ArrayList<String>();
// on parcourt la pile des exceptions
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// on rend le résultat
return messages;
}
}
- ligne 17 : l'annotation [@Controller] indique que la classe est un contrôleur MVC dont les méthodes traitent des requêtes pour certaines URL de l'application web ;
- lignes 21-22 : l'annotation [@Autowired] demande à Spring d'injecter dans le champ, un composant de type [IMetier]. Ce sera la classe [Metier] précédente. C'est parce que nous avons mis à celle-ci l'annotation [@Service] qu'elle est gérée comme un composant Spring ;
- lignes 24-25 : nous faisons de même avec un mappeur jSON que nous définirons ultérieurement. Notre service web va envoyer sa réponse sous la forme d'une chaîne jSON. C'est ce mappeur qui opèrera la sérialisation de la réponse en jSON ;
- ligne 30 : la méthode qui génère le nombre aléatoire. Son nom n'a pas d'importance. Lorsqu'elle s'exécute, ses paramètres 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 28 ;
- ligne 28 : l'annotation [@RequestMapping] définit certaines propriétés de la méthode annotée :
- [value] : l'URL acceptée par la méthode ;
- [method] : la méthode HTTP acceptée par la méthode. Il y en a principalement deux, GET et POST. La méthode [POST] est utilisée lorsque le client veut joindre un document à sa requête HTTP ;
- [produces] : fixe un des entêtes de la réponse HTTP qui sera faite au client. Ici, dans les entêtes HTTP envoyés avec la réponse du client, il y en aura un qui lui dira que la réponse lui est envoyée sous la forme d'une chaîne jSON. Cet entête n'est pas obligatoire. Elle est donnée à titre informatif au client si celui-ci attend des réponses qui peuvent avoir diverses formes ;
- [consumes] : n'est pas présente ici. Elle permet d'indiquer les entêtes HTTP qui doivent accompagner la requête HTTP du client pour qu'elle soit acceptée ;
- ligne 29 : l'annotation [@ResponseBody] indique que le résultat produit par la méthode doit être envoyé au client. Sans cette annotation, la réponse de la méthode est considérée comme une clé permettant de sélectionner la page HTML à envoyer au client. Dans un service web / jSON, il n'y a pas de pages HTML ;
- ligne 28 : l'URL traitée est de la forme /{a}/{b} où {x} représente une variable. Les variables {a} et {b} sont affectées aux paramètres de la méthode ligne 30. Cela se fait via l'annotation @PathVariable("x"). On notera que {a} et {b}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, la méthode getAlea de la ligne 30 s'exécutera avec les paramètres entiers a=100, b=200 ;
- ligne 36 : on demande à la couche [métier] un nombre aléatoire dans l'intervalle [a,b]. On se souvient que la méthode [metier].getAlea peut lancer une exception ;
- ligne 37 : pas d'erreur ;
- ligne 39 : code d'erreur ;
- ligne 40 : la liste des messages de la réponse est celle de la pile d'exceptions (lignes 46-57). Ici, nous savons que la pile ne contient qu'une exception mais nous avons voulu montrer une méthode plus générique ;
- ligne 43 : la réponse de type [Response<Integer>] est rendue sous la forme d'une chaîne jSON ;
1.16.1.6. Configuration du projet Spring
![]() |
Il existe diverses façons de configurer Spring :
- avec des fichiers XML ;
- avec du code Java ;
- avec un mix des deux ;
Nous choisissons de configurer notre application web avec du code Java. C'est la classe [Config] suivante qui assure cette configuration :
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
// configuration web ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mappeur jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- ligne 12 : on dit à Spring dans quels packages il va trouver les deux composants qu'il doit gérer :
- le composant [Metier] annoté [@Service] dans le package [exemples.android.server.metier] ;
- le composant [WebController] annoté [@Controller] dans le package [exemples.android.server.web] ;
- ligne 13 : l'annotation [@EnableWebMvc] permet à Spring Boot de faire lui-même un certain nombre de configurations standard pour une application Spring MVC. Cela décharge d'autant le développeur ;
- lignes 16, 22, 27 et 33 : l'annotation [@Bean] définit elle-aussi des composants (beans) Spring au même titre que les deux annotations rencontrées (@Service, @Controller). Ici l'annotation [@Bean] annote une méthode et non une classe et c'est le résultat de la méthode qui est le composant Spring. En l'absence d'attribut de nommage au sein de l'annotation [@Bean], le composant Spring créé porte le nom de la méthode annotée ;
- lignes 16-20 : définissent le bean [dispatcherServlet]. C'est un nom prédéfini de Spring MVC qui définit le front controller de l'application MVC, un objet par qui passent toutes les requêtes des clients et qui les dispatche (d'où son nom) aux différents [@Controller] de l'application Spring MVC ;
- ligne 18 : le bean [dispatcherServlet] est une instance de la classe [DispatcherServlet] fournie par Spring MVC ;
- lignes 22-25 : le bean [servletRegistrationBean] sert à définir quelles URL sont acceptées par l'application. Ligne 24, on accepte toutes les URL ;
- lignes 27-30 : le bean [embeddedServletContainerFactory] sert à définir le serveur embarqué dans les dépendances du projet qui doit héberger l'application web. La ligne 29 indique que c'est un serveur Tomcat et que celui-ci travaillera sur le port 8080. Par défaut, les binaires de ce serveur web sont amenés par la dépendance [org.springframework.boot:spring-boot-starter-web] du fichier Gradle ;
1.16.1.7. Exécution du service web / jSON
![]() |
Le projet s'exécute à partir de la classe exécutable [Boot] suivante :
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
// exécution application
SpringApplication.run(Config.class, args);
}
}
- la classe [Boot] est une classe exécutable (lignes 7-10) ;
- ligne 9 : la méthode statique [SpringApplication.run] est une méthode de [spring Boot] (ligne 4) qui va lancer l'application. Son premier paramètre est la classe Java qui configure le projet. Ici la classe [Config] que nous venons de décrire. Le second paramètre est le tableau d'arguments passé à la méthode [main] (ligne 7) ;
On peut lancer l'application web de diverses façons dont la suivante :
![]() |
Dans la console, apparaissent alors un certain nombre de logs :
- lignes 12-14 : le serveur embarqué Tomcat est lancé ;
- lignes 15-19 : la servlet [DispatcherServlet] de Spring MVC est chargée et configurée ;
- ligne 20 : l'URL [/{a}/{b}] du serveur web est détectée ;
Maintenant, prenons un navigateur et testons l'URL du service web / jSON :
![]() |
![]() |
![]() |
![]() |
Nous obtenons à chaque fois, la représentation jSON d'un objet de type [Response<Integer>].
Au lieu de prendre un navigateur standard, prenons maintenant l'extension [Advanced Rest Client] du navigateur Chrome (voir annexes, paragraphe 6.13) :

- en [1], l'URL demandée ;
- en [2], au moyen d'un GET ;
- en [3], on envoie la requête ;

- en [4], les entêtes HTTP de la réponse du serveur. On remarquera que celui-ci indique que le document envoyé est une chaîne jSON ;
- en [5], la chaîne jSON reçue ;
1.16.1.8. Génération du jar exécutable du projet
Au paragraphe 1.16.1.2, nous avons montré comment configurer le fichier Gradle pour générer un exécutable de l'application avec toutes ses dépendances. Adaptée à l'application présente, cet configuration devient la suivante :
// créer un binaire avec toutes ses dépendances
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
Pour générer cet exécutable, on peut procéder comme suit [1-5] :
![]() | ![]() |
Pour l'exécuter, on arrêtera le service web s'il est lancé [1], puis on exécutera l'archive [2-4] :
![]() | ![]() |
Prenez un navigateur et demandez l'URL [localhost:8080/100/200]. Vous devez obtenir les mêmes résultats qu'auparavant.
1.16.1.9. Gestion des logs
Lorsqu'on exécute l'archive exécutable, on constate qu'on n'a pas les mêmes logs que si on exécute le projet à partir de l'IDE. On a des logs en mode [DEBUG] :
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)
On peut gérer le niveau des logs en ajoutant un fichier [logback.xml] dans le dossier [resources] du projet :
![]() |
Ce fichier pourrait avoir le contenu suivant :
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- contrôle niveau des logs -->
<root level="info"> <!-- info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
Le niveau des logs est contrôlé ligne 12. Si maintenant, on régénère l'archive exécutable et qu'on exécute celle-ci, on n'a que des logs de niveau [info] :
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. Le client Android du serveur web / jSON
Le client Android aura l'architecture suivante :
![]() |
Le client aura deux composantes :
- une couche [Présentation] (vue+activité) analogue à celle que nous avons étudiée dans l'exemple [Exemple-14] ;
- la couche [DAO] qui s'adresse au service [web / jSON] que nous avons étudié précédemment.
1.16.2.1. Création du projet
Nous dupliquons le projet précédent [Exemple-14] dans [Exemple-15] en suivant la procédure du paragraphe 1.4. Nous obtenons le résultat suivant :
![]() | ![]() |
Dans la suite, le lecteur est invité à créer le projet qui suit.
1.16.2.2. Configuration Gradle
![]() |
Le fichier [build.gradle] est le suivant :
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// options de packaging nécessaires pour être capable de produire l'APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
Nous ne commentons que ce qui n'a pas déjà été rencontré :
- lignes 46-47 : insertion d'un plugin AA. Le plugin [rest-spring-api] permet de déléguer à la bibliothèque AA les échanges client / serveur ;
- ligne 50 : la bibliothèque [spring-android-rest-template] est la bibliothèque utilisée par AA pour assurer les échanges client / serveur. La version [2.0.0.M3] est une version dite 'milestone' qui ne se trouve pas dans les dépôts Maven habituels. Aussi doit-on préciser, lignes 56-59, le dépôt à utiliser (ligne 58) pour trouver la bibliothèque ;
- ligne 51 : une bibliothèque jSON ;
- lignes 33-39 : sans cette propriété, des erreurs apparaissent au moment de la génération du binaire APK du projet ;
1.16.2.3. Le manifeste de l'application Android
![]() |
Le fichier [AndroidManifest.xml] doit évoluer. En effet, par défaut, les accès Internet sont désactivés. Il faut les activer par une directive spéciale :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- ligne 5 : les accès internet sont autorisés ;
1.16.2.4. La couche [DAO]
![]() |
![]() |
1.16.2.4.1. L'interface [IDao] de la couche [DAO]
L'interface de la couche [DAO] sera la suivante :
package exemples.android.dao;
public interface IDao {
// nombre aléatoire
int getAlea(int a, int b);
// URL du service web
void setUrlServiceWebJson(String url);
// délai d'attente (ms) max de la réponse du serveur
void setTimeout(int timeout);
// délai d'attente en millisecondes du client avant requête
void setDelay(int delay);
}
- ligne 6 : la méthode du service web / jSON pour obtenir un nombre aléatoire dans l'intervalle [a,b] de ce service web ;
- ligne 9 : l'URL du service web / jSON de génération de nombres aléatoires ;
- ligne 12 : on se fixe un délai d'attente maximal pour attendre la réponse du serveur ;
- ligne 15 : on veut fixer un délai d'attente avant exécution de la requête au serveur, ceci pour laisser le temps à l'utilisateur d'annuler sa demande ;
1.16.2.4.2. L'interface [WebClient]
![]() |
L'interface [WebClient] s'occupe de dialoguer avec le service web. Son code est le suivant :
package exemples.android.dao;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// 1 nombre aléatoire dans l'intervalle [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- ligne 12 : [WebClient] est une interface que la bibliothèque AA va implémenter elle-même grâce aux annotations qu'on va y mettre. Cette interface doit implémenter les appels aux URL exposées par le service web / jSON :
// nombre aléatoire
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- ligne 11 : l'annotation [@Rest] est une annotation AA. La valeur de l'attribut [converters] est un tableau de convertisseurs. Ici le convertisseur [MappingJackson2HttpMessageConverter.class] fait que lorsque le serveur envoie une chaîne jSON, celle-ci est automatiquement désérialisée. Ainsi on voit en ligne (d), que l'URL [/{a}/{b}] renvoie un type String qui est en fait une chaîne jSON (ligne b). Avec ces informations et celle du type attendu ligne 16, l'instance [WebClient] du client va désérialiser la chaîne qu'il va recevoir en un type [Response<Integer>] ;
- ligne 15 : une annotation AA indiquant que l'URL doit être appelée avec une méthode HTTP GET. Le paramètre de l'annotation [@Get] est la forme de l'URL attendue par le service web. Il suffit de reprendre le paramètre [value] de l'annotation [@RequestMapping] (ligne b) de la méthode appelée dans le contrôleur [WebController] du serveur. Les accolades {} entourent les paramètres de l'URL qui doivent être repris dans les paramètres de la méthode ligne 16. La syntaxe [@Path("a") int a] fait que le paramètre [a] de la méthode est affecté à la valeur {a} de l'URL. Lorsque le paramètre de l'URL et celui de la méthode portent le même nom comme ici, on peut écrire plus simplement [@Path int a] ;
Dans le cas d'une requête HTTP POST, la méthode d'appel aurait la signature suivante :
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
C'est l'annotation [@Body] qui désigne la valeur postée. Celle-ci sera automatiquement sérialisée en jSON. Côté serveur, on aura la signature suivante :
// nombres aléatoires
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- ligne 2 : on précise qu'on attend une requête HTTP POST et que le corps de cette requête (objet posté) doit être transmis sous forme d'une chaîne jSON (attribut consumes) ;
- ligne 4 : la valeur postée sera récupérée dans le paramètre [@RequestBody T body] de la méthode ;
Revenons au code de la classe [WebClient] :
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- il nous faut pouvoir indiquer l'URL du service web à contacter. Ceci est obtenu en étendant l'interface [RestClientRootUrl] fourni par AA. Cette interface expose une méthode [setRootUrl(urlServiceWeb] qui permet de fixer l'URL du service web à contacter ;
- par ailleurs, nous voulons contrôler l'appel au service web car nous voulons limiter le temps d'attente de la réponse. Pour cela, nous étendons l'interface [RestClientSupport] qui expose la méthode [setRestTemplate] qui nous permettra de :
- créer nous-mêmes l'objet [RestTemplate] qui sert à gérer les échanges client / serveur ;
- paramétrer cet objet pour fixer le temps d'attente maximal de la réponse ;
1.16.2.4.3. La classe [Response]
La méthode [getAlea] de l'interface [IDao] rend une réponse du type [Response] suivant :
package exemples.android.dao;
import java.util.List;
public class Response<T> {
// ----------------- propriétés
// statut de l'opération
private int status;
// les éventuels messages d'erreur
private List<String> messages;
// le corps de la réponse
private T body;
// constructeurs
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters et setters
...
}
C'est la classe [Response] déjà utilisée côté serveur (paragraphe 1.16.1.5). En fait, d'un point de vue programmation, tout se passe comme si la couche [DAO] du client communiquait directement avec le contrôleur [WebController] du service web :
![]() |
La communication réseau entre client et serveur ainsi que la sérialisation / désérialisation des objets Java côté client sont transparents pour le programmeur.
1.16.2.4.4. Implémentation de la couche [DAO]
![]() |
L'interface [IDao] est implémentée avec la classe [Dao] suivante :
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// client du service REST
@RestService
protected WebClient webClient;
// mappeur jSON
private ObjectMapper mapper = new ObjectMapper();
// délay d'attente avant exécution requête
private int delay;
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- ligne 15 : nous annotons la classe [Dao] avec l'annotation [@EBean] pour en faire un bean AA qu'on va pouvoir injecter ailleurs ;
- lignes 19-20 : nous injectons l'implémentation qui sera faite de l'interface [WebClient] que nous avons décrite. C'est l'annotation [@RestService] qui assure cette injection ;
- les autres méthodes implémentent l'interface [IDao] (lignes 27-46) ;
Méthode [setTimeout]
La méthode [setTimeout] est la suivante :
@Override
public void setTimeout(int timeout) {
// on fixe le timeout des requêtes du client REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// on construit le restTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// on fixe le convertisseur jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// on fixe le restTemplate du client web
webClient.setRestTemplate(restTemplate);
}
- l'interface [WebClient] va être implémentée par une classe AA utilisant la dépendance Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implémente le dialogue du client avec le serveur web / jSON au moyen d'une classe de type [RestTemplate] ;
- ligne 4 : la classe [SimpleClientHttpRequestFactory] est fournie par la dépendance [spring-android-rest-template]. Elle va nous permettre de fixer le délai d'attente maximum de la réponse du serveur (lignes 5-6) ;
- ligne 8 : nous construisons l'objet de type [RestTemplate] qui va être le support de la communication avec le service web. Nous lui passons comme paramètre l'objet [factory] qui vient d'être construit ;
- ligne 10 : le dialogue client / serveur peut prendre diverses formes. Les échanges se font par lignes de texte et nous devons indiquer à l'objet de type [RestTemplate] ce qu'il doit faire avec cette ligne de texte. Pour cela, nous lui fournissons des convertisseurs, des classes capables de traiter les lignes de texte. Le choix du convertisseur se fait en général via les entêtes HTTP qui accompagnent la ligne de texte. Ici, nous savons que nous recevons uniquement des lignes de texte au format jSON. Par ailleurs, nous avons vu paragraphe 1.16.1.7, que le serveur envoyait l'entête HTTP :
Content-Type: application/json;charset=UTF-8
Ligne 10, l'unique convertisseur du [RestTemplate] sera un convertisseur jSON implémenté avec la bibliothèque [Jackson]. Il y a une bizarrerie à propos de ces convertisseurs : AA nous impose de l'avoir également dans l'annotation du client web [WebClient] :
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
Ligne 1, on est obligé de préciser un convertisseur alors même que nous le précisons par programmation.
- ligne 12 : l'objet [RestTemplate] ainsi construit est injecté dans l'implémentation de l'interface [WebClient] et c'est cet objet qui va opérer le dialogue client / serveur ;
Méthode [getAlea]
La méthode [getAlea] est la suivante :
@Override
public int getAlea(int a, int b) {
// exécution service
Response<Integer> info;
DaoException ex;
try {
// attente
waitSomeTime(delay);
// exécution service
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// on rend le résultat
return info.getBody();
} else {
// on note l'exception
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// on note l'exception
ex = new DaoException(e, 100);
}
// on lance l'exception
throw ex;
}
...
// méthodes privées -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- ligne 8 : on attend [delay] millisecondes ;
- ligne 10 : on se contente d'appeler la méthode de même signature dans la classe implémentant l'interface [WebClient] ;
- ligne 11 : on analyse la réponse obtenue du serveur en regardant son [status] ;
- lignes 12-14 : s'il n'y a pas eu d'erreur côté serveur (status=0), alors on rend le résultat de la méthode ;
- ligne 17 : s'il y a eu erreur côté serveur (status!=0), alors on prépare une exception sans la lancer. Le serveur a transmis une liste de messages d'erreur. Nous créons une exception avec, comme unique message, la chaîne jSON de la liste des messages du serveur ;
- lignes 19-22 : autres cas d'exception ;
- ligne 24 : lorsqu'on arrive là, il y a forcément eu une exception. Alors on la lance ;
L'exception [DaoException] utilisée par ce code est le suivant :
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// code d'erreur
private int code;
// constructeurs
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// getters et setters
...
}
- ligne 6 : l'exception [DaoException] est une exception non contrôlée ;
Méthode [setUrlServiceWebJson]
La méthode [setUrlServiceWebJson] est la suivante :
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// on fixe l'URL du service REST
webClient.setRootUrl(urlServiceWebJson);
}
- ligne 4 : on fixe l'URL du service web via la méthode [setRootUrl] de l'interface [WebClient]. C'est parce que cette interface étend l'interface [RestClientRootUrl] que cette méthode existe ;
1.16.2.5. Le package [architecture]
Le package [architecture] regroupe les éléments qui structurent l'application :
![]() |
![]() |
1.16.2.5.1. L'interface [IMainActivity]
L'interface [IMainActivity] liste les méthodes que doit implémenter l'activité de l'application :
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// accès à la session
Session getSession();
// changement de vue
void navigateToView(int position);
// attente
void beginWaiting();
void cancelWaiting();
// mode debug
boolean IS_DEBUG_ENABLED = true;
// délai d'attente de la réponse
int TIMEOUT = 1000;
// adjacence des fragments
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- ligne 5 : l'interface [IMainActivity] étend l'interface [IDao] ;
- lignes 13-16 : aux méthodes déjà présentes dans les exemples précédents (lignes 7-11), nous avons ajouté deux méthodes pour gérer l'image d'attente de l'application (lignes 14, 16) ;
- ligne 21 : on fixe un délai maximal d'attente de la réponse du serveur à 1 seconde ;
1.16.2.5.2. La classe [Utils]
On a rassemblé dans la classe [Utils] des méthodes utilitaires statiques qui peuvent être appelées de différents endroits de l'architecture de l'application :
package exemples.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// liste de messages d'une exception - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// on crée une liste avec les msg d'erreur de la pile d'exceptions
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// liste de messages d'une exception - version 2
static public String getMessagesForAlert(Throwable th) {
// on construit le texte à afficher
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// résultat
return texte.toString();
}
}
- lignes 9-18 : crée la liste des messages d'erreur contenu dans un Throwable ;
- lignes 21-32 : s'appuie sur la méthode précédente pour construire à partir de la liste de messages obtenue, le texte à afficher dans un message d'alerte Android ;
- lignes 27-28 : les messages sont numérotés. Le n° le plus petit (1) correspond à l'exception initiale et le n° le plus élevé à l'exception la plus récente dans la pile des exceptions ;
1.16.2.5.3. La classe abstraite [AbstractFragment]
La classe [AbstractFragment] a deux vocations :
- faire en sorte que la méthode [updateFragments] des classes filles soit toujours appelée lors de l'affichage du fragment et ce qu'une fois ;
- factoriser l'état et les méthodes des classes filles qui peuvent l'être ;
C'est la vocation 2 qui nous fait mettre dans cette classe les opérations de gestion de l'image d'attente : tous les fragments d'une application Android asynchrone ont à gérer ce type de problématique :
// gestion de l'attente
protected void beginWaiting() {
// on met le sablier
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// on enlève le sablier
mainActivity.cancelWaiting();
}
1.16.2.6. La vue
![]() |
1.16.2.6.1. La vue [vue1.xml]
![]() |
Par rapport à l'exemple précédent, la vue [vue1.xml] évolue de la façon suivante :
![]() |
![]() |
- en [1], l'utilisateur doit préciser l'URL du service web ainsi que le délai d'attente [2] avant chaque appel au service web ;
- en [3], les réponses sont comptées ;
- en [4], l'utilisateur peut annuler sa demande ;
- en [5], un indicateur d'attente s'affiche lorsque les nombres sont demandés. Il s'efface lorsqu'ils ont été tous reçus ou que l'opération a été annulée ;

- en [6], la validité des saisies est contrôlée ;
Le lecteur est invité à charger le fichier [vue1.xml] à partir des exemples. Pour la suite, nous donnons l'identifiant des nouveaux composants :

Les boutons [10-11] sont physiquement l'un sur l'autre. A un moment donné, on ne rendra visible que l'un des deux.
1.16.2.6.2. Le fragment [Vue1Fragment]
![]() |
Le squelette du fragment [Vue1Fragment] est le suivant :
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les éléments de l'interface visuelle
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// données locales
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// mémoire
afterViewsDone=true;
// au départ pas de messages d'erreur
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// bouton [Annuler] caché
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// liste des réponses
reponses = new ArrayList<>();
}
...
- lignes 24-49 : les références sur les composants de la vue [vue1.xml] (ligne 20) ;
- lignes 55-69 : la méthode [@AfterViews] exéutée lorsque les références des lignes 24-49 ont été initialisées ;
- ligne 58 : à ne pas oublier - nécessaire pour le cycle de vie du fragment ;
- lignes 60-63 : les messages d'erreur sont cachés ;
- lignes 65-66 : on cache le bouton [Annuler] (ligne 65) et on affiche le bouton [Exécuter] (ligne 66). On rappelle qu'ils sont physiquement l'un sur l'autre ;
- ligne 68 : le champ de la ligne 52 va contenir la liste des chaînes de caractères à afficher par le ListView des réponses ;
Juste après la méthode [@AfterViews], la méthode [updateFragment] suivante va être exécutée :
@Override
protected void updateFragment() {
// on crée l'adaptateur de la liste des réponses
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- lignes 4-5 : on crée l'adaptateur du ListView des réponses. Il est mémorisé dans une variable d'instance pour être disponible aux autres méthodes de la classe ;
Le 'clic' sur le bouton [Exécuter] provoque l'exécution de la méthode suivante :
// les saisies
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// données locales
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// on efface les réponses précédentes
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// on remet à 0 le compteur de réponses
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// on teste la validité des saisies
if (!isPageValid()) {
return;
}
// initialisation activité
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// on demande les nombres aléatoires
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// on commence l'attente
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// il faut faire le moins de choses possibles ici
// en tout cas aucun affichage - ceux ci doivent se faire dans l'UiThead
try {
// on affiche le résultat dans l'UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// on affiche l'exception dans l'UiThread
showAlert(e);
}
}
- lignes 17-18 : on efface la précédente liste de réponses du serveur. Pour cela, ligne 17, on vide la source de données [reponses] associée à l'adaptateur du ListView ;
- ligne 19 : un booléen qui nous servira à savoir si l'utilisateur a annulé ou non sa demande ;
- lignes 21-22 : on affiche un compteur nul pour le nombre de réponses ;
- lignes 24-26 : on récupère les saisies des lignes [2-6] et on vérifie leur validité. Si l'une d'elles est invalide, la méthode est abandonnée (ligne 25) et pour l'utilisateur il y a retour à l'interface visuelle ;
- lignes 28-29 : si les données saisies sont toutes valides, alors on transmet à l'activité l'URL du service web (ligne 28) ainsi que le délai d'attente avant chaque appel au service (ligne 29). Ces informations sont nécessaires à la couche [DAO] et on rappelle que c'est l'activité qui communique avec celle-ci ;
- lignes 31-33 : les nombres aléatoires sont demandés un par un à la méthode [getAlea] de la ligne 39 ;
- ligne 38 : la méthode [getAlea] est annotée avec l'annotation AA [@Background], ce qui fait qu'elle va être exécutée dans un autre thread (flux d'exécution, process) que celui dans lequel s'exécute l'interface visuelle. Il est en effet obligatoire d'exécuter tout appel internet dans un thread différent de celui de l'interface visuelle. Ainsi, à un moment donné, on pourra avoir plusieurs threads :
- celui qui affiche l'interface visuelle UI (User Interface) et gère ses événements,
- les [nbAleas] threads qui chacun demandent un nombre aléatoire au service web. Ces threads sont lancés de façon asynchrone : le thread de l'UI lance un thread [getAlea] (ligne 32) qui demande un nombre aléatoire au service web et n'attend pas sa fin. Celle-ci lui sera signalée par un événement. Ainsi, les [nbAleas] threads vont être lancés en parallèle. Il est possible de configurer l'application pour qu'elle ne lance qu'un thread à la fois. Il y a alors une file d'attente des threads à exécuter ;
Ligne 38, le paramètre [id] donne un nom au thread généré. Ici les [nbAleas] threads portent tous le même nom [alea]. Cela va nous permettre de les annuler tous en même temps. Ce paramètre est facultatif si on ne gère pas l'annulation du thread ;
- ligne 44 : la méthode [getAlea] de l'activité est appelée. Elle le sera donc dans un thread à part de celui de l'UI. Celui-ci fera l'appel au service web et n'attendra pas la réponse. Il sera prévenu plus tard par un événement que la réponse est disponible. C'est à ce moment que ligne 44, la méthode [showInfo] sera appelée avec comme paramètre la réponse reçue ;
- lignes 45-47 : l'exécution de la requête web peut produire une exception. On demande alors l'affichage des messages d'erreur de l'exception dans un message d'alerte ;
- ligne 35 : on se met en attente des résultats :
- un indicateur d'attente va être affiché ;
- le bouton [Annuler] va remplacer le bouton [Exécuter]. Parce que les threads lancés sont asynchrones, le thread de l'UI ne les attend pas et la ligne 35 est exécutée avant leur fin. Une fois la méthode [beginWaiting] terminée, l'UI peut de nouveau répondre aux sollicitations de l'utilisateur tel que le clic sur le bouton [Annuler]. Si les threads lancés avaient été synchrones, on arriverait à la ligne 35 qu'une fois tous les thread terminés. L'annulation de ceux-ci n'aurait alors plus de sens ;
La méthode [showInfo] est la suivante :
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// une info de plus
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// a-t-on terminé ?
if (nbInfos == nbAleas) {
// on termine l'attente
cancelWaiting();
}
// on ajoute l'information à la liste des reponses
reponses.add(0, String.valueOf(alea));
// on affiche les reponses
adapterReponses.notifyDataSetChanged();
}
}
- la méthode [showInfo] est appelée à l'intérieur du thread [getAlea] annotée par [@Background]. Cette méthode va mettre à jour l'interface visuelle UI. Elle ne peut le faire qu'en étant exécutée à l'intérieur du thread de l'UI. C'est la signification de l'annotation [@UiThread] de la ligne 1 ;
- ligne 2 : la méthode reçoit un nombre aléatoire ;
- ligne 3 : le corps de la méthode n'est exécuté que si l'utilisateur n'a pas annulé sa demande ;
- lignes 5-6 : on incrémente le compteur de réponses et on l'affiche ;
- lignes 8-11 : si on a reçu toutes les réponses attendues, alors on termine l'attente (fin du signal d'attente, le bouton [Exécuter] remplace le bouton [Annuler]) ;
- lignes 12-15 : on ajoute le nombre aléatoire reçu à la liste des réponses affichée par le composant [ListView listReponses] et on rafraîchit celui-ci ;
La méthode [showAlert] est la suivante :
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// on annule tout
doAnnuler();
// on l'affiche
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
On retrouve une logique analogue à celle de la méthode [showInfo] :
- ligne 1 : l'annotation [@UiThread] est obligatoire ;
- ligne 2 : la méthode reçoit l'exception qui s'est produite ;
- ligne 3 : la méthode est exécutée que si l'utilisateur n'a pas annulé sa demande ;
- ligne 5 : on annule la demande de l'utilisateur comme s'il avait cliqué lui-même sur le bouton [Annuler] ;
- ligne 7 : on affiche l'alerte à l'aide de la classe Android [AlertDialog] :
- [activity] : est l'activité de type [Activity] mémorisée dans la classe parent [AbstractFragment] ;
- [setTitle] : fixe le titre de la fenêtre d'alerte [1] ;
- [setMessage] : fixe le message affiché par la fenêtre d'alerte [2] ;
- [setNeutral] : fixe le bouton qui va fermer la fenêtre d'alerte [3] ;
- [show] : demande l'affichage de la fenêtre d'alerte ;
![]() |
Le 'clic' sur le bouton [Annuler] est géré avec la méthode suivante :
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// mémoire
hasBeenCanceled=true;
// on annule la tâche asynchrone
BackgroundExecutor.cancelAll("alea", true);
// fin de l'attente
cancelWaiting();
}
- ligne 4 : on note que l'utilisateur a annulé sa demande ;
- ligne 6 : annule toutes les tâches identifiées par la chaîne [alea]. Le second paramètre [true] signifie qu'elles doivent être annulées même si elles ont déjà été lancées. L'identifiant [alea] est celui utilisé pour qualifier la méthode [getAlea] du fragment (ligne 1 ci-dessous) :
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
Note : il s'est avéré que la ligne 6 du code de la méthode [doAnnuler] marchait incorrectement. C'est pour cette raison qu'on a ajouté le booléen [hasBeenCanceled]. En effet, en cas d'exception (serveur absent), on récupérait n fois la fenêtre d'alerte si on avait demandé n nombres aléatoires.
1.16.2.7. L'activité [MainActivity]
![]() |
1.16.2.7.1. La vue [activity-main.xml]
![]() |
Par rapport à l'exemple précédent, nous avons ajouté une image d'attente dans la vue associée à l'activité [MainActivity] :
...
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
<!-- image d'attente -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- image d'attente -->
</android.support.design.widget.AppBarLayout>
...
- lignes 17-21 : l'image d'attente ;
1.16.2.7.2. L'activité [MainActivity]
L'activité [MainActivity] bouge peu vis à vis de ce qu'elle était dans [Exemple-14]. Tout d'abord, on lui injecte la couche [DAO] :
// injection dao
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// on paramètre la couche [DAO]
setTimeout(TIMEOUT);
}
- lignes 2-3 : injection de la couche [DAO] par une annotation AA ;
- lignes 5-13 : code exécuté après cette injection ;
- ligne 12 : on fixe le timeout de la couche [DAO]
Par ailleurs, l'activité [MainActivity] doit implémenter l'interface [IMainActivity] qui elle-même étend l'interface [IDao] :
// implémentation IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// on affiche la vue position
if (mViewPager.getCurrentItem() != position) {
// affichage fragment
mViewPager.setCurrentItem(position);
}
}
// gestion de l'image d'attente
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// implémentation IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// exécution
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. Exécution du projet
Lancez le service web (paragraphe 1.16.1.7) puis lancez le client Android :

Pour savoir quoi mettre en [1], procédez comme suit. Ouvrez une fenêtre de commande et tapez la commande suivante :
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
Si vous avez installé [GenyMotion], la machine virtuelle VirtualBox a ajouté des adresses IP à votre poste (lignes 10 et 18). Ces adresses sont particulièrement pratiques car elles ne sont pas bloquées par le pare-feu de Windows. La ligne 30 donne l'adresse IP de votre poste sur un réseau local. Pour utiliser cette adresse, il faut en général inhiber le pare-feu de Windows. Si vous êtes connecté à un réseau wifi, utilisez l'adresse wifi et là aussi, inhibez le pare-feu si vous en avez un.
Testez l'application dans les cas suivants :
- 100 nombres aléatoires dans l'intervalle [1000, 2000] sans délai d'attente ;
- 2000 nombres aléatoires dans l'intervalle [10000, 20000] sans délai d'attente et annulez l'attente avant la fin de la génération ;
- 5 nombres aléatoires dans l'intervalle [100, 200] avec un délai d'attente de 5000 ms et annulez l'attente avant la fin de la génération ;
1.16.2.9. Gestion de l'annulation
Pour suivre ce qui se passe lorsque l'utilisateur demande l'annulation ou que celle-ci est demandée parce qu'une exception s'est produite, nous ajoutons la méthode suivante à l'interface [IDao] (cf paragraphe 1.16.2.4.1) :
package exemples.android.dao;
public interface IDao {
...
// mode debug
void setDebugMode(boolean isDebugEnabled);
}
Dans la classe [Dao], nous ajoutons le code suivant :
// mode debug
private boolean isDebugEnabled;
// nom de la classe
private String className;
..
// constructeur
public Dao() {
// nomde la classe
className = getClass().getSimpleName();
}
...
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// exécution service
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- ligne 9 : nous notons le nom de la classe ;
- lignes 16-18 : nous écrivons un log à chaque fois que la méthode [getAlea] est appelée ;
Par ailleurs, dans le fragment [Vue1Fragment], nous ajoutons les logs suivants :
@UiThread
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
A chaque fois que le fragment [Vue1Fragment] reçoit une information de la couche [DAO], un log est émis. Par ailleurs, lorsque la méthode [doAnnuler] est appelée, on logue l'événement.
Test 1
On demande 5 nombres alors que le serveur n'a pas été lancé. On a les logs suivants :
- lignes 1-5 : la méthode [getAlea] de la classe [Dao] est appelée cinq fois. On rappelle que ce sont des appels asynchrones faits par le fragment [VueFragment] et que celui-ci n'attend pas le résultat de son appel ;
- ligne 7 : la première requête HTTP a eu lieu et le fragment [VueFragment] a reçu sa première exception ;
- ligne 8 : il demande alors l'annulation de toutes les demandes ;
- lignes 9-12 : on voit cependant qu'il reçoit les quatre exceptions suivantes. Donc les requêtes asynchrones qui étaient en attente ont toutes été exécutées ;
Test 2
Maintenant, lançons le serveur et demandons 5 nombres avec un délai de 5 secondes et cliquons sur [Annuler] avant la fin de ce délai. Les logs sont les suivants :
- lignes 1-5 : la méthode [getAlea] de la classe [Dao] est appelée cinq fois ;
- ligne 7 : l'utilisateur a demandé l'annulation des requêtes ;
- ligne 8 : on voit que [Vue1_Fragment] reçoit 5 valeurs. De nouveau, les requêtes asynchrones qui étaient en attente ont toutes été exécutées ;
C'est la raison pour laquelle nous avons du gérer un booléen [hasBeenCanceled] afin d'éviter d'afficher quelque chose alors qu'une annulation avait été demandée. Dans le code de l'annulation :
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// mémoire
hasBeenCanceled = true;
// on annule la tâche asynchrone
BackgroundExecutor.cancelAll("alea",true);
// fin de l'attente
cancelWaiting();
}
le code de la ligne 10 ne fait pas ce qui est attendu. Il est possible que ce soit parce que les tâches asynchrones se partagent la même méthode annotée [@Background] :
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. Exemple-16 : gérer l'asynchronisme avec RxAndroid
Nous nous proposons maintenant de gérer l'asynchronisme nécessaire aux applications Android avec une bibliothèque appelée RxJava [http://reactivex.io/] et sa version dérivée pour l'environnement Android [RxAndroid]. Pour cela, nous utiliserons le cours [Introduction à RxJava. Application aux environnements Swing et Android].
1.17.1. Création du projet
Nous dupliquons le projet [Exemple-1] dans [Exemple-16] :
![]() | ![]() |
1.17.2. Configuration Gradle
![]() |
Dans [build.gradle], nous ajoutons la dépendance sur la bibliothèque [RxAndroid] :
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. La couche [DAO]
![]() |
1.17.4. L'interface [IDao]
L'interface [IDao] devient la suivante :
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// nombre aléatoire
Observable<Integer> getAlea(int a, int b);
// URL du service web
void setUrlServiceWebJson(String url);
// délai d'attente (ms) max de la réponse du serveur
void setTimeout(int timeout);
// délai d'attente en millisecondes du client avant requête
void setDelay(int delay);
// mode debug
void setDebugMode(boolean isDebugEnabled);
}
- ligne 8 : la méthode [getAlea] rend désormais un type [Observable] de la bibliothèque RxJava (ligne 3). Le principe est le suivant :
Un flux d'éléments de type Observable<T> est observé par un ou plusieurs souscripteurs (abonnés, observateurs, consommateurs) de type Subscriber<T>. La bibliothèque RxJava permet que le flux Observable<T> s'exécute dans un thread T1 et son observateur Subscriber<T> dans un thread T2 sans que le développeur n'ait à se soucier de gérer le cycle de vie de ces threads et de problèmes naturellement difficiles, tels que le partage de données entre threads et la synchronisation de ceux-ci pour exécuter une tâche globale. Elle facilite donc la programmation asynchrone.
1.17.5. La classe [AbstractDao]
Nous ferons dériver la classe [Dao] de la classe [AbstractDao] suivante :
package exemples.android.dao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mappeur jSON
private ObjectMapper mapper = new ObjectMapper();
// méthodes protégées ----------------------------------------------------------
// interface générique
protected interface IRequest<T> {
Response<T> getResponse();
}
// requête générique
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// exécution service
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// exécution service
try {
// on fait la requête synchrone et on fait suivre la réponse à l'abonné
Response<T> response = request.getResponse();
// erreur ?
int status = response.getStatus();
if (status != 0) {
// on note l'exception
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// on émet la réponse
subscriber.onNext(response.getBody());
// on signale la fin de l'observable
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// on note l'exception
ex = new DaoException(e, 100);
}
// exception ?
if (ex != null) {
// on émet l'exception
subscriber.onError(ex);
}
}
});
}
}
- la classe [AbstractDao] a comme principal élément une méthode générique [getResponse] qui sert à obtenir du serveur un type [Response<T>] où T est le type du résultat désiré par le client HTTP (ici Integer) ;
- ligne 20 : l'unique paramètre de la méthode générique [getResponse] est une instance de l'interface générique [IRequest<T>] des lignes 15-17. Celle-ci n'a qu'une méthode [getResponse] et c'est cette méthode qui fournit la réponse [Response<T>] désirée ;
- grâce aux deux éléments précédents, la classe [AbstractDao] peut servir de classe parent à toute couche [Dao] cliente d'un serveur envoyant des réponses de type [Response<T>] ;
- ligne 20 : la méthode générique [getResponse] rend un type [Observable<T>] qui représente le résultat réellement attendu par le client HTTP (ici un type Observable<Integer>) ;
- lignes 22-51 : la méthode statique [rx.Observable.create] crée un type [Observable] ;
- ligne 22 : l'unique paramètre de cette méthode est une instance du type [rx.Observable.OnSubscribe<T>], une interface qui possède les méthodes suivantes :
- [onNext(T element)] : permet d'émettre vers un observateur un élément de type T ;
- [onError(Throwable th)] : permet d'émettre vers un observateur une exception ;
- [onCompleted] : permet d'indiquer à un observateur la fin des émissions ;
Un type [Observable<T>] obéit à certaines contraintes :
- il émet ses éléments avec la méthode [onNext(T element)] ;
- la méthode [onCompleted] doit être appelée une unique fois dès qu'il n'y a plus d'éléments à émettre vers l'observateur ;
- la méthode [onCompleted] n'est pas appelée si la méthode [onError(Throwable th)] l'a été ;
Dans notre exemple :
- l'observateur sera le fragment [Vue1Fragment]. C'est lui qui consomme les éléments émis par le [Observable<T>] (élément ou exception) ;
- le type [Observable<T>] créé n'émettra qu'un unique élément (ligne 37) ;
- ligne 29 : fait une requête HTTP synchrone au serveur et obtient le type [Response<T>]. Cette requête HTTP est assurée par le type [IRequest] passé en paramètre à la méthode générique [getResponse] ;
- ligne 31 : on récupère le status de la réponse ;
- lignes 32-34 : si ce status est celui d'une erreur, on prépare une exception ;
- lignes 36-39 : si ce status n'est pas celui d'une erreur, alors on émet la réponse attendue réellement par le client (ligne 37) et on indique à l'observateur qu'il n'y aura plus d'autres émissions (ligne 39) ;
- lignes 41-44 : si la requête HTTP se termine par une exception, on note celle-ci ;
- lignes 46-49 : si l'exception [ex] est différente de null, alors on l'émet vers l'observateur. Il n'y a là pas lieu d'appeler la méthode [onCompleted] pour indiquer à l'observateur qu'il n'y aura plus d'émissions d'éléments. C'est implicite ;
On retiendra de ces explications que :
- la méthode générique [<T> Observable<T> getResponse(final IRequest<T> request)] rend un type [Observable<T>] qui n'émet qu'un élément de type T ou bien une exception ;
- que cette méthode admet pour unique paramètre un type [IRequest<T>] dont l'unique méthode [getResponse()] réalise l'accès HTTP qui renvoie le type [Response<T>] ;
1.17.6. La classe [Dao]
La classe [Dao] évolue de la façon suivante :
@EBean
public class Dao extends AbstractDao implements IDao {
// client du service REST
@RestService
protected WebClient webClient;
// délay d'attente avant exécution requête
private int delay;
// mode debug
private boolean isDebugEnabled;
// nom de la classe
private String className;
// constructeur
public Dao() {
// nomde la classe
className = getClass().getSimpleName();
}
// interface IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// exécution client web
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// attente
waitSomeTime(delay);
// appel HTTP synchrone
return webClient.getAlea(a, b);
}
});
}
...
- ligne 2 : la classe [Dao] étend la classe [AbstractDao] ;
- ligne 24 : la méthode [getAlea] rend désormais un type [Observable<Integer>] ;
- ligne 30 : appel de la méthode générique [getResponse] de la classe parent. On lui passe un paramètre de type [IRequest<Integer>] ;
- lignes 32-37 : implémentation de l'interface [IRequest<Integer>] ;
- ligne 36 : on fait la requête HTTP via l'interface AA [webClient] comme il avait été fait précédemment. On sait qu'on va récupérer un type [Response<Integer>] qui est bien le type que doit rendre la méthode [IRequest<Integer>.getReponse()] ;
- ligne 36 : on utilise ici une propriété appelée closure : la capacité d'encapsuler dans une instance des valeurs extérieures à celle-ci lorsqu'elle est créée, ici les valeurs de [a, b] de la ligne 24. C'est ce qui permet à la méthode [IRequest<Integer>.getReponse()] de ne pas avoir de paramètres. Ceux-ci ont été gravés dans le corps de la méthode. Et là où normalement on changerait les paramètres de la méthode (a,b) -> (x,y), ici on crée une nouvelle instance de [IRequest<Integer>] encapsulant les valeurs de x et y ;
1.17.7. La classe [MainActivity]
La classe [MainActivity] qui implémente l'interface [IDao] évolue comme suit :
// implémentation IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// exécution
return dao.getAlea(a, b);
}
1.17.8. La classe [Vue1Fragment]
La classe [Vue1Fragment] évolue de la façon suivante :
@Click(R.id.btn_Executer)
protected void doExecuter() {
// on efface les reponses précédentes
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// on remet à 0 le compteur de réponses
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// on teste la validité des saisies
if (!isPageValid()) {
return;
}
// initialisation activité
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// on demande les nombres aléatoires
getAleasInBackground(a, b);
// on commence l'attente
beginWaiting();
}
- ligne 18 : on demande les nombres aléatoires à la méthode [getAleasInBackground] appelée ainsi parce que les nombres vont être demandés dans un thread différent de celui de l'Ui ;
private int nbReponses = 0;
// les abonnements aux observables
private List<Subscription> abonnements;
// annotation [Background] inutile
void getAleasInBackground(int a, int b) {
// au départ pas de réponses et pas d'abonnements
nbReponses = 0;
abonnements.clear();
// on prépare l'observable
Observable<Integer> response = Observable.empty();
// on fusionne les résultats des différents appels HTTP
// ils sont exécutés sur un thread d'E/S
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// l'observable cumulé sera observé sur le thread de l'UI
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// on exécute l'observable
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// on ajoute l'information à la liste des reponses
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// message d'erreur
showAlert(th);
// fin attente
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// fin attente
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// on affiche l'exception dans l'UiThread
showAlert(e);
}
}
- ligne 3 : un observable a des abonnés. Le lien entre un abonné et le processus qu'il observe est appelé un abonnement (Subscription). Ici nous n'aurons qu'un processus observé et qu'un abonné. Nous n'aurons donc qu'un abonnement. Pour le principe, nous faisons comme si nous pouvions avoir plusieurs processus observés par différents observateurs, ce qui ferait plusieurs abonnements ;
- lignes 11-18 : on configure le processus observé (observable). Il faut comprendre que ce n'est que de la configuration : le processus n'est pas exécuté ;
- ligne 11 : on part d'un observable vide, un observable qui n'émet rien ;
- lignes 14-16 : à cet observable vide, on ajoute les [nbAleas] observables qui seront les [nbAleas] requêtes HTTP qui ramèneront [nbAleas] nombres aléatoires ;
- ligne 15 : comme précédemment, le nombre aléatoire n° i est demandé à la classe [MainActivity]. Il faut vraiment comprendre qu'ici, aucune requête HTTP n'est encore exécutée. La méthode [mainActivity.getAlea(a, b)] est exécutée et rend un type [Observable<Integer>]. C'est un processus qui sera observé lorsqu'il sera lancé ;
- ligne 15 : la méthode [subscribeOn(Schedulers.io())] demande à ce que le processus soit exécuté (lorsqu'il le sera) sur un thread d'E/S. La bibliothèque RxJava offre différents types de thread. Celui d'E/S est adapté aux appels HTTP ;
- ligne 15 : l'observable n° i est fusionné à l'observable initial de la ligne 11 : des [nbAleas] Observables émettant chacun un élément, on crée un observable qui lui émettra [nbAleas] éléments. C'est lui qui sera observé. Cet observable émet la notification [onCompleted] lorsque tous les observables qui le composent auront émis leur propre notification [onCompleted]. Cela nous évitera d'avoir à compter les réponses, comme nous l'avions fait dans la version précédente, pour savoir si on a reçu tous les nombres attendus ;
- ligne 18 : lorsqu'on arrive là, on a configuré un observable qui est la composition de [nbAleas] observables s'exécutant chacun sur un thread d'E/S ;
- ligne 18 : la méthode [observeOn(AndroidSchedulers.mainThread())] sert à dire sur quel thread l'observation des valeurs émises par l'observable doit se faire. Ici le thread [AndroidSchedulers.mainThread())] appartient à la bibliothèque RxAndroid et non RxJava. Il désigne le thread de l'Ui appelé également event loop. Ce point est important : dans une application Android, la modification d'un composant de l'Ui ne peut se faire que dans le thread de l'Ui, sinon on a une exception ;
- lignes 19-45 : maintenant que le processus à observer a été configuré, on l'exécute ;
- ligne 21 : c'est l'opération [Observable.subscribe] qui lance l'exécution du processus observé. Cette opération va lancer les [nbAleas] processus asynchrones configurés précédemment. Les résultats de ceux-ci seront automatiquement mis à disposition de l'observateur sur le thread de l'Ui ;
- nous nous rappelons que l'observable émet trois types d'événements :
- [onNext] : lorsqu'il émet un élément ;
- [onError] : lorsqu'il a rencontré une exception ;
- [onCompleted] : lorsqu'il signale qu'il ne va plus émettre ;
La méthode [Observable.subscribe] a pour paramètres, trois objets [Action1<Integer>, Action1<Throwable>, Action0] dont les méthodes [call] servent à traiter chacun de ces trois événements ;
- lignes 21-27 : le 1er paramètre de type [Action1<Integer>] sert à traiter l'événement [onNext]. Sa méthode [call] reçoit l'élément qui a été émis par l'observable (ligne 23) ;
- ligne 25 : on réutilise la méthode [showInfo] de l'exemple précédent ;
- lignes 27-35 : le second paramètre de type [Action1<Throwable>] sert à traiter l'événement [onError]. Sa méthode [call] reçoit l'exception qui a été émis par l'observable (ligne 29) ;
- ligne 31 : on réutilise la méthode [showAlert] de l'exemple précédent ;
- ligne 33 : on lance la procédure d'annulation de la demande de l'utilisateur. Cela va consister à annuler tous les observables qui sont en cours d'exécution ;
- lignes 35-41 : le troisième paramètre de type [Action0] sert à traiter l'événement [onCompleted]. Sa méthode [call] ne reçoit aucun paramètre ;
- ligne 39 : on annule l'attente ;
La méthode [showInfo] évolue de la façon suivante :
// annotation [UiThread] inutile
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// une info de plus
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// on ajoute l'information à la liste des reponses
reponses.add(0, String.valueOf(alea));
// on affiche les reponses
adapterReponses.notifyDataSetChanged();
}
}
La méthode présente deux changements :
- ligne 1 : on a enlevé l'annotation AA [@UiThread] ;
- on ne compte plus les réponses pour savoir si on doit arrêter ou non l'attente. C'est désormais l'événement [onCompleted] de l'observable qui nous donne cette information ;
La méthode [showAlert] évolue de la façon suivante :
// annotation [UiThread] inutile
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// on annule tout
doAnnuler();
// on l'affiche
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- le seul changement est ligne 1 : on a enlevé l'annotation AA [@UiThread] ;
Enfin, la méthode [doAnnuler] évolue comme suit :
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// mémoire
hasBeenCanceled = true;
// on annule les tâches asynchrones
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// fin de l'attente
cancelWaiting();
}
- ligne 12 : annule un abonnement et donc l'observation du processus associé ;
1.17.9. Exécution
Lancez le service web (paragraphe 1.16.1.7), lancez le client Android et refaites les tests que vous avez faits avec l'exemple précédent (paragraphe 1.16.2.8).
1.17.10. Gestion de l'annulation
On refait les mêmes tests que pour l'exemple précédent (paragraphe 1.16.2.9).
Test 1
On demande 5 nombres alors que le serveur n'a pas été lancé. On a les logs suivants :
Après la ligne 7, il n'y a plus de logs, ce qui montre que l'observateur (Vue1Fragment) ne reçoit plus de notifications du processus observé.
Test 2
Maintenant, lançons le serveur et demandons 5 nombres avec un délai de 5 secondes et cliquons sur [Annuler] avant la fin de ce délai. Les logs sont les suivants :
Après la ligne 6, il n'y a plus de logs ce qui montre que l'observateur (Vue1Fragment) ne reçoit plus de notifications du processus observé.
On a là le comportement attendu d'une annulation. On peut donc dans le code de [Vue1Fragment] enlever le booléen [hasBeenCanceled] que nous avions introduit dans l'exemple précédent parce que l'annulation ne faisait pas ce qu'on attendait d'elle.
Le fait que l'observateur ne reçoive plus de notifications après l'annulation de l'observable, ne veut pas dire que les requêtes HTTP sont elles-mêmes annulées. Pour le voir, nous modifions la classe [Dao] de la façon suivante :
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// exécution client web
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// attente
waitSomeTime(delay);
// appel HTTP synchrone
Response<Integer> response= webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- lignes 15-21 : nous loguons le résultat de la requête HTTP de la ligne 14 ;
Les logs pour le test n° 2 sont alors les suivants :
- lignes 1-5 : les 5 demandes ont été faites ;
- ligne 6 : l'utilisateur a annulé ;
- lignes 7-11 : on reçoit bien les réponses des cinq requêtes HTTP. Seulement, à cause de l'annulation de l'observable, ces éléments ne sont pas transmis à l'observateur ;
1.17.11. Conclusion
Dans la suite de ce document, les applications client / serveur seront réalisées avec la bibliothèque RxAndroid plutôt qu'avec la bibliothèque AA pour les raisons suivantes :
- RxAndroid peut s'utiliser dans une application Android n'utilisant pas AA ;
- RxAndroid fait plus que faciliter les opérations asynchrones. Elle offre de très nombreuses méthodes pour créer un nouvel observable à partir d'un autre. Ces méthodes n'ont pas d'équivalent AA ;
- dès qu'on veut dériver une classe annotée par AA, telle qu'un fragment, on rencontre de sérieux problèmes. On est alors amené à abandonner AA et à utiliser la solution 1 pour la programmation asynchrone ;
Le lecteur intéressé par approfondir les possibilités de la bibliothèque RxAndroid pourra lire le document [Introduction à RxJava. Application aux environnements Swing et Android]. On y utilise RxAndroid sans la bibliothèque AA.
1.18. Exemple-17 : 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.
1.18.1. Création du projet
Nous dupliquons le projet [Exemple-13] dans [Exemple-17] :
![]() | ![]() |
Le nouveau projet n'aura qu'une vue [vue1.xml]. Aussi supprimons-nous la vue [vue2.xml] et son fragment associé [Vue2Fragment] [2]. Nous prenons en compte cette modification dans le gestionnaire de fragments de [Mainactivity] :
// notre gestionnaire de fragments à redéfinir pour chaque application
// doit définir les méthodes suivantes : getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// les fragments
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
Réexécutez le projet. Il doit faire apparaître la vue n° 1 comme précédemment. Nous allons travailler à partir de ce projet.
1.18.2. La vue XML du formulaire
![]() |
La vue produite par le fichier [vue1.xml] est la suivante :

Le texte XML de la vue est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
Les principaux composants du formulaire sont les suivants :
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.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 / strings.xml] suivant :
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. Le fragment du formulaire
![]() |
La classe [Vue1Fragment] est la suivante :
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
// un fragment est une vue affichée par un conteneur de fragments
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les champs de la vue affichée par le fragment
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// liste déroulante
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// on coche le premier bouton
radioButton1.setChecked(true);
// le calendrier
datePicker1.setCalendarViewShown(false);
// le seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// la liste déroulante
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
protected void updateFragment() {
// initialisation adaptateur de la liste déroulante
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
}
- lignes 22-49 : on récupère les références de tous les composants du formulaire XML [vue1] (ligne 18) ;
- ligne 58 : la méthode [setChecked] permet de cocher un bouton radio ou une case à cocher ;
- ligne 60 : par défaut le composant [DatePicker] affiche et une boîte de saisie de la date et un calendrier. La ligne 60 élimine le calendrier ;
- ligne 62 : [SeekBar].setMax() permet de fixer la valeur maximale de la barre de réglage. La valeur minimale est 0 ;
- lignes 63-74 : 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 49 ;
- ligne 71 : le paramètre [progress] représente la valeur de la règle ;
- lignes 76-79 : une liste de [String] qu'on va associer à la liste déroulante ;
- ligne 90 : la méthode [updateFragment] du fragment. Lorsqu'elle est exécutée, la variable [activity] de la classe parent a été initialisée ;
- ligne 92 : la source de données [list] est associée à l'adaptateur de la liste déroulante ;
- lignes 93-94 : l'adaptateur [dataAdapter] est associé à la liste déroulante [dropDownList] ;
- ligne 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 :
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// liste des messages à afficher
List<String> messages = new ArrayList<>();
// case à cocher
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// les boutons radio
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// le SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// le champ de saisie
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// le switch
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// la date
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// le texte multi-lignes
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// l'heure
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// liste déroulante
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// affichage
doAfficher(messages);
}
- ligne 4 : les valeurs saisies vont être cumulées dans une liste de messages ;
- ligne 6 : la méthode [CheckBox].isCkecked() permet de savoir si une case est cochée ou non ;
- ligne 9 : 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 10 : le code [activity.findViewById(id)] permet de retrouver le bouton radio coché et d'avoir ainsi son libellé ;
- ligne 13 : la méthode [SeekBar].getProgress()] permet d'avoir la valeur d'une barre de réglage ;
- ligne 19 : la méthode [Switch].isChecked() permet de savoir si un switch est On (true) ou Off (false) ;
- ligne 22 : la méthode [DatePicker].getYear() permet d'avoir l'année choisie avec un objet [DatePicker] ;
- ligne 23 : la méthode [DatePicker].getMonth() permet d'avoir le mois choisi avec un objet [DatePicker] dans l'intervalle [0,11] ;
- ligne 24 : la méthode [DatePicker].getDayOfMonh() permet d'avoir le jour du mois choisi avec un objet [DatePicker] dans l'intervalle [1,31] ;
- ligne 30 : la méthode [TimePicker].getHour() permet d'avoir l'heure choisie avec un objet [TimePicker] ;
- ligne 31 : la méthode [TimePicker].getMinute() permet d'avoir les minutes choisies avec un objet [TimePicker] ;
- ligne 34 : la méthode [Spinner].getSelectedItemPosition() permet d'avoir la position de l'élément sélectionné dans une liste déroulante ;
- ligne 35 : 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 :
private void doAfficher(List<String> messages) {
// on construit le texte à affiche
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// on l'affiche
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- 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 :

1.18.5. Exécution du projet
Exécutez le projet et testez les différents composants de saisie.
1.19. Exemple-18 : utilisation d'un patron de vues
1.19.1. Création du projet
Nous créons un nouveau projet [Exemple-18] par recopie du projet [Exemple-13].
![]() | ![]() |
1.19.2. Le patron des vues
Nous voulons reprendre les deux vues du projet et les inclure dans un patron :
![]() |

Chacune des deux vues sera 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 [activity_main.xml] de l'activité ;
![]() | ![]() |
Le code XML de la vue [main] est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- l'entête [1] est obtenu avec les lignes 38-54 ;
- la bande gauche [2] est obtenue avec les lignes 56-84 ;
- le bas de page [3] est obtenu avec les lignes 86-101 ;
- le contenu [4] est obtenu avec les lignes 78-84 ;
La vue XML [main] utilise des informations trouvées dans les fichiers [res / values / colors.xml] et [res / values / strings.xml] :
![]() |
Le fichier [colors.xml] est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
et le fichier [strings.xml] le suivant :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
Créez un contexte d'exécution pour ce projet et exécutez-le.
1.20. Exemple-19 : 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 ;
1.20.1. Création du projet
Nous créons un nouveau projet [Exemple-19] par recopie du projet [Exemple-18].
![]() | ![]() |
![]() |
Nous allons faire évoluer le projet comme indiqué en [3].
1.20.2. La session
![]() |
La session mémorise les données partagées entre activité et fragments :
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// une liste de données
private List<Data> liste=new ArrayList<>();
// getters et setters
...
}
- ligne 11 : la liste de données exploitée par les deux vues ;
La classe [Data] est la suivante :
package exemples.android.architecture;
public class Data {
// données
private String texte;
private boolean isChecked;
// constructeur
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// getters et setters
...
}
- ligne 6 : le texte qui va alimenter le premier [TextView] de chaque élément de la liste ;
- ligne 7 : le booléen qui va servir à cocher ou non le [checkBox] de chaque élément de la liste ;
1.20.3. L'activité [MainActivity]
Le code de la méthode [@AfterInject] devient le suivant :
// injection session
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// on crée une liste de données
List<Data> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- lignes 12-15 : initialisation de la liste des données présente en session ;
1.20.4. La vue [Vue1] initiale
![]() | ![]() |
La vue XML [vue1.xml] affiche la zone [1] ci-dessus. Son code est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- lignes 7-16 : le composant [TextView] [2] ;
- lignes 27-35 : le composant [ListView] [4] ;
- lignes 18-25 : le composant [Button] [3] ;
1.20.5. La vue répétée par le [ListView]
![]() | ![]() |
La vue répétée par le [ListView] est la vue [list_data] suivante :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Libellé"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- lignes 8-14 : le composant [TextView] [1] ;
- lignes 16-23 : le composant [CheckBox] [2] ;
- lignes 25-35 : le composant [TextView] [3] ;
1.20.6. Le fragment [Vue1Fragment]
![]() |
Le fragment [Vue1Fragment] gère la vue XML [vue1]. Son code est le suivant :
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// les champs de la vue affichée par le fragment
@ViewById(R.id.listView1)
protected ListView listView;
// l'adaptateur de liste
private ListAdapter adapter;
// init done
private boolean initDone = false;
@AfterViews
void afterViews() {
// mémoire
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// on navigue vers la vue 2
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// on associe des données au [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// cas où le fragment a été (ré)généré - dans ce cas il faut relier de nouveau le ListView à son adaptateur
listView.setAdapter(adapter);
// cas où d'autres fragments ont changé la source de données - dans ce cas il faut rafraîchir le ListView
adapter.notifyDataSetChanged();
}
}
- ligne 15 : la vue XML [vue1] est associée au fragment ;
- lignes 26-30 : la méthode [@AfterViews] ne fait rien. Elle est cependant nécessaire pour mettre la variable [afterViewsDone] à true car celle-ci est utilisée par la classe parent [AbstractFragment] ;
- lignes 42-53 : la méthode [updateFragment] qui est appelée à chaque fois que le fragment va être visible. La méthode a été écrite ici comme si le fragment pouvait sortir de l'adjacence du fragment affiché et donc réinitialiser son cycle de vie. Ce n'est pas le cas ici, mais ce serait le cas si l'application venait à avoir 3 fragments avec une adjacence de 1 ;
- ligne 44 : l'adaptateur du [ListView] n'a besoin d'être initialisé qu'une fois ;
- ligne 46 : on associe à ce [ListView] un adaptateur de type [ListAdapter]. 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]. Nous passons diverses informations au constructeur de [ListAdapter] :
- une référence sur l'activité courante,
- l'identifiant 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 gérer le clic sur un lien [Retirer] du [ListView] par la méthode [doRetirer] de la ligne 38 ;
- ligne 50 : l'adaptateur est associé au [ListView]. Par la même occasion, la source de données [listes] est associé au [ListView]. Cette opération sera ici faite à chaque fois que la vue n° 1 est affichée. Elle n'aurait besoin d'être faite en réalité que lorsque la méthode [@AfterViews] a été exécutée. Ici l'instruction est exécutée trop souvent. On sent le besoin d'un booléen qui nous dirait que la méthode [@AfterViews] vient juste d'être exécutée et que donc le [ListView] doit être de nouveau associé à son adaptateur ;
- ligne 52 : on rafraîchit le [ListView]. Dans cet exemple, cela ne sert à rien car seule la vue n° 1 peut modifier la source de données du [ListView]. On se place dans un cas plus général où la vue n° 2 pourrait elle aussi changer la source de données du [ListView]. On rencontrera de tels exemples plus loin dans ce document. Dans ce cas, lorsqu'on passe de la vue n° 2 à la vue n° 1, le [ListView] de la vue n° 1 doit être rafraîchi ;
1.20.7. 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 :
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// le contexte d'exécution
private Context context;
// l'id du layout d'affichage d'une ligne de la liste
private int layoutResourceId;
// les données de la liste
private List<Data> data;
// le fragment qui affiche le [ListView]
private Vue1Fragment fragment;
// l'adapteur
final ListAdapter adapter = this;
// constructeur
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// on mémorise les infos
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- 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 :
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// on crée la ligne courante du ListView
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// le texte
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// la case à cocher
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// le lien [Retirer]
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// on gère le clic sur la case à cocher
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// on rend la ligne
return row;
}
- ligne 2 : 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 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 ;
1.20.8. Retirer un élément de la liste
Le clic sur le lien [Retirer] est géré dans le fragment [Vue1Fragment] par la méthode [doRetirer] suivante :
public void doRetirer(int position) {
// on enlève l'élément n° [position] dans la liste
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// on note la position du scroll pour y revenir
// lire
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position du 1er élément visible complètement ou non
int firstPosition = listView.getFirstVisiblePosition();
// offset Y de cet élément par rapport au haut du ListView
// mesure la hauteur de la partie éventuellement cachée
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// on rafraîchit le [ListView]
adapter.notifyDataSetChanged();
// on se positionne au bon endroit du ListView
listView.setSelectionFromTop(firstPosition, top);
}
- 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 rafraîchit le [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 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.
1.20.9. La vue XML [Vue2]
![]() | ![]() |
Le code XML de la vue est le suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- lignes 6-15 : le composant [TextView] n° 1 ;
- lignes 26-33 : le composant [TextView] n° 2 ;
- lignes 17-24 : le composant [Button] n° 3 ;
1.20.10. Le fragment [Vue2Fragment]
![]() | 123 ![]() |
Le fragment [Vue2Fragment] gère la vue XML [vue2]. Son code est le suivant :
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// les champs de la vue
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// mémoire
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// on navigue vers la vue 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// on affiche les éléments de la liste qui ont été sélectionnés dans la vue 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
Le code important est dans la méthode [updateFragment] de la ligne 32 :
- ligne 34 : on calcule le texte à afficher dans le [TextView] n° 2 ;
- lignes 35-39 : on parcourt la liste des données affichée par le [ListView]. Elle est stockée dans l'activité ;
- ligne 36 : si la donnée n° i a été cochée, on ajoute le libellé associé dans un type [StringBuilder] ;
- ligne 41 : le [TextView] affiche le texte calculé ;
1.20.11. Exécution
Créez une configuration d'exécution pour ce projet et exécutez-la.
1.20.12. 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 :
package exemples.android.fragments;
public class Data {
// données
private String texte;
private boolean isChecked;
// constructeur
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
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 :
package exemples.android.fragments;
public class Data {
// données
private String texte;
// constructeur
public Data(String texte) {
this.texte = texte;
}
// getters et setters
...
}
On crée une classe [CheckedData] dérivée de la précédente :
package exemples.android.fragments;
public class CheckedData extends Data {
// élément coché
private boolean isChecked;
// constructeur
public CheckedData(String text, boolean isChecked) {
// parent
super(text);
// local
this.isChecked = isChecked;
}
// getters et setters
...
}
Il suffit ensuite de remplacer partout dans le code (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment), le type [Data] par le type [CheckedData]. Par exemple dans [MainActivity] :
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// on crée une liste de données
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
Le projet de cette version vous est fourni sous le nom [Exemple-19B].
1.21. Exemple-20 : utiliser un menu
1.21.1. Création du projet
Nous dupliquons le projet [Exemple-19B] dans le projet [Exemple-20] :
![]() | ![]() |
![]() | 3 ![]() |
Nous allons supprimer les boutons des vues 1 et 2 pour les remplacer par des options de menu [1-2].
1.21.2. La définition XML des menus
![]() |
Le fichier [res / menu / menu_vue1] définit le menu de la vue n° 1 :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
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 ;
- app: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 ;
- une option de menu peut elle-même être un sous-menu (balise <menu>, lignes 25, 29) ;
Le fichier [res / menu / menu_vue2] définit le menu de la vue n° 2 :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. La gestion du menu dans la classe abstraite [AbstractFragment]
Nous allons factoriser la gestion du menu dans la classe parent [AbstractFragment] des deux vues :
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// données accessibles aux classes filles
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// activité
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// menu
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// constructeur
public AbstractFragment() {
// init
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// mémoire
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// on récupère les # options du menu si ce n'a pas déjà été fait
if (!initDone) {
// on récupère les # options du menu
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// on transfère la liste des options dans un tableau
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// activité
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// mémoire
initDone = true;
}
// on demande au fragment fille de se mettre
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// affichage options de menu -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// update classe fille
protected abstract void updateFragment();
}
- ligne 42 : les logs montrent que la méthode [onCreateOptionsMenu] est appelée à chaque fois que le fragment est affiché. Elle est appelée très tard, notamment après que la méthode [updateFragment] ait été appelée. Cela suggère qu'elle pourrait être utilisée pour mettre à jour le fragment. C'est ce que nous allons faire ici (ligne 63) ;
- ligne 42 : la méthode a deux paramètres :
- [menu] : qui est un menu vide ;
- [inflater] : un outil qui permet de créer le menu à partir de sa description initiale. Nous n'utiliserons pas cette possibilité ici car nous utiliserons une annotation AA qui le fera pour nous ;
- ligne 44 : nous mémorisons le menu. Nous en aurons besoin ultérieurement ;
- lignes 52-53 : nous stockons dans le tableau de la ligne 28 les identifiants de tous les éléments du menu ;
- lignes 55-57 : les logs montrent que lorsque la méthode [onCreateOptionsMenu] est appelée, la méthode [Fragment.getActivity()] rend l'activité associée au fragment ;
- ligne 55 : nous mémorisons l'activité comme instance de la classe Android [Activity] ;
- ligne 56 : nous mémorisons l'activité comme instance de l'interface [IMainActivity] ;
- ligne 57 : nous mémorisons la session ;
- ligne 59 : nous notons que l'initialisation de la classe a été faite pour ne pas avoir à la refaire (ligne 50) ;
- ligne 63 : on demande au fragment fille de se mettre à jour. C'est possible parce que le fragment est à la fois visible, associé à sa vue et à son menu ;
La méthode [getMenuOptions] qui permet d'avoir les identifiants des éléments d'un menu est la suivante :
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// on parcourt tous les items du menu
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// si item n° i est un sous-menu, alors on recommence
if (menuItem.hasSubMenu()) {
// récursivité
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
La méthode [setAllMenuOptions] permet de cacher / montrer l'ensemble des options du menu ;
protected void setAllMenuOptions(boolean isVisible) {
// on met à jour toutes les options du menu
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
La méthode [setMenuOptions] permet de cacher / montrer certaines des options du menu ;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// on met à jour certaines options du menu
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
La classe [MenuItemState] est la suivante :
![]() |
package exemples.android.architecture;
public class MenuItemState {
// identifiant de l'option de menu
private int menuItemId;
// visibilité de l'option
private boolean isVisible;
// constructeurs
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters et setters
...
}
1.21.4. La gestion du menu dans le fragment [Vue1Fragment]
La classe [Vue1Fragment] devient la suivante :
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// on navigue vers la vue 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// on affiche un message
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// on change d'état
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
// on change d'état
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// on change d'état
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// on met à jour le menu
//setMenuOptions(...)
}
}
- ligne 2 : le menu [res / menu / menu_vue1.xml] est associé au fragment ;
- ligne 48 : lorsque la méthode [updateFragment] s'exécute, le menu peut être lui aussi mis à jour pour refléter le nouvel état du fragment ;
- ligne 7 : l'annotation [@OptionsItem(R.id.navigationVue2)] annote la méthode qui doit être exécutée lors d'un clic sur l'option de menu [Navigation / Vue 2] ;
- lignes 19-25 : pour cacher une branche du menu, il suffit de cacher l'option racine de celle-ci ;
- ligne 24 : on montre / cache les options racine [menuNavigation, menuActions] ;
- ligne 40 : pour montrer une option d'une branche de menu, il faut non seulement montrer celle-ci mais également toutes les options qu'on rencontre en remontant de l'option feuille vers la racine du menu ;
1.21.5. La gestion du menu dans le fragment [Vue2Fragment]
On retrouve un code similaire dans le fragment de la vue n° 2 :
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// les champs de la vue
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// on navigue vers la vue 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// on affiche les éléments de la liste qui ont été sélectionnés dans la vue 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// on met à jour le menu
// setMenuOptions(...)
}
}
- ligne 35 : on affiche l'option [Navigation / Vue 1] ;
- lignes 17-20 : lors du clic sur l'option [Navigation / Vue1], on appelle la méthode [navigateToView1] ;
1.21.6. Exécution
Créez un contexte d'exécution pour ce projet et exécutez-le.
1.22. Exemple-21 : refactoring de la classe abstraite [AbstractFragment]
L'exemple précédent nous a montré que lorsque le fragment a un menu, sa méthode [onCreateOptionsMenu] est un bon endroit pour demander au fragment de se mettre à jour :
- elle est appelée exactement une fois lorsque le fragment va s'afficher ;
- lorsqu'elle est appelée, les associations du fragment avec son activité, sa vue et son menu sont faites ;
Pour le montrer, on reprend l'exemple 12 qui a la propriété d'avoir beaucoup de fragments dont on peut modifier l'adjacence. Dans cet exemple, les fragments n'avaient pas de menu. On va leur associer un menu vide.
1.22.1. Création du projet
Nous dupliquons le projet [Exemple-12] dans le projet [Exemple-21] :
![]() | ![]() |
1.22.2. Le menu des fragments
![]() |
Le menu ajouté pour les fragments sera vide :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
</menu>
Ce qu'il faut comprendre ici c'est que l'activité a déjà son propre menu [menu_main] :
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
Lorsqu'une activité a déjà un menu, le menu associé aux fragments s'ajoute à celui de l'activité : on a donc les options de deux menus. Ici, le menu des fragments sera vide. Donc on ne verra que le menu de l'activité.
1.22.3. Les fragments
![]() |
Nous reprenons la classe abstraite [AbstractFragment] de l'exemple précédent (cf paragraphe 1.21.3). Nous associons le menu [menu_fragment] aux deux fragments :
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
Dans les deux fragments [PlaceholderFragment] et [Vue1Fragment], nous nous débarrassons de ce qui fait référence à l'ancienne classe abstraite [AbstractFragment].
1.22.4. Exécution
Exécutez l'application et constatez qu'elle fonctionne. Suivez les logs pour voir quand la méthode [onCreateOptionsMenu] de la classe [AbstractFragment] est exécutée. C'est désormais elle qui appelle la méthode [updateFragment] des fragments filles.
1.23. Exemple-22 : sauvegarde / restauration de l'état de l'activité et des fragments
1.23.1. Le problème
Nous abordons ici le problème de la rotation du périphérique Android (portrait <--> paysage). Pour l'illustrer, nous reprenons l'exemple 21 précédent :

Si nous faisons tourner le périphérique [1], nous obtenons la nouvelle vue suivante :

On voit que :
- en [1], l'onglet [Fragment n° 3] a disparu ;
- en [2], le texte affiché est bien celui du fragment n° 3 mais le compteur de visites est incorrect ;
Lors de cette rotation, les logs sont les suivants :
- ligne 1 : on voit que l'activité est totalement reconstruite ;
- lignes 3-7 : il en est de même pour les cinq fragments gérés par l'activité ;
- ligne 21 : le fragment n° 3 va être affiché. On voit qu'avant incrémentation, le n° de la visite est 0 ;
On peut alors expliquer le résultat obtenu après rotation de la façon suivante :
- la classe [MainActivity] crée au départ une barre d'onglets avec un seul onglet, libellé [Vue 1]. C'est l'onglet qu'on voie ;
- après la rotation du périphérique, le gestionnaire de pages [mViewPager] réaffiche le même fragment, donc ici le fragment n° 3. Il faut se souvenir ici que onglets et fragments sont des notions différentes et ont un cycle de vie différent. La méthode [updateFragment] du fragment n° 3 va s'exécuter :
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texte modifié
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- ligne 7 : le dernier n° de la visite est lu dans la session. Or celle-ci, comme tout le reste, a été reconstruite et le n° de visite a été remis à zéro. Ce qui explique le résultat affiché dans le fragment n° 3 ;
1.23.2. Les méthodes de sauvegarde / restauration de l'activité et des fragments
1.23.2.1. Solution 1 : sauvegarde manuelle
Lors de la rotation du périphérique, deux méthodes de l'activité sont appelées :
// gestion sauvegarde / restauration de l'activité ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// sauvegarde état de l'activité
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// restauration de l'activité
// ...
}
- lignes 2-8 : la méthode [onSaveInstanceState] est appelée par le système lors de la rotation. C'est l'endroit où la sauvegarde de l'activité peut se faire. Si on ne fait rien, rien n'est sauvegardé. La sauvegarde de l'état de l'activité doit se faire dans le paramètre [Bundle outState] passé à la méthode. La classe [Bundle] ressemble à un dictionnaire. Elle possède des méthodes [putString, putInt, putLong, putBoolean, putChar, ...] à deux paramètres : void putT(String key, T value) ;
- lignes 10-16 : la méthode [onCreate] est appelée lors de la création de l'activité. Si l'état de celle-ci a été sauvegardé, cette sauvegarde lui est passée dans le paramètre [Bundle savedInstanceState]. Pour récupérer les valeurs sauvegardées, on dispose de méthodes telles que [getString, getInt, getLong, geBoolean, getChar, ...] à un paramètre : T getT(String key) ;
Les fragments disposent de ces deux mêmes méthodes pour sauvegarder leur état.
Nous allons utiliser ces informations pour sauvegarder et restaurer l'état de l'exemple 21. Pour cela nous dupliquons le projet [Exemple-21] dans [Exemple-22].
1.23.2.2. Solution 2 : sauvegarde automatique
La documentation Android indique que lors de la rotation du périphérique, on peut éviter la destruction d'un fragment en utilisant l'instruction : [Fragment].setRetainInstance(true). Plusieurs articles de [StackOverflow] préconisent d'utiliser cette instruction uniquement pour les fragments sans interface visuelle [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. J'ai testé cette instruction sur deux exemples : Exemple-17 (paragraphe 1.18 - une application à un fragment qui affiche un formulaire) et Exemple-21 (paragraphe 1.22), une application à cinq fragments). Dans les deux cas, cette seule instruction appliquée à tous les fragments de l'application s'est révélée insuffisante pour restaurer correctement la vue affichée lors de la rotation du périphérique. Plutôt que de construire deux modèles, l'un basé sur [setRetainInstance(true)] et un autre basé sur [setRetainInstance(false)] qui est la valeur par défaut, j'ai décidé de suivre les préconisations de [StackOverflow] et de garder la valeur false par défaut de la méthode [setRetainInstance(boolean )]. L'instruction : [Fragment].setRetainInstance(true) n'a jamais été utilisée dans la suite de ce document.
1.23.3. La méthode de sauvegarde / restauration du projet [Exemple-22]
Le projet [Exemple-22] évolue comme suit :
![]() |
On y voit apparaître deux nouvelles classes :
- [PlaceHolderFragmentState] qui mémorisera l'état d'un fragment de type [PlaceHolderFragment] ;
- [Vue1FragmentState] qui mémorisera l'état du fragment de type [Vue1Fragment] ;
Ces classes sont les suivantes :
package exemples.android;
public class Vue1FragmentState {
// état Vue1Fragment
private boolean hasBeenVisited=false;
// getters et setters
...
}
- ligne 5 : le booléen [hasBeenVisited] est vrai si le fragment [Vue1Fragment] a été visité (affiché) au moins une fois. Ce champ a été créé pour l'exemple car le fragment [Vue1Fragment] n'a rien à sauvegarder ;
La classe [PlaceHolderFragmentState] est la suivante :
package exemples.android;
public class PlaceHolderFragmentState {
// état visité ou pas
private boolean hasBeenVisited;
// texte affiché
private String text;
// getters et setters
...
}
- ligne 5 : on retrouve le booléen [hasBeenVisited] ;
- ligne 7 : le texte affiché par le fragment au moment où il doit être sauvegardé. On a vu que ce texte avait été perdu lors de la rotation ;
L'état des fragments va être stocké dans la session et c'est l'activité qui aura la charge de sauvegarder / restaurer cette session. La session évolue de la façon suivante :
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// nombre de fragments visités
private int numVisit;
// n° fragment de type [PlaceholderFragment] affiché dans le second onglet
private int numFragment = -1;
// n° onglet sélectionné
private int selectedTab = 0;
// n° vue courante
private int currentView;
// sauvegardes fragments ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// constructeur
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters et setters
...
}
- ligne 18 : l'état du fragment [Vue1Fragment] ;
- ligne 19 : l'état des fragments de type [PlaceHolderFragment] ;
- lignes 22-27 : dans le constructeur de la session, on initialise les champs des lignes 18 et 19 ;
- lignes 12-15 : deux nouveaux champs apparaissent :
- ligne 13 : le n° du dernier onglet sélectionné ;
- ligne 15 : le n° du dernier fragment affiché ;
L'activité sauvegarde / restaure la session de la façon suivante :
// gestion sauvegarde / restauration de l'activité ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// sauvegarde session
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// récupération session
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
- ligne 8 : on sauvegarde la session sous la forme de sa chaîne jSON ;
- ligne 29 : on restaure la session à partir de sa chaîne jSON ;
Pour gérer la sauvegarde / restauration des fragments, la classe abstraite [AbstractFragment] évolue de la façon suivante :
// gestion sauvegarde / restauration -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// sauvegarde ?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// le fragment va être caché - on le sauvegarde
saveFragment();
saveFragmentDone = true;
}
// mémoire
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// le fragment doit être restauré
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// parent
super.onSaveInstanceState(outState);
// sauvegarde du fragment seulement s'il est visible
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// classes filles
protected abstract void updateFragment();
protected abstract void saveFragment();
- on décide de sauvegarder l'état des fragments dans la session à deux moments :
- lignes 2-14 : lorsque le fragment passe de visible à caché ;
- lignes 29-42 : lorsque le système indique qu'il faut faire une sauvegarde du fragment et que celui-ci est visible (ligne 38) ;
Ce mécanisme évite de faire des sauvegardes plus souvent que nécessaire. En effet, comme on a sauvegardé l'état du fragment i lorsqu'il est passé de visible à caché, lorsque le fragment j est affiché et qu'on fait une rotation, il est inutile de sauvegarder de nouveau le fragment i. S'il n'a pas été réaffiché depuis sa dernière sauvegarde, alors son état n'a pas changé. Seul l'état du fragment j doit être sauvegardé. Cette mécanique a également un autre avantage : il n'y a pas que lors d'une rotation du périphérique qu'on a besoin de sauvegarder l'état d'un fragment. Il y a aussi le cas de la navigation pure entre fragments par exemple dans un système à onglets. On veut alors retrouver un fragment dans l'état où on l'a laissé la dernière fois qu'il était affiché. Cet état peut avoir en partie disparu si ce fragment est sorti à un moment donné de l'adjacence des fragments affichés. Le fragment n'est alors pas reconstruit dans sa totalité mais sa vue associée, elle, l'est. La sauvegarde qu'on a faite lorsque le fragment est devenu caché va servir à retrouver le dernier état de cette vue ;
- lignes 10, 40 : pour éviter de faire deux sauvegardes successives, on utilise le booléen [saveFragmentDone] pour indiquer qu'une sauvegarde a été faite ;
- lignes 9, 39 : on demande au fragment fille de sauvegarder son état. La méthode [saveFragment] est abstraite (ligne 47). C'est donc aux classes filles de l'implémenter ;
- lignes 16-26 : la méthode [onActivityCreated] est utilisée pour positionner le booléen [fragmentHasToBeInitialized] à vrai. En effet, le fragment fille doit savoir qu'elle doit réinitialiser entièrement l'état du fragment à partir d'un état qu'elle trouvera dans la session ;
Toujours dans la classe [AbstractFragment], la méthode [onCreateOptionsMenu] évolue comme suit :
// mise à jour du fragment
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// mémoire
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// on demande au fragment fille de se mettre à jour
updateFragment();
// sauvegarde à faire
saveFragmentDone = false;
}
- ligne 14 : on a vu que le booléen [saveFragmentDone] était passé à vrai lorsqu'une sauvegarde avait été faite. Il faut à un moment donné qu'il revienne à faux. Lorsque la méthode [updateFragment] (ligne 12) du fragment fille est exécuté, celui-ci va devenir visible. Or c'est lorsqu'il est visible qu'un fragment doit être sauvegardé, au moment particulier où il passera de l'état visible à l'état caché. On positionne alors le booléen [saveFragmentDone] à false pour que la sauvegarde puisse avoir lieu ;
1.23.4. Sauvegarde du fragment [Vue1Fragment]
La sauvegarde des fragments se fait dans la méthode [saveFragment] appelée par la classe mère [AbstractFragment] :
// sauvegarde état fragment
@Override
public void saveFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// sauvegarde en session de l'état du fragment
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- lignes 9-11 : sauvegarde de l'état du fragment en session. Lorsque la méthode [saveFragment] est appelée, le fragment est visible. Il faut donc mettre le booléen [hasBeenVisited] à vrai (ligne 10) ;
1.23.5. Sauvegarde du fragment [PlaceHolderFragment]
La sauvegarde des fragments se fait dans la méthode [saveFragment] appelée par la classe mère [AbstractFragment] :
@Override
public void saveFragment() {
// on sauvegarde l'état du fragment en session
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- lignes 4-7 : sauvegarde en session de l'état du fragment ;
- ligne 5 : le texte actuellement affiché par le [TextView] textViewInfo est sauvegardé ;
- ligne 6 : le booléen [hasBeenVisited] du fragment est passé à vrai ;
- ligne 7 : l'état du fragment est mis en session dans le tableau [placeHolderFragmentStates]. Le n° de l'élément à initialiser est le n° de section du fragment moins un ;
1.23.6. Restauration du fragment [Vue1Fragment]
La restauration des fragments se fait dans la méthode [updateFragment] :
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// restauration ?
if (fragmentHasToBeInitialized) {
// restauration état
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// navigation ?
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// on affiche le n° de la visite
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// changement n° vue courante
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- lignes 8-12 : restauration de l'état du fragment. Le booléen [fragmentHasToBeInitialized] a été initialisé par la classe parent [AbstractFragment]. Lorsqu'il est à vrai, le fragment vient d'être reconstruit et il faut le réinitialiser. C'est ici que ça se passe. Dans cet exemple précis, il n'y a rien à faire. Nous avons simplement montré qu'on pouvait retrouver la valeur du booléen [hasBeenVisited] dans l'état sauvegardé du fragment (ligne 10) ;
- ligne 11 : il ne faut pas oublier de remettre le [fragmentHasToBeInitialized] à faux, pour que lorsqu'on reviendra ultérieurement sur ce fragment sans qu'il y ait eu de rotation du périphérique, on ne refasse pas une initialisation inutile du fragment ;
- lignes 18-26 : incrément du compteur de visites. Ici, il y a une difficulté : lorsqu'on fait une restauration du fragment, on ne veut pas incrémenter ce compteur. Il nous faut distinguer ici entre :
- une simple navigation qui ramène l'utilisateur sur l'onglet [Vue 1] ;
- une restauration lorsque l'utilisateur fait tourner son périphérique lorsque l'onglet [Vue 1] est affiché ;
On distingue ces deux cas, grâce au n° de vue stocké dans la session. Ce n° est celui de la dernière vue affichée (ligne 28).
- ligne 18 : il y a navigation et non pas restauration si le n° de la dernière vue est différent de celui de la vue courante ;
- lignes 21-25 : incrémentation du compteur de visites et son affichage ;
1.23.7. Restauration du fragment [PlaceHolderFragment]
La restauration des fragments se fait dans la méthode [updateFragment] :
// data
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// de quel fragment s'agit-il ?
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// le fragment doit-il être initialisé ?
if (fragmentHasToBeInitialized) {
// texte initial
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// navigation ?
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// incrément n° de visite
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texte modifié
newText = String.format("%s, visite %s", text, numVisit);
} else {
// on a affaire à une restauration
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// affichage texte
textViewInfo.setText(newText);
// vue courante
session.setCurrentView(numView);
}
- lignes 15-16 : on détermine le n° de la vue que l'on est en train de mettre à jour ;
- lignes 18-22 : cas où le fragment est dans un cycle sauvegarde / restauration après un changement d'orientation du périphérique. Il faut ici le restaurer. Il s'agit généralement de restaurer certains champs du fragment ;
- ligne 20 : le champ [text] de la ligne 2 doit contenir le texte initial affiché par le fragment : [Hello world from section i]. Il doit ici être régénéré ;
- ligne 21 : on note que l'initialisation du fragment a été faite ;
- lignes 24-36 : comme précédemment pour le fragment [Vue1Fragment], l'incrémentation du compteur de visites ne doit pas se faire lors d'une restauration. Comme précédemment, il nous faut distinguer entre navigation et restauration ;
- lignes 32-36 : cas de la restauration ;
- ligne 34 : l'état du fragment avant la rotation du périphérique est récupéré dans la session ;
- ligne 35 : on y récupère le texte qui était alors affiché ;
- ligne 38 : ce texte est de nouveau affiché ;
- ligne 40 : on note en session, le n° de la nouvelle vue affichée ;
1.23.8. Gestion des onglets
Les paragraphes précédents n'ont pas abordé la gestion des onglets. Or nous avons vu un problème dans l'exemple 21 lors de la rotation du périphérique : seul le 1er onglet [Vue 1] était conservé. Le second onglet lui était perdu.
Nous résolvons ce problème dans la classe [MainActivity] de la façon suivante :
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// barre d'outils
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// 1er onglet
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2ième onglet ?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
// quel onglet sélectionner ?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- lignes 14-16 : création du 1er onglet ;
- lignes 18-23 : création du 2ième onglet. Pour savoir si on doit le créer, on regarde dans la session le n° du fragment affiché dans l'onglet 2. Si ce n° est différent de -1, sa valeur initiale, alors le second onglet est créé. A ce stade, on a deux onglets dont par défaut le 1er est sélectionné ;
- ligne 26 : on va chercher dans la session le n° de l'onglet qui était sélectionné avant la sauvegarde / restauration et on sélectionne celui-ci de nouveau. Si le champ [selectedTab] n'a pas encore été initialisé par le code, c'est sa valeur initiale 0 qui est alors utilisée ;

















































































































































































































































































































































