Skip to content

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 :

  1. Android pour les développeurs J2EE ;
  1. Introduction à la programmation de tablettes Android par l'exemple ;
  2. Commander un Arduino avec une tablette Android ;
  3. 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 :

Exemple
Nature
1
Importation d'un projet Android existant
2
Un projet Android basique
3
Un projet [Android Annotations] basique
4
Vues et événements
5
Navigation entre vues
6
Navigation par onglets
7
Utilisation de la bibliothèque [Android Annotations] avec Gradle
8 à 12
Gestion des fragments d'une application Android
13
Navigation entre vues revisitée
14
Architecture à deux couches
15
Architecture client / serveur
16
Gérer l'asynchronisme avec RxJava
17, 17B
Composants de saisie de données
18
Utilisation d'un patron de vues
19
Le composant ListView
20
Utiliser un menu
21
Utilisation d'une classe parent pour les fragments
22, 22B
Sauvegarde et restauration de l'état de l'activité et des fragments
23
Client météo
Chap 2
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.
Chap 3
Gestion des rendez-vous d'un cabinet de médecins
Chap 4
Exercice d'application - Gestion d'une paie basique
Chap 5
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] :

 

Image

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

 

Image

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] ;

Image

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

Image

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 :

Id
Type
Rôle
1
textViewTitreVue1
TextView
Titre de la vue
2
textView1
TextView
une question
3
editTextNom
EditText
saisie d'un nom
4
buttonValider
Button
pour valider la saisie
5
buttonVue2
Button
pour passer à la vue n° 2

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 :

Image

1.5.3. Gestion des événements

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

Image

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 :

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

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.

Image

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

Image

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

  1. 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 ;
  2. 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] ;
  3. 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 ;
  4. 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 :

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 1

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 :

1
2
3
05-28 09:38:09.429 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/exemples.android D/MainActivity: afterViews

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 :

1
2
3
05-28 09:39:26.607 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/exemples.android D/SecondActivity: afterViews

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 :

1
2
3
05-28 09:39:56.393 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/exemples.android D/MainActivity: afterViews

La classe [MainActivity] est donc de nouveau instanciée. Lorsqu'on clique sur le bouton [Vue n° 2], les nouveaux logs sont les suivants :

1
2
3
05-28 09:40:59.099 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/exemples.android D/SecondActivity: afterViews

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

Image

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 :

Image

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">&lt;</span><span style="color:#3f7f7f">resources</span><span style="color:#008080">&gt;</span></span>
<span class="odt-code-line"><span style="color:#000000">  </span><span style="color:#008080">&lt;</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">&quot;app_name&quot;</span><span style="color:#008080">&gt;</span><span style="color:#000000">Exemple-06</span><span style="color:#008080">&lt;/</span><span style="color:#3f7f7f">string</span><span style="color:#008080">&gt;</span></span>
<span class="odt-code-line"><span style="color:#000000">  </span><span style="color:#008080">&lt;</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">&quot;action_settings&quot;</span><span style="color:#008080">&gt;</span><span style="color:#000000">Settings</span><span style="color:#008080">&lt;/</span><span style="color:#3f7f7f">string</span><span style="color:#008080">&gt;</span></span>
<span class="odt-code-line"><span style="color:#000000">  </span><span style="color:#008080">&lt;</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">&quot;section_format&quot;</span><span style="color:#008080">&gt;</span><span style="color:#000000">Hello World from section: %1$d</span><span style="color:#008080">&lt;/</span><span style="color:#3f7f7f">string</span><span style="color:#008080">&gt;</span></span>
<span class="odt-code-line"><span style="color:#008080">&lt;/</span><span style="color:#3f7f7f">resources</span><span style="color:#008080">&gt;</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 :

Image

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 :

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • 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 :

1
2
3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • 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 :

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

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 :

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • 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 :

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • 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 :

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

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 :

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

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 :

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours

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 :

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

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 :

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

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 :

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

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 :

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • 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 :

// offset des fragments
    [ViewPager].setOffscreenPageLimit(n);

Avec l'instruction ci-dessus,

  1. 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] ;
  2. 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 :

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • 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 :

06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Passons de l'onglet 2 à l'onglet 3 :

06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Puis de l'onglet 3 à l'onglet 1 :

06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

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 :

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • 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 ;

Image

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

navigateToView(...);

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 :

Image

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 :

Id
Type
Rôle
1
edtNbAleas
EditText
nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]
2
edtA
EditText
valeur de a
2
edtB
EditText
valeur de b
4
btnExécuter
Button
lance la génération des nombres
5
ListView
lstReponses
liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

Son code XML est le suivant :


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


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] est l'activité qui affiche le [ListView] ;
  • [resource] est l'entier identifiant la vue utilisée pour afficher un élément du [ListView]. Cette vue peut avoir une complexité quelconque. C'est le développeur qui la construit en fonction de ses besoins ;
  • [textViewResourceId] est l'entier identifiant un composant [TextView] dans la vue [resource]. La chaîne affichée le sera par ce composant ;
  • [objects] : la liste d'objets affichés par le [ListView]. La méthode [toString] des objets est utilisée pour afficher l'objet dans le [TextView] identifié par [textViewResourceId] dans la vue identifiée par [resource].

Le travail du développeur est de créer la vue [resource] qui va afficher chaque élément du [ListView]. Pour le cas simple où on ne désire afficher qu'une simple chaîne de caractères comme ici, Android fournit la vue identifiée par [android.R.layout.simple_list_item_1]. Celle-ci contient un composant [TextView] identifié par [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 :

  1. 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) ;
  1. 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] ;
  1. 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 :

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.1.RELEASE)

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • 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) :

Image

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

Image

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

  1. une couche [Présentation] (vue+activité) analogue à celle que nous avons étudiée dans l'exemple [Exemple-14] ;
  2. 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 :

  1. 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 ;
  2. 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 ;

Image

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

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

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 :

Image

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 :

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • 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 :

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • 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 :

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

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 :

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

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 :

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • 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 :

  1. RxAndroid peut s'utiliser dans une application Android n'utilisant pas AA ;
  2. 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 ;
  3. 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 :

Image

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 :

  • ligne 2 : un layout [ScrollView] vertical. Il permet de
  • présenter un formulaire plus grand que l'écran de la
  • tablette. On obtient la totalité du formulaire par
  • défilement ;
 
  • lignes 125-132 : une case à cocher
  • lignes 134-159 : un groupe de trois boutons radio
  • lignes 161-166 : une barre de recherche
  • lignes 16-176 : une boîte de saisie
  • lignes 178-186 : un switch oui / non
  • lignes 188-195 : une boîte de saisie de l'heure
  • lignes 197-207 : une boîte de saisie multi-lignes
  • lignes 209-215 : une liste déroulante
  • lignes 217-225 : une boîte de saisie d'une date
  • tous les autres composants sont des [TextView] qui affichent des textes.
 

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 :

Image

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 :

  

Image

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 :

Image

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 :

Image

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

Image

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 :

07-13 04:08:27.188 1677-1677/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • 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 ;