Cours Complet sur Android Studio PDF
Cours Complet sur Android Studio PDF
d’applications mobile
1
Associer une activité à un intent ................................................................................................ 44
Émettre un intent implicite ......................................................................................................... 47
Chapitre 4 : Gestion des activités d’une application ..................................................................... 48
L’application de démo .................................................................................................................. 48
Structure de l’application ............................................................................................................. 49
La classe OccupationsSingleton................................................................................................ 49
L’activité principale et RecyclerView ......................................................................................... 49
La classe OccupationAdapter .................................................................................................... 50
La classe MainActivity ................................................................................................................. 52
Détail d’une occupation et pile des activités .............................................................................. 53
Ouverture d’une activité (intent explicite) ................................................................................... 53
Utilisation d’un intent explicite pour ouvrir une activité ............................................................ 54
La classe OccupationActivity ....................................................................................................... 54
Stratégie de gestion de la pile des activités ................................................................................ 55
Terminer une activité ..................................................................................................................... 57
Appel à la méthode finish depuis une activité ............................................................................ 57
Chapitre 5 : Gestion des messages broadcast ............................................................................... 58
Le BroadcastReceiver ................................................................................................................... 58
Suivre le branchement sur secteur ............................................................................................ 58
Suivre l’état de la connexion Wifi ............................................................................................... 59
Recevoir des broadcasts depuis une activité ............................................................................. 60
Émettre un message broadcast ................................................................................................... 62
Chapitre 6 : Accès Web ..................................................................................................................... 63
Permission pour l’accès Internet ................................................................................................. 63
Utilisation de la classe URL .......................................................................................................... 63
Utilisation de la bibliothèque Volley ............................................................................................ 67
Ajout de la dépendance .............................................................................................................. 67
Émission d’une requête .............................................................................................................. 67
Émission d’une requête pour une réponse JSON ..................................................................... 69
Chapitre 7 : Les services Android ..................................................................................................... 74
Service en tâche de fond............................................................................................................... 74
Implémentation et déclaration du service ................................................................................ 74
Le cycle de vie .............................................................................................................................. 76
Exécution en tâche de fond ........................................................................................................ 78
Un service s’exécutant en tâche de fond .................................................................................. 78
Interaction avec l’utilisateur ....................................................................................................... 80
Lancement du service ................................................................................................................. 80
2
Service lié ....................................................................................................................................... 81
Déclaration du service ................................................................................................................ 81
Le cycle de vie .............................................................................................................................. 81
Implémentation de la méthode onBind ..................................................................................... 82
Lier le service à une activité ........................................................................................................ 83
3
Chapitre 1 : Une première application
Dans ce chapitre, nous allons créer un premier projet avec Android Studio pour
comprendre la structure d’une application Android.
Création d’une application simple
Lancez Android Studio et sur la fenêtre d’accueil, choisissez Start a new Android Studio
project .
4
Le package correspond à l’identifiant technique de votre application. Si vous souhaitez
déployer plus tard votre application sur Android Store, c’est le package qui permet
d’identifier de manière unique votre application et l’organisation à qui elle appartient.
5
Les projets Android sont composés de modules. Les projets les plus simples n’ont besoin
que d’un seul module qui contient l’ensemble des fichiers sources. Par défaut, un projet
Android contient un seul module nommé app . Dans le répertoire de votre projet, vous
pouvez trouver un répertoire app qui contient l’ensemble des fichiers pour ce module.
(c’est-à-dire les tests qui doivent être exécutés directement sur un périphérique
Android).
• libs : ce répertoire contient éventuellement les bibliothèques nécessaires au
module
• build : ce répertoire contient les fichiers de travail créés par Gradle.
6
adaptée. Il ne vous affiche donc pas l’organisation réelle des fichiers sur votre disque dur.
Vous pouvez choisir le mode Project pour voir l’organisation réelle mais le
mode Android est généralement plus lisible et plus adapté pour réaliser les
développements.
• java : qui présente toutes les sources Java en séparant les packages de
• Gradle Scripts : ce répertoire regroupe tous les fichiers Gradle de l’application et des
modules.
Lancement de l’application
Depuis Android Studio, vous pouvez lancer une application sur votre téléphone Android.
Il vous suffit de brancher votre appareil en USB sur votre station de travail et de choisir le
menu Run > Run “app” . Android Studio utilise l’outils adb (Android Debug Bridge) pour
installer l’application créée sur votre appareil et l’exécuter. Une fois installée,
l’application est accessible depuis le menu des applications Android sur votre appareil.
Pour pouvoir utiliser le mode debug depuis Android Studio, vous devez activer le
mode developer sur votre appareil. Pour plus d’information, consultez le guide utilisateur
:
7
L’émulateur Android
Si vous ne disposez pas d’un appareil Android ou si vous souhaitez tester votre
application dans des conditions particulières, vous pouvez utiliser l’émulateur Android.
Il vous permet de choisir le type d’appareil que vous souhaitez émuler.
Pour créer un émulateur, cliquez sur le menu Tools > AVD Manager (AVD pour Android
Virtual Device).
Cliquez sur le bouton Create Virtual Device… pour ajouter, depuis la liste fournie, le modèle
d’appareil à émuler.
À partir de la fenêtre du AVD Manager, vous pouvez directement lancer l’émulateur.
Lorsque vous lancez votre application depuis Android Studio, l’émulateur apparaît dans
la liste des périphériques disponibles.
Notion d’activité
Le modèle de projet choisi génère le code pour une application avec une activité. Une
activité est un composant d’une application Android qui représente un écran avec lequel
l’utilisateur va pouvoir interagir. Un composant Android est représenté par une classe
Java. Cependant, un composant n’est pas instancié directement dans le code de
l’application. C’est lors de l’exécution que le système prend en charge la création et le
cycle de vie d’un objet de cette classe. Dans le cas d’une activité, un objet correspondant
est instancié par le système quand il est nécessaire d’afficher l’écran à l’utilisateur (par
exemple au lancement de l’application).
Pour notre application, le générateur de projet a créé une classe MainActivity dans le
répertoire java dont voici le code :
1 package [Link];
2 import [Link];
3 import [Link];
4 public class MainActivity extends AppCompatActivity {
7
8 @Override
9 protected void onCreate(Bundle savedInstanceState) {
10 [Link](savedInstanceState);
11 setContentView([Link].activity_main);
}
8
}
La classe MainActivity hérite de AppCompatActivity. Cette dernière fournit du code de base
pour la gestion d’une activité. Dans notre exemple, nous surchargeons la
méthode onCreate pour changer le comportement de l’activité au moment de sa création.
Notion de ressources
En plus du code Java, une application Android peut contenir un ensemble d’informations
que l’on désigne de manière générale comme des ressources. Le type d’une ressource
peut être très divers. Il peut s’agir d’un nombre, d’une chaîne de caractères, d’une image,
d’un fichier quelconque, de la description du rendu d’une activité…
Les ressources sont stockées dans des sous-répertoires dans [module]/src/main/res . Dans
le module par défaut app , les ressources sont donc stockées dans app/src/main/res . Dans
la vue Android de l’éditeur, le répertoire res est directement affiché sous le module. Par
défaut, ce répertoire contient les sous-répertoires :
• drawable : ce répertoire contient les images (PNG, JPEG…) ou des fichiers XML
activité.
• mipmap : ce répertoire contient les icônes de l’application. Comme une application
Android doit pouvoir être installée sur différents périphériques, il est nécessaire de
fournir plusieurs résolutions de la même icône. La technique du mipmap consiste
précisément à fournir plusieurs résolutions d’une même image afin d’éviter la
pixellisation à l’affichage en permettant au système de choisir la résolution
adéquate.
• values : ce répertoire contient les variables que l’on ne souhaite pas écrire en dur
9
L’éditeur de layout
Accéder à un identifiant de ressource depuis le code Java
Toutes les ressources sont accessibles à partir de leur identifiant (une constante
numérique créée automatiquement par le compilateur). Depuis votre code Java,
l’identifiant est fourni par la classe R . La classe R est une classe dont le code source est
automatiquement écrit par Android Studio et qui est déclarée dans le package par défaut
de votre application. Pour accéder à l’identifiant d’un layout, il faut écrire
[Link].[nom du layout]
Le nom du layout correspond au nom du fichier XML en omettant l’extension.
De la même manière, vous pouvez accéder aux identifiants des fichiers dans les
différents sous-répertoires de ressources. Il existe une particularité pour le
répertoire values . Ce dernier contient des fichiers XML qui permettent de déclarer des
valeurs comme des entiers, des valeurs booléennes, des chaînes de caractères, des
couleurs…
Si vous éditez le fichier app/res/values/[Link] , vous trouverez :
<resources>
<string name="app_name">Première Application</string>
</resources>
Ce fichier déclare une chaîne de caractères nommée app_name est qui correspond au
nom de l’application. Pour accéder à l’identifiant de cette constante depuis le code
source Java, on utilise la classe R de la manière suivante :
int appNameId = [Link].app_name;
Accéder à une ressource depuis une autre ressource
Il est très souvent utile de pouvoir accéder à une ressource depuis une autre ressource.
C’est notamment le cas dans les fichiers de layout. Si l’on souhaite placer un composant
graphique avec un libellé, la valeur du libellé peut être externalisé dans le
fichier app/res/values/[Link] et avoir lui-même un ID. Dans ce cas, on utilise la notation
suivante : @[type]/id
Par exemple, pour accéder au nom de l’application déclaré dans le
fichier app/res/values/[Link] , on écrira : @string/app_name
10
On peut donc très facilement ajouter du texte dans le layout d’une activité pour afficher
le nom de l’application :
<TextView
android:text="@string/app_name"
/>
N’importe quelle valeur peut être remplacée par un identifiant de ressource à condition
que le type de la ressource soit compatible avec le type de la valeur.
Identifier une ressource dans un layout
Lorsqu’on définit un layout dans un fichier XML, il est utile de pouvoir associer un
composant graphique à un identifiant. Nous verrons que nous pouvons alors récupérer
en Java une référence sur ce composant à partir de son identifiant.
La déclaration d’un identifiant dans un layout XML se fait avec l’attribut android:id et le
format de l’identifiant est :
@+id/[ID]
Par exemple, pour identifier un TextView dans un fichier XML de layout :
<TextView
android:id="@+id/monTexte"
/>
Le fichier [Link]
Chaque module de votre projet fournit un fichier manifeste nommé [Link] . Il
se trouve dans le répertoire src/main de votre module. Android Studio l’affiche dans le
répertoire manifests . Ce fichier est lu au lancement de l’application par le système pour
identifier le comportement de votre application. Nous verrons au cours des chapitres
suivants que ce fichier contient la déclaration de tous les composants de votre
application (activités, broadcast receivers, services) mais aussi la déclaration des
autorisations à demander à l’utilisateur à l’installation, la version d’Android requise…
Pour notre application simple, le fichier [Link] contient :
Le fichier [Link]
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="[Link]
3 package="[Link]">
4
5 <application
6 android:allowBackup="true"
7 android:icon="@mipmap/ic_launcher"
8 android:label="@string/app_name"
9 android:roundIcon="@mipmap/ic_launcher_round"
10 android:supportsRtl="true"
11 android:theme="@style/AppTheme">
12 <activity android:name=".MainActivity">
13 <intent-filter>
14 <action android:name="[Link]" />
15 <category android:name="[Link]" />
16 </intent-filter>
17 </activity>
11
18 </application>
19 </manifest>
20
Parmi les nombreuses informations fournies, on peut regarder plus précisément :
• ligne 3 : l’attribut package fournit le package identifiant l’application. C’est
notamment dans ce package que la classe R sera créée.
• ligne 5 : l’élément application marque le début de la déclaration des informations sur
notre application.
• ligne 7 : l’attribut android:icon déclare l’icône de l’application qui pointe vers la
ressource @mipmap/ic_launcher .
• ligne 8 : l’attribut android:label fournit le nom de l’application à partir de la
ressource @string/app_name (déclarée dans app/res/values/[Link] )
• ligne 12 : l’élément activity permet de déclarer une activité en fournissant sa classe
d’implémentation grâce à l’attribut android:name . Remarquez que dans l’exemple, la
classe d’implémentation de l’activité est .MainActivity . Le point au début signale que la
classe doit être cherchée dans le package de l’application. Donc l’activité est fournie
par la classe : [Link]
• les lignes 13 à 15 indiquent que cette activité est l’activité principale. Autrement dit,
lorsque l’application est lancée par l’utilisateur, c’est cette activité qui doit être
affichée. Nous reviendrons plus tard sur les notions d’intent et de filtre d’intent qui
sont très importantes dans le développement Android pour l’échange d’information
entre composants.
12
Chapitre 2 : Conception d’interfaces
graphiques
La plupart des applications Android impliquent une interaction avec l’utilisateur. Nous
avons introduit au chapitre précédent la notion d’activité qui correspond à un écran.
Dans ce chapitre nous allons voir plus en détail comment créer des activités riches
et responsives.
View et Layout
Une activité est composée de views. Il s’agit des composants graphiques qui peuvent
être affichés à l’écran (on parle plus généralement en programmation
de widgets graphiques). Par exemple, une zone de texte ou un bouton sont
des views représentés en Java respectivement par les classes TextView et Button.
Certaines vues permettent de grouper d’autres vues ensembles (ViewGroup) afin de
créer une hiérarchie structurée des composants graphiques à afficher.
13
Layout XML
Nous avons vu au chapitre précédent qu’il est possible de créer un fichier XML pour
décrire le layout d’une activité (le terme layout est ici à prendre au sens général d’un
arrangement de composants graphiques sur l’écran). Au démarrage de l’activité, on
associe ce layout grâce à la méthode setContentView :
@Override
protected void onCreate(Bundle savedInstanceState) {
[Link](savedInstanceState);
setContentView([Link].activity_main);
}
Avec Android Studio, vous avez la possibilité d’utiliser l’outil de conception graphique ou
d’éditer et de modifier directement le fichier XML.
Layout linéaire et positionnement
Pour illustrer les bases du layout, nous allons utiliser le fichier suivant :
Layout linéaire vertical
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bonjour" />
</[Link]>
Ce fichier commence par utiliser un LinearLayoutCompat. Ce dernier permet d’organiser
les vues qu’il contient horizontalement ou verticalement.
L’attribut android:orientation indique pour cet exemple que le placement sera vertical.
14
Rendu du layout linéaire vertical
Layout linéaire horizontal
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bonjour" />
</[Link]>
15
match_parent
Le composant doit, autant que possible, adapter sa taille à celle du composant
dans lequel il se trouve sans tenir compte de son contenu.
wrap_content
Le composant doit s’assurer que sa taille d’affichage est suffisante pour afficher
correctement son contenu (par exemple le texte pour un TextView).
fill_parent
Le composant doit simplement recouvrir son composant parent.
Si nous modifions notre fichier layout :
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bonjour" />
</[Link]>
Nous indiquons maintenant que le premier TextView a une hauteur match_parent .
16
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bonjour" />
</[Link]>
De même, vous pouvez définir un padding grâce à l’attribut android:padding (et toutes ses
variantes
: android:paddingBottom , android:paddingTop android:paddingHorizontal , android:paddingLeft , android:pa
17
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="50dp"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp"
android:text="Bonjour" />
</[Link]>
largeur et/ou une hauteur à partir de wrap_content , la vue aura simplement sa largeur
et/ou sa hauteur modifiées pour tenir compte du padding. Le padding n’a dont pas
d’impact direct sur le layout.
layout_gravity et gravity
Les attributs android:layout_gravity et android:gravity permettent de définir comment le
contenu d’une vue doit être attiré vers tel ou tel bord ou au contraire être
centré. android:gravity n’agit que sur le contenu de la vue indépendamment du reste
du layout tandis que android:layout_gravity agit sur le positionnement de la vue en fonction
du layout.
<?xml version="1.0" encoding="utf-8"?>
<[Link]
18
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Hello"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Bonjour" />
</[Link]>
Utilisation du layout_gravity
layout_weight
Vous pouvez définir un poids pour chaque vue dans un layout grâce à
l’attribut android:layout_weight . S’il s’agit d’un layout vertical, alors la hauteur de chaque vue
sera proportionnelle à son poids et, respectivement, s’il s’agit d’un layout horizontal,
alors la largeur de chaque vue sera proportionnelle à son poids. Si la valeur de
l’attribut android:layout_weight est inférieur à 1, alors elle est interprétée comme un
pourcentage.
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
19
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_dark"
android:layout_weight=".2"
android:text="Hello"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:layout_weight=".3"
android:text="Bonjour" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_purple"
android:layout_weight=".5"
android:text="à tous" />
</[Link]>
Utilisation du layout_weight
Dans l’exemple ci-dessus, nous déclarons trois TextView avec des poids respectifs de
20%, 30% et 50%. Pour que le résultat soit plus facile à comprendre, nous utilisons
également l’attribut android:background pour attribuer une couleur prédéfinie comme fond
à chaque TextView.
Layout par contraintes
Même s’il est possible d’utiliser une combinaison de LinearLayoutCompat pour aligner
horizontalement et verticalement les vues, cela finit par créer une arborescence
complexe de layouts. Comme alternative, vous pouvez également utiliser
un ConstraintLayout. Ce dernier permet de créer plus librement des agencements
20
complexes avec un unique layout au prix, néanmoins, d’une plus grande difficulté de
prise en main.
Le principe du ConstraintLayout est de positionner les vues les unes par rapport aux
autres. Une vue doit au minimum posséder une contrainte pour sa position verticale et
une pour sa position horizontale. Par exemple, vous pouvez contraindre un bouton à se
positionner à droite d’un autre bouton et à la même hauteur.
Ci-dessous un exemple de layout contraint avec le rendu :
1 <?xml version="1.0" encoding="utf-8"?>
2
3 <[Link]
4 xmlns:android="[Link]
5 xmlns:app="[Link]
6 android:layout_width="match_parent"
7 android:layout_height="match_parent" >
8
9 <TextView
10 android:id="@+id/textViewHello"
11 android:layout_width="wrap_content"
12 android:layout_height="wrap_content"
13 android:layout_marginLeft="5dp"
14 android:layout_marginTop="50dp"
15 android:text="Hello"
16 app:layout_constraintTop_toTopOf="parent"
17 app:layout_constraintLeft_toLeftOf="parent" />
18
19 <TextView
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 android:layout_marginLeft="5dp"
23 android:text="world"
24 app:layout_constraintLeft_toRightOf="@+id/textViewHello"
25 app:layout_constraintBaseline_toBaselineOf="@+id/textViewHello" />
26
27 <TextView
28 android:layout_width="wrap_content"
29 android:layout_height="wrap_content"
30 android:layout_marginBottom="50dp"
31 android:text="Bas de page"
32 app:layout_constraintBottom_toBottomOf="parent"
33 app:layout_constraintEnd_toEndOf="parent"
34 app:layout_constraintStart_toStartOf="parent" />
35
36 </[Link]>
21
Le rendu du layout contraint
Pour le layout précédent, le premier TextView doit avoir le haut au même niveau que le
composant parent (c’est-à-dire le layout lui-même) et la bordure gauche doit également
être au même niveau que le parent. On précise également une marge en haut et à gauche
pour décoller le TextView.
Le second TextView a sa bordure gauche placée à droite du premier TextView. Notez que
le premier TextView est référencé par un id ( @+id/textViewHello ) et la ligne de texte est au
même niveau (à la même hauteur) que la ligne de texte du premier TextView. On applique
à ce second TextView une marge à gauche. Donc le layout doit positionner le
second TextView exactement à droite du premier avec un espacement de 5dp.
Enfin le troisième TextView doit avoir sa bordure du bas au même niveau que le bas du
composant parent (c’est-à-dire le layout lui-même). De même, le début et la fin
du TextView sont identiques au composant parent (c’est-à-dire le layout lui-même).
Comme cela n’est pas possible car le taille du TextView est limité par son contenu, cela
va avoir pour conséquence de centrer ce TextView. Enfin une marge du bas de 50dp est
appliquée pour remonter légèrement vers le haut le composant.
22
Ajouter une image
La vue ImageView permet d’insérer une image dans une activité. Grâce à
l’attribut android:src , vous spécifiez la source de l’image (par exemple un fichier PNG). La
source correspond à un identifiant de ressource de la forme @drawable/ suivi du nom du
fichier image sans l’extension.
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/mon_image"/>
Une image PNG ou JPG est une image rastérisée (ou image matricielle). L’image est
définie par une grille ou chaque point à une couleur. L’inconvénient de ce type d’images
est qu’il s’adapte mal à des résolutions d’écran différentes.
Android supporte la possibilité de fournir plusieurs fois la même ressource en fonction
du contexte d’exécution de l’application (par exemple de la résolution de l’écran). Pour
une image, une application Android accepte plusieurs résolutions :
• ldpi (low dots per inch) correspond à une image en 120 dpi.
• mdpi (medium dots per inch) correspond à une image en 160 dpi.
• hdpi (high dots per inch) correspond à une image en 240 dpi.
• xhdpi (extra high dots per inch) correspond à une image en 320 dpi.
• xxhdpi (extra extra high dots per inch) correspond à une image en 480 dpi.
• xxxhdpi (extra extra extra high dots per inch) correspond à une image en 640 dpi.
Pour mémoire, vous pouvez utiliser un facteur multiplicateur de zoom pour connaître les
dimensions nécessaires d’une image en prenant comme référence le mdpi. Chacune
des images doit être placée dans un répertoire ressource comme indiqué ci-dessous.
Chaque fichier image doit porter le même nom pour indiquer qu’il s’agit de la même
ressource avec une résolution différente.
Densité Facteur de zoom Répertoire
mdpi 1 res/drawable-mdpi
xhdpi 2 res/drawable-xhdpi
xxhdpi 3 res/drawable-xxhdpi
xxxhdpi 4 res/drawable-xxxhdpi
Vous devez créer ces répertoires dans votre projet et placer chaque image dans le bon
répertoire.
23
Exemple de formulaire
Une activité assez courante consiste à présenter un formulaire à l’utilisateur pour qu’il
puisse saisir un ensemble d’informations. Ci-dessous, un exemple de layout avec son
rendu :
Un layout pour un formulaire
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent">
<[Link]
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/prenom"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="Prénom"/>
<EditText
android:id="@+id/nom"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="Nom" />
<EditText
android:id="@+id/email"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:hint="Email"/>
<EditText
android:id="@+id/naissance"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="date"
android:hint="Date de naissance JJ/MM/AAAA"/>
<EditText
android:id="@+id/description"
android:layout_margin="5sp"
24
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:hint="Description"
android:gravity="top"
android:height="200dp"/>
<EditText
android:id="@+id/commentaire"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:hint="Commentaire"
android:gravity="top"
android:height="200dp"/>
<CheckBox
android:id="@+id/acceptation_conditions"
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Accepter les conditions d'utilisation" />
<[Link]
android:layout_margin="5sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="right">
<Button
android:id="@+id/bouton_annuler"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="handleAnnuler"
android:text="Annuler"/>
<Button
android:id="@+id/bouton_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="handleOk"
android:text="Ok" />
</[Link]>
</[Link]>
</ScrollView>
25
Le rendu du formulaire
Notez l’utilisation de la ScrollView à la racine du layout pour permettre à l’utilisateur de
faire défiler l’écran pour parcourir le formulaire. Ce layout utilise ensuite
un LinearLayoutCompat vertical pour afficher chaque élément du formulaire. En dernier,
il utilise un autre LinearLayoutCompat horizontal pour afficher les boutons côte-à-côte.
Le formulaire utilise des vues EditText en précisant, avec l’attribut android:inputType , le type
du champ pour permettre au système de proposer un type de saisie adapté au champ.
L’utilisation de l’attribut android:hint permet de préciser le rôle du champ sans avoir à
ajouter de libellé de manière à ne pas surcharger le formulaire.
Chaque champ de saisie, ainsi que chaque bouton, déclare un ID afin de pouvoir être
identifié dans le code Java.
Pour traiter les données saisies par l’utilisateur, nous pouvons modifier l’implémentation
de l’activité.
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8
9 public class MainActivity extends AppCompatActivity {
10
11 private static final String TAG = "MainActivity";
12
13 private TextView nom;
14 private TextView prenom;
15 private TextView email;
16 private TextView naissance;
17 private TextView description;
26
18 private TextView commentaire;
19 private CheckBox acceptationConditions;
20
21 @Override
22 protected void onCreate(Bundle savedInstanceState) {
23 [Link](savedInstanceState);
24 setContentView([Link].activity_main);
25
26 nom = findViewById([Link]);
27 prenom = findViewById([Link]);
28 email = findViewById([Link]);
29 naissance = findViewById([Link]);
30 description = findViewById([Link]);
31 commentaire = findViewById([Link]);
32 acceptationConditions = findViewById([Link].acceptation_conditions);
33 }
34
35 public void handleOk(View v) {
36 // TODO faire quelque chose de plus utile
37 Log.i(TAG, "Ok");
38 Log.i(TAG, [Link]("Prénom : %s", [Link]()));
39 Log.i(TAG, [Link]("Nom : %s", [Link]()));
40 Log.i(TAG, [Link]("Email : %s", [Link]()));
41 Log.i(TAG, [Link]("Naissance : %s", [Link]()));
42 Log.i(TAG, [Link]("Description : %s", [Link]()));
43 Log.i(TAG, [Link]("Commentaire : %s", [Link]()));
44 Log.i(TAG, [Link]("Conditions acceptées : %b",
45 [Link]()));
46 }
47
48 public void handleAnnuler(View v) {
49 // TODO faire quelque chose de plus utile
50 Log.i(TAG, "Annulé");
51 }
52
53 }
54
Dans la méthode onCreate , à partir de la ligne 28, nous utilisons la
méthode findViewById pour récupérer chaque vue et la conserver dans un attribut.
Pour associer une action à chaque bouton, nous utilisons l’attribut android:onClick dans
le layout XML pour donner le nom d’une méthode de l’activité qui doit être appelée. Cela
évite de déclarer manuellement un listener dans le code de l’activité (comme nous
l’avons fait au chapitre précédent). La méthode de l’activité doit être publique, ne
retourner aucun résultat ( void ) et prendre en paramètre un objet de
type View correspondant à l’objet cliqué. Nous déclarons une méthode handleOk pour
l’associer au clic sur le bouton Ok et une méthode handleAnnuler pour l’associer au clic
sur le bouton Annuler .
27
La barre d’action
La barre d’action est un composant habituel de la conception d’une interface mobile. Elle
s’affiche en haut de l’écran et propose des actions contextuelles sous la forme d’icônes
ou d’un menu.
28
En cliquant sur Ok , un fichier XML nommé app_menu.xml est ajouté dans le
répertoire res/menu qui est créé à cette occasion. Un menu est décrit par un document
XML.
Exemple de menu
1 <?xml version="1.0" encoding="utf-8"?>
2 <menu
3 xmlns:android="[Link]
4 xmlns:app="[Link]
5
6 <item
7 android:id="@+id/menu_edition"
8 android:title="Edition"
9 android:icon="@android:drawable/ic_menu_edit"
10 app:showAsAction="ifRoom"/>
11
12 <item
13 android:id="@+id/menu_preferences"
14 android:title="Préférences"
15 android:icon="@android:drawable/ic_menu_preferences"
16 app:showAsAction="collapseActionView"/>
17
18 <item
19 android:id="@+id/menu_aide"
20 android:title="Aide"
21 android:icon="@android:drawable/ic_menu_help"
22 app:showAsAction="collapseActionView"/>
23
24 </menu>
Chaque item du menu possède un identifiant unique grâce à l’attribut android:id ainsi
qu’un titre et une icône. Vous pouvez fournir votre propre icône sous la forme d’une
ressource de type drawable. Notez qu’il existe des ressources fournies directement par
Android, et notamment des icônes de menu auxquelles vous pouvez accéder avec les
identifiants commençant par @android:drawable/ic_menu_ .
L’attribut app:showAsAction indique comment un item de menu doit être pris en compte par
la barre d’action. Vous pouvez donner une des valeurs suivantes :
always
L’item doit être toujours affiché dans la barre d’action.
never
L’item ne doit pas être affiché ni être présent dans le menu.
collapseActionView
L’item ne sera présent que dans le menu et jamais directement dans la barre
d’action
withText
29
L’item doit afficher son titre et pas uniquement son icône. Cela entraîne la plupart
du temps que l’item ne peut pas s’afficher correctement dans la barre d’action et
sera donc présent dans le menu.
ifRoom
L’item peut s’afficher dans la barre d’action que si cela est possible. Sinon, il sera
présent dans le menu.
Associer un menu à une activité
Pour associer un menu à une activité, il faut surcharger la
méthode onCreateOptionsMenu dans la classe de l’activité :
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate([Link].app_menu, menu);
return true;
}
L’objet retourné par la méthode getMenuInflater permet d’associer l’identifiant
du layout de menu au menu de l’activité.
Implémenter les actions associées aux items de menu
Pour déclencher une action lorsque l’utilisateur clique sur un bouton du menu, il faut
surcharger la méthode onOptionsItemSelected. Cette dernière prend en paramètre
l’item choisi dont il est possible de récupérer l’ID pour décider de l’action à réaliser.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ([Link]()) {
case [Link].menu_aide:
Log.i(TAG, "Clic sur le bouton d'aide");
return true;
case [Link].menu_edition:
Log.i(TAG, "Clic sur le bouton d'édition");
return true;
case [Link].menu_preferences:
Log.i(TAG, "Clic sur le bouton des préférences");
return true;
default:
return [Link](item);
}
}
Les boites de dialogue
Pour créer une boite de dialogue, vous pouvez utiliser la classe AlertDialog ainsi que la
classe [Link] pour vous aider à créer votre boite de dialogue.
Boite de dialogue avec boutons
Dans une application Android, les boites de dialogue ont généralement un, deux ou trois
boutons désignés sous le terme de positif (par exemple ok), de neutre (par exemple « plus
tard ») et de négatif (par exemple « annuler »). Vous pouvez utiliser la
classe [Link] pour définir le titre, le message et les boutons de la boite de
30
dialogue et appeler ensuite la méthode show pour présenter la boite de dialogue.
L’exemple ci-dessous présente du code qui peut être ajouté dans une activité :
1 public void afficherBoiteDeDialogue() {
2 [Link] dialogListener = new [Link]() {
3 @Override
4 public void onClick(DialogInterface dialog, int which) {
5 onClickBoiteDeDialogue(dialog, which);
6 }
7 };
8 new [Link](this)
9 .setTitle("Ma boite")
10 .setMessage("Comment allez-vous ?")
11 .setPositiveButton("Bien", dialogListener)
12 .setNeutralButton("Ne sais pas", dialogListener)
13 .setNegativeButton("Pas super", dialogListener)
14 .show();
15 }
16
17 private void onClickBoiteDeDialogue(DialogInterface dialog, int which) {
18 switch (which) {
19 case AlertDialog.BUTTON_POSITIVE:
20 new [Link](this).setMessage("Vous positivez !").show();
21 break;
22 case AlertDialog.BUTTON_NEUTRAL:
23 new [Link](this).setMessage("Vous avez le droit de ne pas savoir.").show();
24 break;
25 case AlertDialog.BUTTON_NEGATIVE:
26 new [Link](this).setMessage("Ça ne va pas fort !").show();
27 break;
28 }
29 }
30
31
Exemple d’une boite de dialogue sans bouton
À la ligne 2, la méthode afficherBoiteDeDialogue commence par déclarer un listener qui
implémente l’interface [Link]. Cette interface ne contient
qu’une seule méthode qui est appelée lorsque l’utilisateur interagit avec la boite de
dialogue. Cette méthode reçoit en paramètre la boite de dialogue concernée ainsi qu’un
paramètre which indiquant quel bouton a été cliqué. Dans cet exemple, on se contente
d’appeler une autre méthode onClickBoiteDeDialogue qui affiche elle-même une autre boite
de dialogue sans bouton.
À la ligne 9, on affiche une boite de dialogue en précisant les différents éléments (titre,
message, boutons avec leur listener) pour finalement appeler la méthode show() . Notez
que pour créer une instance de [Link], il faut passer en paramètre un
contexte d’exécution Android. Une activité est un contexte d’exécution. Si ces méthodes
sont ajoutées au code d’une activité, il suffit de passer this en paramètre du
constructeur.
Boite de dialogue avec choix
Un autre type de boite de dialogue assez courant consiste à permettre à l’utilisateur de
faire un choix dans une liste. Pour cela, il est possible d’utiliser la méthode setItems pour
préciser la liste des choix (ou l’ID de la ressource correspondant à un array). Comme pour
les boutons, on associe un listener à la liste des items. Le paramètre which reçu par la
méthode onClick correspond à l’index du choix de l’utilisateur.
1 public void afficherBoiteDeDialogue() {
2 [Link] dialogListener = new [Link]() {
3 @Override
4 public void onClick(DialogInterface dialog, int which) {
5 onClickBoiteDeDialogue(dialog, which);
6 }
32
7 };
8
9 String[] listeEtats = {"Bien", "Pas super", "Ne sais pas"};
10
11 new [Link](this)
12 .setTitle("Mon autre boite")
13 .setItems(listeEtats, dialogListener)
14 .show();
15 }
16 private void onClickBoiteDeDialogue(DialogInterface dialog, int which) {
17 switch (which) {
18 case 0:
19 new [Link](this).setMessage("Vous positivez !").show();
20 break;
21 case 1:
22 new [Link](this).setMessage("Ça ne va pas fort !").show();
23 break;
24 case 2:
25 new [Link](this).setMessage("Vous avez le droit de ne pas savoir.").show();
26 break;
27 }
28 }
29
33
paramètre. Le second paramètre est le texte à afficher et le troisième paramètre est une
constante pour indiquer la durée d’affichage du message (courte ou longue).
34
Chapitre 3 : Les activités
Une activité (activity) représente un écran dans une application Android. Nous avons vu
au chapitre précédent que l’emploi des layouts permet de garantir un rendu adapté selon
le type d’appareil et les dimensions de l’écran.
Pour développer des activités, il est aussi nécessaire de comprendre que
l’environnement Android se distingue du développement d’applications de bureau ou
d’applications Web. En effet, l’environnement Android est généralement plus limité en
ressources qu’un ordinateur classique. Il est donc important de limiter au mieux l’usage
de ces ressources (batterie, mémoire…). Pour cela, le système Android gère un cycle de
vie des activités en privilégiant l’économie de ressources. Même si l’utilisateur peut avoir
l’impression que l’écran qui lui est présenté existe aussi longtemps que l’application
s’exécute, dans la réalité, l’activité associée peut être détruite et recréée plusieurs fois
durant la vie de l’application. Par exemple, si l’utilisateur répond au téléphone alors
qu’une activité s’exécute, cette dernière est suspendue par le système et, si nécessaire,
le système peut décider de la supprimer pour économiser des ressources. Lorsque
l’utilisateur voudra reprendre l’utilisation de l’application, l’activité sera intégralement
recréée.
Le développement d’activités suppose de s’astreindre à tenir compte de ce cycle de vie
assez particulier afin que les applications se comportent comme attendues quelle que
soit la situation.
Le cycle de vie
Les activités sont des composants, c’est-à-dire qu’il n’est pas possible d’instancier
directement une activité dans le code d’une application (avec le mot-clè new par
exemple). Une activité est créée et gérée par le système. Un développeur doit donc être
soucieux de respecter la façon dont le système va gérer les activités de l’application. Le
système Android définit un cycle de vie d’une activité : cette dernière passe par des états
différents. Une activité peut être prévenue du passage dans un des états suivants :
• initialisée (initialized)
• créée (created)
• démarrée (started)
• relancée (resumed)
• mise en pause (paused)
• arrêtée (stopped)
• détruite (destroyed)
Chaque passage vers un nouvel état (hormis initialisée) est signalé par un appel à une
méthode de callback : onCreate, onStart, onResume, onPause, onStop, onDestroy plus
la méthode onRestart pour indiquer qu’une activité qui a été arrêtée est à nouveau
35
démarrée. Pour intervenir à un moment du cycle de vie de l’activité, il suffit de redéfinir la
méthode voulue. Attention, il faut penser à appeler la super implémentation de la
méthode.
Pour les méthodes onCreate, onStart, onResume, il vaut mieux appeler la super
implémentation avant son propre code :
@Override
protected void onCreate(Bundle savedInstanceState) {
[Link](savedInstanceState);
[Link]();
}
Le schéma ci-dessous détaille les transitions possibles entre chaque état avec l’appel
des méthodes de callback.
36
Le cycle de vie d’une activité (extrait de la documentation officielle)
Pour mieux comprendre ce schéma, prenons un exemple. Un utilisateur lance une
application de consultation de mail qui doit afficher une activité pour présenter la liste
des mails. Cette activité est créée par le système et ses
méthodes onCreate, onStart et onResume sont appelées à la suite. Après cela, l’activité
est affichée à l’écran. L’utilisateur clique sur un mail pour en consulter le détail. Cette
action crée et affiche une nouvelle activité. L’activité de liste des mails est arrêtée
(onPause puis onStop sont appelées) car elle n’est plus au premier plan. Puis l’utilisateur
appuie sur le bouton Back qui a pour objectif de le ramener sur la liste des mails.
L’activité doit être à nouveau affichée et ses
méthodes onRestart, onStart, onResume sont appelées avant cela. Puis, l’utilisateur
reçoit un coup de téléphone et répond. Cela amène l’application de téléphonie en
premier plan. Du coup, l’activité de liste des mails est à nouveau arrêtée
(onPause, onStop). Comme la communication téléphonique dure un certain temps, le
système décide de libérer des ressources. Lorsque l’utilisateur raccroche et revient à
l’application de mails, l’activité est entièrement recréée (onCreate, onStart, onResume).
Enfin, l’utilisateur consulte la liste des applications sur son téléphone. Cela amène
37
l’application en arrière-plan et suspend l’activité (onPause et onStop). L’utilisateur
décide d’arrêter l’application de mail. L’activité est alors complètement détruite
(onDestroy).
On voit avec l’exemple précédent que le cycle de vie d’une activité peut être très
complexe à décrire. Cette complexité permet au développeur de gérer au mieux les
ressources lorsqu’il implémente une activité. Pour chaque méthode de callback nous
allons fournir quelques bonnes pratiques.
onCreate
La redéfinition de cette méthode permet de réaliser les tâches d’initialisation les
plus coûteuse (comme par exemple, créer le layout d’affichage de l’activité).
C’est dans cette méthode que doivent être réalisés les traitements à exécuter une
seule fois pour toute la vie de l’activité.
onStart
La redéfinition de cette méthode permet de savoir quand une activité va être
rendue visible à l’utilisateur. Vous pouvez, par exemple, faire des mises à jour de
l’interface graphique avant affichage. Si votre activité joue une animation, cette
méthode est une bonne candidate pour lancer automatiquement l’animation, soit
parce que l’utilisateur démarre l’activité pour la première fois soit parce que
l’activité a été arrêtée et que l’animation doit être continuée.
onResume
La redéfinition de cette méthode permet de savoir quand une activité va être
accessible (y compris quand elle sort d’une pause). C’est le bon moment pour
votre activité pour déclencher des opérations consommatrices de ressources
(activation de la caméra, ouverture de connexions réseau…)
onPause
La redéfinition de cette méthode permet de savoir quand une activité n’est plus
active pour l’utilisateur. C’est le moment de suspendre les opérations
consommatrices de ressources (désactivation de la caméra, fermeture des
connexions réseau…). Attention, une activité en pause peut toujours être visible
de l’utilisateur. Vous ne devriez donc pas utiliser cette méthode pour modifier
l’interface graphique ou stopper des animations.
onStop
La redéfinition de cette méthode permet de savoir que l’activité va être stoppée et
ne sera plus visible de l’utilisateur. Si votre activité a besoin de sauvegarder des
informations (notamment sur disque) pour pouvoir redémarrer convenablement,
vous pouvez effectuer ces traitements à ce moment-là. Vous pouvez utiliser cette
redéfinition pour stopper les opérations graphiques (animations, lecteur de
vidéo…).
onDestroy
38
La redéfinition de cette méthode permet de savoir que l’activité va être détruite. Il
s’agit de l’ultime étape et vous pouvez effectuer ici des traitements avant la
suppression totale de l’activité. Attention, une activité peut être détruite par le
système pour libérer temporairement des ressources. Cela ne signifie donc pas
que l’activité a été simplement fermée par l’utilisateur.
Il existe une méthode de callback particulière :
onRestart
La redéfinition de cette méthode permet de savoir qu’une activité va être à
nouveau démarrée après avoir été stoppée. L’appel à cette méthode se situe
entre onStop et onStart. Cela permet plus facilement de faire la différence entre
le démarrage normal de l’activité et son redémarrage.
Terminer une activité
Vous pouvez terminer une activité en appelant sa méthode finish. L’activité est alors
détruite en passant par tous les états nécessaires. Si vous voulez savoir si l’activité est
détruite suite à un appel à finish, vous pouvez tester le retour de la méthode isFinishing.
Cette dernière méthode retourne false si l’activité est détruite à la demande du système
pour libérer des ressources.
Gestion de l’état d’une activité
Une des grandes difficultés lorsqu’on débute le développement d’activités est la gestion
de leur état. En effet, comme nous l’avons vu à la section précédente, un objet activité
peut être détruit et recréé plusieurs fois alors que l’utilisateur à l’impression à chaque
fois de voir le même écran. Si une activité possède un état interne alors cet état doit être
sauvé et restauré pour garantir une expérience utilisateur cohérente. Pour illustrer cette
situation, prenons un exemple simple. Voici un exemple d’activité avec son layout :
Une activité avec un état
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 public class MainActivity extends AppCompatActivity {
7
8 private TextView message;
9 private int nbClics;
10
11 @Override
12 protected void onCreate(Bundle savedInstanceState) {
13 [Link](savedInstanceState);
14 setContentView([Link].activity_main);
15 message = findViewById([Link]);
16 updateMessage();
17 }
18
39
19 public void onBoutonClic(View v) {
20 nbClics++;
21 updateMessage();
22 }
23
24 private void updateMessage() {
25 [Link]([Link]("Vous avez cliqué %d fois", nbClics));
26 }
27
28 }
Le layout de l’activité
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textMessage"
android:padding="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clic"
android:onClick="onBoutonClic"
android:layout_gravity="center" />
</[Link]>
Cette application affiche un bouton et un message. À chaque fois que l’utilisateur clique
sur le bouton, l’attribut nbClics est incrémenté et le message est mis à jour.
40
L’application au démarrage
41
Pour cela, vous pouvez redéfinir les
méthodes onSaveInstanceState et onRestoreInstanceState. La
méthode onSaveInstanceState est appelée lorsque le système va détruire l’activité pour
récupérer des ressources afin que celle-ci ait l’opportunité de sauver son contexte. Elle
prend en paramètre un objet de type Bundle. Un bundle permet de sauvegarder des types
simples (valeur booléenne, entier, chaîne de caractères…). Nous pouvons surcharger
cette méthode pour placer la valeur de l’attribut nbClics dans le bundle.
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
[Link]("nbClics", nbClics);
[Link](savedInstanceState);
}
Lorsque vous redéfinissez la méthode onSaveInstanceState, vous devez
impérativement appeler sa super implémentation car l’activité a besoin de
sauvegarder d’autres informations d’état.
Pour récupérer les informations à la recréation, nous pouvons redéfinir la
méthode onRestoreInstanceState pour, cette fois, lire les informations depuis
le bundle reçu en paramètre et mettre à jour le message :
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
[Link](savedInstanceState);
nbClics = [Link]("nbClics");
updateMessage();
}
Lorsque vous redéfinissez la méthode onRestoreInstanceState, vous devez
impérativement appeler sa super implémentation car l’activité a besoin de restaurer
d’autres informations d’état.
Après l’ajout de ces deux méthodes, vous pouvez relancer l’application et constater
qu’elle fonctionne comme attendu même lorsqu’on modifie l’orientation du téléphone
ou que l’application est passée temporairement en arrière-plan.
Plutôt que de redéfinir la méthode onRestoreInstanceState, nous pouvons utiliser
directement la méthode onCreate qui prend en paramètre le bundle. Ce paramètre
vaut null lorque l’activité est créée la première fois.
@Override
protected void onCreate(Bundle savedInstanceState) {
[Link](savedInstanceState);
setContentView([Link].activity_main);
message = findViewById([Link]);
if (savedInstanceState != null) {
nbClics = [Link]("nbClics");
}
updateMessage();
}
42
Astuce
La gestion de l’état est parfois une partie complexe à gérer pour le développeur. Pour
nous aider, l’implémentation par défaut sauvegarde automatiquement les informations
d’état de chaque vue. Si votre activité affiche un formulaire avec des champs de saisie,
la valeur de ces champs est automatiquement sauvegardée et restaurée si ces champs
possèdent un id donné par l’attribut android:id .
Il existe d’autres manières de conserver l’état d’une activité. L’utilisation d’un bundle est
adaptée pour conserver des états simples et uniquement pour gérer les cas de recréation
de votre activité pour des raisons d’économie de ressources. Si vous souhaitez disposer
d’un état persistant lorsque l’utilisateur relance l’application, vous devez opter pour une
solution basée sur des fichiers ou même une base de données. Le modèle par
composant défini par Android JetPack propose également une
approche Model/View pour gérer la sauvegarde d’état lorsqu’une application est
stoppée par le système.
Notion d’intent
Un intent est un message qui est envoyé pour activer un composant Android (comme une
activité). Un intent peut être émis par un composant de l’application vers un autre
composant de l’application. Par exemple, une activité peut ainsi démarrer une nouvelle
activité (comme nous le verrons au chapitre suivant). Mais un intent peut également
servir à déclencher un composant d’une autre application. Le système Android gère ainsi
un système de messagerie inter-applicative. Certains composants émettent
des intents et d’autres demandent au système à être sollicités lors de l’envoi de
ces intents. Cela permet un système à couplage faible et permet à une application de
s’insérer facilement dans l’écosystème des applications.
Un intent peut contenir les informations suivantes :
Nom du composant
Un intent peut désigner une application spécifiquement avec son nom de package
ou même un composant précis de l’application (comme une activité). Dans ce
cas, on dit que l’intent est explicite. Mais il est également possible d’utiliser un
intent implicite, c’est-à-dire qu’il ne fournit pas de nom de composant. Dans ce
cas, le système est responsable de trouver l’application la plus appropriée pour
répondre. Par exemple, on peut émettre un intent pour envoyer un mail. Le
système doit ouvrir l’application la plus adaptée pour cela. Si plusieurs
applications sont éligibles, le système demande à l’utilisateur celle qu’il préfère
utiliser.
Action
L’action est un identifiant sous la forme d’une chaîne de caractères. Il existe des
identifiants d’action prédéfinis par le système mais vous pouvez très bien créer
les vôtres si nécessaire.
43
Data
La data se présente sous la forme d’une URI ou d’un type MIME pour désigner
l’objet de l’intent.
Catégorie
La catégorie est un identifiant sous la forme d’une chaîne de caractères. Elle
permet de préciser l’action.
Extras
Les extras sont en fait des paramètres sous la forme clé/valeur. Par exemple pour
un intent destiné à envoyer un mail, on peut ajouter un extrait précisant le sujet du
mail.
Flags
Les flags sont des méta-informations que l’on peut ajouter à l’intent pour des
utilisations avancées.
Associer une activité à un intent
Lorsque l’on déclare une activité dans le fichier [Link] , il est possible de lui
associer un filtre d’intent (intent-filter) pour indiquer au système dans quels cas cette
activité doit être déclenchée. Si vous consultez le fichier [Link] pour un projet
créé par Android Studio, vous verrez un fichier ressemblant à celui-ci :
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest
3 xmlns:android="[Link]
4 package="[Link]">
5
6 <application
7 android:allowBackup="true"
8 android:icon="@mipmap/ic_launcher"
9 android:label="@string/app_name"
10 android:roundIcon="@mipmap/ic_launcher_round"
11 android:supportsRtl="true"
12 android:theme="@style/AppTheme">
13 <activity android:name=".MainActivity">
14 <intent-filter>
15 <action android:name="[Link]" />
16 <category android:name="[Link]" />
17 </intent-filter>
18 </activity>
19 </application>
20
21 </manifest>
À partir de la ligne 13, ce ficher manifeste déclare une activité en l’associant au filtre
d’intent avec l’action [Link] de la catégorie [Link] .
Il s’agit de l’intent qui est envoyé à l’application lorsque l’utilisateur lance l’application.
Donc ce filtre permet au système de savoir quelle activité créer au lancement de
l’application.
44
Une (et une seule) activité devrait être associée à ce type d’intent pour permettre à
l’application de pouvoir être exécutée directement par l’utilisateur en cliquant sur son
icône. Mais il est également possible d’envisager que la même activité (ou une autre)
puisse être lancée à partir d’une autre action.
Un exemple assez courant est la possibilité de lancer une activité lorsque le système
reçoit la directive d’afficher le contenu d’une URI. Prenons le cas du QR Code suivant :
une application de lecture de QR Code sur un système Android, cette dernière va décoder
le QR Code et va demander au système d’afficher l’URI décodée demo://test . Pour cela,
elle va émettre un intent. Le système va alors chercher une application capable de
prendre en charge le schéma demo . Les schémas les plus courants
comme http , https ou tel est déjà pris en charge par des applications installées par
défaut. Mais nous pouvons fournir une activité prenant spécifiquement en charge une
URI de la forme demo:// grâce à l’utilisation d’un filtre d’intent.
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest
3
4 xmlns:android="[Link]
5 package="[Link]">
6
7 <application
8
android:allowBackup="true"
9
10 android:icon="@mipmap/ic_launcher"
11 android:label="@string/app_name"
12 android:roundIcon="@mipmap/ic_launcher_round"
13 android:supportsRtl="true"
14
15 android:theme="@style/AppTheme">
16 <activity android:name=".MainActivity">
17 <intent-filter>
18 <action android:name="[Link]" />
19
20
<category android:name="[Link]" />
21 </intent-filter>
22 </activity>
23 <activity android:name=".DemoActivity">
24 <intent-filter>
25
26 <action android:name="[Link]"/>
27 <category android:name="[Link]"/>
45
28 <category android:name="[Link]"/>
29 <data android:scheme="demo"/>
</intent-filter>
</activity>
</application>
</manifest>
L’activité déclarée dans le manifeste à partir de la ligne 19 est associée à un filtre d’intent
pour l’action [Link] et dont les données correspondent au
schéma demo (ligne 24). Dit autrement, si on demande au système d’afficher une URI
commençant par demo:// , il démarrera notre application pour lancer
l’activité DemoActivity .
L’activité DemoActivity
1 package [Link];
2 import [Link];
3
4 import [Link];
5 import [Link];
6 import [Link];
7
8
9
public class DemoActivity extends AppCompatActivity {
10
11 @Override
12 protected void onCreate(Bundle savedInstanceState) {
13 [Link](savedInstanceState);
14
15
16 TextView textView = new TextView(this);
17 Uri intentData = getIntent().getData();
18 String message = [Link]("Vous avez ouvert l'activité à partir de l'adresse %s",
19
[Link]());
20
21 [Link](message);
setContentView(textView);
}
}
Notez à la ligne 16 que l’activité peut accéder à l’intent qui a déclenché sa création grâce
à la méthode getIntent. On peut ainsi facilement récupérer l’URI grâce à la propriété data .
Dans notre exemple, on se contente de l’afficher à l’écran.
1 <activity android:name=".OtherDemoActivity">
2 <intent-filter>
3 <action android:name="[Link]"/>
4 <category android:name="[Link]"/>
5 <category android:name="[Link]"/>
6 <data android:scheme="https"
7 android:host="[Link]"
8 android:path="/launch"/>
9 </intent-filter>
10 </activity>
46
L’activité déclarée ci-dessus est ouverte quand le système reçoit un message lui
demandant d’ouvrir l’URI [Link] . Vous pouvez vous contenter de
placer un lien sur une page Web de votre site.
Émettre un intent implicite
Nous pouvons très facilement demander le lancement d’une activité en utilisant
un intent. Android distingue les intents implicites et les intents explicites. Un intent
implicite doit être résolu par le système, c’est-à-dire qu’il doit sélectionner l’application
la plus appropriée pour réaliser l’action (si plusieurs applications sont éligibles, alors le
système affiche une boite de dialogue à l’utilisateur pour qu’il choisisse l’application
qu’il préfère lancer). A contrario, un intent explicite permet de désigner explicitement
l’application, voire l’activité, que l’on souhaite exécuter. Un intent implicite permet très
facilement à une application d’interagir avec les autres applications. En effet, le
programme se contente de déclarer le type d’action qu’il souhaite voir réaliser et le
système doit choisir l’application appropriée. Un intent explicite est généralement utilisé
pour ouvrir des activités au sein de la même application et gérer les enchaînements entre
les activités (comme nous le verrons au chapitre suivant).
L’émission d’un intent implicite se fait en trois étapes :
1. On crée l’intent pour déclarer l’action et les données
2. On vérifie qu’il existe au moins une application pour prendre en charge ce type
d’intent
3. On demande au système de lancer l’activité capable de prendre en compte notre
intent.
Ci-dessous un exemple d’émission d’un intent implicite pour déclencher un appel
téléphonique :
1 Uri numero = [Link]("[Link]
2 Intent intentAppel = new Intent(Intent.ACTION_DIAL, numero);
3 List<ResolveInfo> infos = getPackageManager().queryIntentActivities(intentAppel,
4 PackageManager.MATCH_DEFAULT_ONLY);
5 if (! [Link]()) {
6 startActivity(intentAppel);
7 } else {
8 [Link](this, "Impossible d'appeler", Toast.LENGTH_LONG).show();
9 }
10
Aux lignes 1 et 2, on crée une URI représentant le numéro de téléphone à appeler avec le
schéma tel: et on crée l’intent pour une action DIAL . La ligne 4 permet d’effectuer une
requête pour savoir s’il existe une ou plusieurs activités capables de prendre en charge
l’intent. Pour notre code, il suffit de nous assurer que cette liste n’est pas vide pour être
sûr que l’intent sera pris en charge. Puis, ligne 7, on émet l’intent grâce à la
méthode startActivity et le système se charge des opérations nécessaires.
47
Chapitre 4 : Gestion des activités d’une
application
Dans ce chapitre, nous allons voir un exemple d’application assez classique. Cette
application sera l’occasion de présenter comment gérer plusieurs activités au sein d’une
même application lorsque nous voulons présenter différents écrans à l’utilisateur. Nous
aurons également l’occasion de découvrir comment gérer des listes d’items avec
Android.
L’application de démo
À titre d’exemple, nous nous baserons sur une application de démo qui présente au
démarrage une liste d’occupations (comme par exemple le bricolage, le cinéma…). Les
occupations sont regroupées par catégories. Une catégorie est désignée par une icône
dans l’application.
48
Pour simplifier notre exemple, la liste des occupations a été codée en dur dans
l’application. Cependant, nous pourrions faire évoluer le code pour extraire cette
information depuis une base de données stockée sur le mobile ou en effectuant une
requête vers une API Web.
Structure de l’application
L’application fournit une classe OccupationsSingleton . Comme son nom l’indique, il
s’agit d’un singleton qui permet d’obtenir la liste de toutes les occupations, la liste des
occupations qui sont dans la même catégorie qu’une occupation donnée ou une
occupation à partir de son identifiant.
La classe OccupationsSingleton
package [Link];
import [Link];
import [Link];
public class OccupationsSingleton {
private static OccupationsSingleton instance = new OccupationsSingleton();
public static OccupationsSingleton getInstance() {
return instance;
}
private OccupationsSingleton() {
// code omis
}
public List<Occupation> getOccupations() {
// code omis
}
49
optimise l’utilisation des ressources en recyclant les composants graphiques utilisés
pour afficher chaque élément de la liste. Il s’agit d’un composant évolué de l’API Android.
Il repose sur le modèle MVVM (Model View ViewModel).
Le modèle MVVM (Model View ViewModel) est une variante du modèle MVC. Plutôt que
de permettre au contrôleur et à la vue d’accéder au modèle, on remplace le contrôleur
par le ViewModel qui fournit une interface d’accès au modèle. Il est notifié par la vue en
cas de modification par l’utilisateur et il peut également notifier la vue si le modèle est
modifié.
Ce modèle, bien que plus complexe dans sa conception, est plus facile à mettre en place
car l’implémentation du RecyclerView prend en charge une bonne partie du code pour
nous. Concrètement, nous devons fournir une implémentation du ViewModel qui se
présente sous la forme d’une classe abstraite Java nommée Adapter.
Un héritage de la classe abstraite Adapter a pour principale responsabilité de fournir et
de mettre à jour des objets de type ViewHolder. Ces objets permettent de gérer la
représentation à l’écran de chaque élément de la liste du RecyclerView. Dans notre
application, l’implémentation de cette classe abstraite est faite par la
classe OccupationAdapter .
La classe OccupationAdapter
1 package [Link];
2
3
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8
9
import [Link];
11
12 import [Link];
13 import [Link];
14 import [Link];
15
16
17 public class OccupationAdapter extends
18 [Link]<[Link]> {
19
20
21
public static class OccupationViewHolder extends [Link] {
22 private View content;
23 private Occupation occupation;
24
25 public OccupationViewHolder(View content) {
26
27 super(content);
28 [Link] = content;
29 [Link](new [Link]() {
30 @Override
31
32
public void onClick(View v) {
33 Intent intent = new Intent([Link](), [Link]);
34 [Link]("occupationId", [Link]());
35
50
36 [Link]().startActivity(intent);
37 }
38
39 });
40 }
41
42 public void setOccupation(Occupation occupation) {
43
44
[Link] = occupation;
45 TextView textView = [Link]([Link]);
46 [Link]([Link]());
47 // On extrait l'identifiant de ressource correspondant à l'icône.
48 int imageId = [Link]().getIdentifier("ic_categorie_" +
49
50 [Link](), "drawable", [Link]().getPackageName());
51 if (imageId == 0) {
52 imageId = [Link].ic_categorie_defaut;
53 }
54
55
// On positionne l'image à gauche du libellé du texte.
56 [Link](imageId, 0, 0, 0);
57 }
58 }
59
60
61 private List<Occupation> occupations;
62
63 public OccupationAdapter(List<Occupation> occupations) {
64 [Link] = occupations;
65
66
}
67
68 @NonNull
69 @Override
public OccupationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
viewType) {
View view = [Link]([Link]()).inflate([Link].occupation_view,
parent, false);
return new OccupationViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull OccupationViewHolder viewHolder, int position) {
[Link]([Link](position));
}
@Override
public int getItemCount() {
return [Link]();
}
}
Cette classe connaît la liste des occupations qui lui est passée en paramètre de
constructeur. Elle doit ensuite implémenter les
méthode onCreateViewHolder, onBindViewHolder et getItemCount.
onCreateViewHolder
51
Cette méthode retourne une classe héritant de ViewHolder. Comme son nom
l’indique, elle maintient une référence vers une View qui correspond à la
représentation graphique d’un élément de la liste. Pour notre exemple, à la ligne
56, nous utilisons le LayoutInflater pour créer une View à partir d’un fichier XML
de layout. Il est ainsi possible de créer n’importe quel type de layout pour afficher
un élément de la liste.
onBindViewHolder
Cette méthode est appelée pour associer un ViewHolder précédemment créé à
un élément de la liste. Un élément est représenté par sa position dans la liste.
Ainsi le RecyclerView n’a pas besoin de connaître la type exact du modèle, il
demande à l’adaptateur de s’assurer que la View référencée par
le ViewHolder est bien associée à un élément particulier de la liste.
getItemCount
Retourne le nombre d’éléments dans la liste afin de permettre au RecyclerView de
gérer l’affichage et le défilement des éléments à l’écran.
Pour notre implémentation, nous définissons une classe interne OccupationViewHolder des
lignes 16 à 45. Cette classe maintient une référence vers le composant View d’affichage
et un objet de type Occupation . La méthode setOccupation , déclarée des lignes 33 à 44, met
à jour le composant View à partir des informations de l’occupation. Elle est appelée par
l’adaptateur à la ligne 62 quand il lui est demandé d’assigner un élément de la liste à
un ViewHolder.
Notre classe OccupationAdapter est utilisée lors de l’initialisation de l’activité principale de
l’application dans la classe MainActivity .
La classe MainActivity
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 public class MainActivity extends AppCompatActivity {
7 @Override
8 protected void onCreate(Bundle savedInstanceState) {
9 [Link](savedInstanceState);
10 setContentView([Link].activity_main);
11 RecyclerView occupationsView = findViewById([Link]);
12 [Link](new LinearLayoutManager(this));
13 [Link](new
14 OccupationAdapter([Link]().getOccupations()));
15 }
16
17 }
18
19
52
Cette classe est assez simple. Elle redéfinit la méthode onCreate afin de positionner
un layout à partir d’un fichier XML de ressource. Ce layout contient
une RecyclerView pour laquelle on spécifie son propre layout (pour afficher les éléments
les uns sous les autres) et l’adaptateur à utiliser pour obtenir les objets de
type ViewHolder (ligne 17).
dependencies {
// ...
implementation '[Link]:recyclerview:1.1.0'
// ...
}
Détail d’une occupation et pile des activités
Pour afficher une activité présentant le détail d’une occupation, il faut capter
l’événement de clic dans la liste mais il faut surtout ouvrir une seconde activité depuis
l’activité principale. Le système Android gère les activités comme appartenant à une
tâche (task). Une tâche peut être définie comme un ensemble cohérent d’activités qui
permet de réaliser une macro fonctionnalité. Dans notre exemple, la tâche représente la
consultation des occupations.
Au sein d’une tâche, le système gère les activités sous la forme d’une pile. Une nouvelle
activité s’empile sur la précédente. Nous pouvons l’illustrer comme ci-dessous :
53
précise le nom de la classe de l’activité. Il est également possible de passer des
paramètres à l’intent (que l’on appelle des extras). Si l’activité qui affiche le détail d’une
occupation s’appelle OccupationActivity , alors on peut ouvrir l’activité avec un code comme
celui-ci :
Utilisation d’un intent explicite pour ouvrir une activité
Intent intent = new Intent(context, [Link]);
[Link]("occupationId", occupationId);
[Link](intent);
Dans le code ci-dessus, la variable context correspond au contexte d’exécution de
l’application disponible pour chaque composant graphique et la
variable occupationId contient l’identifiant de l’occupation à afficher. À charge du code
dans la classe OccupationActivity d’analyser l’intent pour extraire l’identifiant de
l’occupation et de récupérer sa représentation objet pour afficher les informations.
Pour l’activité de présentation d’une occupation, le code est le suivant :
La classe OccupationActivity
1 package [Link];
2 import [Link];
3
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8
9
import [Link];
1
0 public class OccupationActivity extends AppCompatActivity {
1 @Override
1 protected void onCreate(Bundle savedInstanceState) {
1
2 [Link](savedInstanceState);
1 setContentView([Link].activity_occupation);
3 Intent intent = getIntent();
1 int occupationId = [Link]("occupationId", 0);
4
1
Occupation occupation =
5 [Link]().getOccupation(occupationId);
1 TextView nomView = findViewById([Link]);
6 [Link]([Link]());
1 TextView descriptionView = findViewById([Link]);
7
[Link]([Link]());
RecyclerView occupationsView = findViewById([Link]);
[Link](new LinearLayoutManager(this));
[Link](new
OccupationAdapter([Link]().getOccupationsMemeCategorie(oc
cupation)));
}
}
À la ligne 18, on récupère l’intent qui a déclenché l’ouverture de l’activité et on extrait l’id
de l’occupation en tant qu’extra. Ensuite, ligne 21, on récupère l’objet Occupation grâce
au singleton. Le reste du code consiste à remplir l’interface graphique avec les
54
informations de l’occupation en utilisant les identifiants des composants définis dans
le layout de l’activité.
20 public OccupationViewHolder(View content) {
21 super(content);
22 [Link] = content;
23 [Link](new [Link]() {
24 @Override
25 public void onClick(View v) {
26 Intent intent = new Intent([Link](), [Link]);
27 [Link]("occupationId", [Link]());
28 [Link]().startActivity(intent);
29 }
30 });
31 }
Stratégie de gestion de la pile des activités
Dans notre application, il se trouve que l’activité présentant le détail d’une occupation
affiche la liste des occupations de la même catégorie. Que se passe-t-il si l’utilisateur
clique sur un élément de cette liste ? Dans notre code, la classe OccupationActivity utilise
un RecyclerView auquel elle passe le même adaptateur que pour la liste principale. La
seule différence est que l’adaptateur reçoit en paramètre de construction uniquement la
liste des occupations de la même catégorie. Donc il est possible de cliquer sur un
élément de cette liste pour ouvrir une activité de détail. Dans ce cas, le système continue
d’empiler les activités les unes sur les autres comme illustré ci-dessous :
55
Empilement successif sans répétition de l’activité de détail
Dans cette situation, l’utilisateur clique sur une occupation dans la liste globale pour
consulter le détail. Puis, quand il clique sur une occupation appartenant à la même
catégorie depuis cet écran de détail, alors l’activité est simplement mise à jour avec les
nouvelles données. Du point de vue de l’utilisateur, ce qui change vraiment, c’est le
comportement de l’application lorsqu’il cliquera sur le bouton de retour en arrière. Peu
importe le nombre d’occupations qu’il aura consulté, il sera directement ramené sur la
liste générale.
Pour réaliser ce comportement, il faut considérer que l’activité de détail d’une
occupation doit être démarrée en mode single top. Cela signifie que, si une telle activité
n’est pas en haut de la pile, elle doit être créée, sinon l’activité présente doit être
réutilisée.
Nous pouvons déclarer une activité single top de deux manières différentes. Soit nous
pouvons préciser le mode grâce à l’attribut launchMode dans le manifeste de l’application
:
Déclaration du mode singletop dans le manifeste de l’application
<activity android:name=".OccupationActivity" android:launchMode="singleTop">
</activity>
Ainsi nous sommes certain qu’une activité de ce type sera toujours single top. Soit, nous
pouvons le faire de manière programmatique en ajoutant un flag à l’intent qui va servir à
démarrer l’activité :
Ajout d’un flag à l’intent pour demander le mode single top
Intent intent = new Intent(context, [Link]);
[Link]("occupationId", occupationId);
[Link](Intent.FLAG_ACTIVITY_SINGLE_TOP);
[Link](intent);
Pour que notre application fonctionne correctement, nous sommes obligés de revoir
l’implémentation de la classe OccupationActivity . En effet le mode single top implique que,
si l’activité est déjà au sommet de la pile, elle n’est pas recréée. Le système se contente
alors d’appeler la méthode onNewIntent.
Évolution de la classe OccupationActivity pour le support du single top
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
56
public class OccupationActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
[Link](savedInstanceState);
setContentView([Link].activity_occupation);
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
[Link](intent);
int occupationId = [Link]("occupationId", 0);
Occupation occupation =
[Link]().getOccupation(occupationId);
57
Chapitre 5 : Gestion des messages broadcast
Nous avons vu que les activités sont lancées à partir de l’émission d’un intent. Un
système Android généralise ce principe avec les messages de type broadcast. En
informatique, la notion de broadcast désigne la télédiffusion d’une information sans
connaissance a priori ni du nombre ni du type de composants qui peuvent traiter cette
information.
Un système Android fournit un modèle de publication/souscription pour l’émission et la
réception des messages de type broadcast. Un composant peut émettre un message
tandis qu’un autre peut s’inscrire pour recevoir ce type de message. Cela permet de créer
un système de communication à couplage faible : les émetteurs n’ont pas besoin de
connaître les récepteurs (et réciproquement).
Le système Android lui-même émet de nombreux messages en broadcast. Par exemple,
si votre application à besoin de se connecter à Internet, elle peut adapter son
comportement lorsque le système perd la connexion réseau (mais également lorsque la
connexion réseau est rétablie). Pour cela, vos activités peuvent souscrire à la réception
de broadcast de type NETWORK_STATE_CHANGED_ACTION pour être prévenue de
l’évolution de la connexion Wifi. On trouve de nombreuses utilisations pour la prise en
charge de broadcast système : connaître les changements d’état du réseau, être prévenu
lorsque la batterie est déchargée, savoir que le téléphone est mis en veille ou sort de
veille, être prévenu du démarrage du système ou de son arrêt…
Le BroadcastReceiver
Le BroadcastReceiver est une classe abstraite qui permet de créer un composant en
charge de recevoir des broadcasts. Une classe héritant de BroadcastReceiver doit
fournir une implémentation de la méthode abstraite onReceive pour traiter le message.
On peut ensuite déclarer le BroadcastReceiver dans le manifeste de l’application pour
l’associer à un filtre d’intent, c’est-à-dire pour définir les messages auxquels le récepteur
s’inscrit.
Suivre le branchement sur secteur
Prenons l’exemple d’une application qui souhaite être prévenue lorsque le téléphone est
branché sur secteur ou bien lorsqu’il passe en mode batterie. Ces changements d’états
correspondent à deux broadcasts différents :
• Pour le branchement sur secteur, l’action
est [Link].ACTION_POWER_CONNECTED qui est donnée par la
constante ACTION_POWER_CONNECTED
58
• Pour le passage sur batterie, l’action
est [Link].ACTION_POWER_DISCONNECTED qui est donnée par la
constante ACTION_POWER_DISCONNECTED
On peut créer un BroadcastReceiver pour traiter ces deux actions :
Un exemple de BroadcastReceiver
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class PowerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if ([Link]().equals(Intent.ACTION_POWER_CONNECTED)) {
[Link](context, "Connecté sur secteur", Toast.LENGTH_LONG).show();
} else if ([Link]().equals(Intent.ACTION_POWER_DISCONNECTED)) {
[Link](context, "En mode batterie", Toast.LENGTH_LONG).show();
}
}
}
Dans cet exemple simple, l’implementation de PowerReceiver se contente de vérifier
l’action correspondant à l’intent reçu en paramètre de la méthode onReceive pour
afficher une information à l’utilisateur.
Enfin, pour que le BroadcastReceiver reçoive les messages, il faut le déclarer dans le
manifeste de l’application dans la balise <application> :
Déclaration du BroadcastReceiver dans [Link]
<receiver android:name="[Link]">
<intent-filter>
<action android:name="[Link].ACTION_POWER_CONNECTED" />
<action android:name="[Link].ACTION_POWER_DISCONNECTED" />
</intent-filter>
</receiver>
En déclarant le BroadcastReceiver dans le manifeste de l’application, il n’est pas
nécessaire que l’application soit démarrée pour que le message soit reçu. Dans ce cas,
le système démarre l’application sans aucune activité et se contente d’instancier un
objet de la classe PowerReceiver pour appeler sa méthode onReceive. Le premier
paramètre de cette méthode correspond au contexte d’exécution du BroadcastReceiver.
Il est possible de s’en servir pour appeler la méthode startActivity.
Un BroadcastReceiver peut donc effectuer des actions complexes en réponse à un
message.
Suivre l’état de la connexion Wifi
Pour une application dépendante de la connexion Internet, il peut être intéressant de
réagir au changement d’état de cette connexion. On peut déclarer
59
une BroadcastReceiver pour l’action [Link].STATE_CHANGE qui est également
fournie par la constante NETWORK_STATE_CHANGED_ACTION.
Ci-dessous un exemple d’implémentation d’un BroadcastReceiver :
60
il ne faut surtout pas oublier d’appeler la méthode unregisterReceiver quand l’activité n’a
plus besoin du BroadcastReceiver.
Pour illustrer notre exemple, voici le code source de l’activité.
Un exemple d’activité utilisant un BroadcastReceiver
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8 import [Link];
9 import [Link];
10 public class AirplaneActivity extends AppCompatActivity {
11 private TextView airplaneModeView;
12 private AirplaneModeReceiver airplaneModeReceiver = new AirplaneModeReceiver();
13 private class AirplaneModeReceiver extends BroadcastReceiver {
14 @Override
15 public void onReceive(Context context, Intent intent) {
16 setAirplaneModeMessage();
17 }
18 }
19 @Override
20 protected void onCreate(Bundle savedInstanceState) {
21 [Link](savedInstanceState);
22 [Link]().getApplicationContext();
23 setContentView([Link].activity_airplane);
24 airplaneModeView = findViewById([Link]);
25 }
26
27 @Override
28 protected void onResume() {
29 [Link]();
30 IntentFilter intentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
31 registerReceiver(airplaneModeReceiver, intentFilter);
32 setAirplaneModeMessage();
33 }
34
35
36 @Override
37 protected void onPause() {
38 [Link]();
39 unregisterReceiver(airplaneModeReceiver);
40 }
41
42 private void setAirplaneModeMessage() {
43 if(airplaneModeView != null) {
44 [Link](isAirplaneMode() ? "Mode avion": "Pas mode avion");
45 }
46 }
47
61
48 private boolean isAirplaneMode() {
49 return [Link]([Link](),
50 [Link].AIRPLANE_MODE_ON,
51 0) != 0;
52 }
53 }
Dans la classe AirplaneActivity , on déclare une classe interne AirplaneModeReceiver qui est
un BroadcastReceiver (lignes 18 à 23). On dispose de la
méthode setAirplaneModeMessage (ligne 47 à 51) qui permet de mettre à jour l’interface
graphique avec un message selon l’état du mode avion. Dans l’activité, on redéfinit la
méthode onResume pour mettre à jour l’interface graphique mais également pour
enregistrer le BroadcastReceiver (lignes 36-37) en l’associant à un IntentFilter. On
redéfinit également la méthode onPause pour être sûr de désenregistrer
le BroadcastReceiver (ligne 44) lorsqu’il n’est plus utile à l’activité.
Émettre un message broadcast
Une application peut elle-même émettre un message broadcast en utilisant la méthode
de contexte sendBroadcast. Le message est représenté sous la forme d’un Intent. Il est
donc possible de concevoir une application dans laquelle les différents composants
peuvent s’échanger des informations sur le modèle de la publication/souscription.
Emission d’un message depuis une activité
Intent intent = new Intent();
[Link]("[Link].MON_MESSAGE");
// on ajoute éventuellement des extras
[Link]("info","Ceci est une info accompagnant le message");
// émission du message
[Link](intent);
Lorsque les messages émis sont à destination uniquement de composants de notre
application, il est possible d’utiliser le LocalBroadcastManager qui permet de limiter la
diffusion de messages à l’application elle-même.
Emission d’un message local à l’application depuis une activité
Intent intent = new Intent();
[Link]("[Link].MON_MESSAGE");
// on ajoute éventuellement des extras
[Link]("info","Ceci est une info accompagnant le message");
// émission locale à l'application
[Link](this).sendBroadcast(intent);
62
Chapitre 6 : Accès Web
Il est courant pour une application mobile d’échanger des données par une interface
réseau. Les périphériques Android offre de nombreuse interface : Web avec le Wifi, USB,
NFC, Bluetooth. Dans ce chapitre nous verrons comment échanger des données sur le
Web en interagissant avec des API Web.
Permission pour l’accès Internet
Certaines fonctionnalités ne sont accessibles que si l’application dispose des droits
suffisants. Pour accéder à Internet, une application doit obtenir la permission pour le
droit [Link] . Les permissions requises sont listées dans le manifeste de
l’application. Elles sont ensuite présentées à l’utilisateur au moment de l’installation de
l’application. Il doit valider l’autorisation de ces permissions pour finaliser l’installation.
Déclaration de la permission dans le fichier [Link]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="[Link]
package="[Link]">
<!--
...
-->
</manifest>
Utilisation de la classe URL
Pour accéder à une ressource sur le Web, vous pouvez utiliser les classes fournies
directement par l’API Java. Cependant, il est nécessaire de prendre en compte les
problématiques de développement asynchrone dans une application Android.
La classe URL en Java sert, bien évidemment, à représenter une URL. Mais elle offre
également une méthode openConnection qui permet d’initier une connexion à l’adresse
représentée. Cette méthode s’occupe de retourner un objet de type URLConnection. Il
est possible de transtyper (cast) cet objet selon le schéma de connexion utilisé. Par,
exemple, l’API Android assure qu’elle peut prendre en charge le schéma http avec la
classe HttpURLConnection et le schéma https avec la classe HttpsURLConnection. À
partir de cette connexion, on peut obtenir un flux d’entrée permettant de lire la réponse.
Exemple d’ouverture de connexion avec la classe URL
1 Scanner scanner = null;
2 String response = "";
3 try {
4 URL url = new URL("[Link]
5 URLConnection connection = [Link]();
6 InputStream stream = [Link]();
7 scanner = new Scanner(stream).useDelimiter("\0");
63
8 response = [Link]();
9 } finally {
10 if (scanner != null) {
11 [Link]();
12 }
13 }
Pour simplifier l’écriture, le code ci-dessus utilise un objet de type Scanner pour lire le
flux d’entrée. Même s’il est fonctionnel, ce code reste simpliste et laisse de côté la
gestion des erreurs, la gestion de l’encodage du message reçu, le positionnement d’en-
têtes pour la requête HTTP… Mais surtout, si vous exécutez ce code dans une activité,
vous obtiendrez une exception de type NetworkOnMainThreadException car Android
refuse de réaliser des accès réseaux directement depuis le code d’une activité. Cela tient
au fait qu’il est difficile d’anticiper le temps nécessaire au traitement d’une requête HTTP
: elle peut être quasi-instantanée ou nécessiter plusieurs secondes, voire dizaines de
secondes. Or ce code est bloquant, puisqu’à la ligne 8, l’appel à la méthode next ne
retourne que lorsque la totalité de la réponse aura été lue. Cela signifie que l’activité
serait figée et que l’utilisateur ne pourrait plus interagir avec l’application pendant ce
temps. Afin d’éviter ce comportement, Android nous impose d’utiliser un appel
asynchrone.
Les appels asynchrones permettent de réaliser des traitements pour lesquels nous ne
souhaitons pas que le programme attende le résultat. Cela permet à l’application de ne
pas se bloquer et de rester réactive aux demandes de l’utilisateur. Pour une application
Android, les traitements pouvant prendre plusieurs secondes doivent être réalisés de
manière asynchrone. Pour les appels réseaux, cela est rendu obligatoire par
l’implémentation de l’API Android.
Un appel asynchrone peut être réalisé en utilisant les classes fournies par l’API Java telles
que Thread ou l’interface Executor du package [Link]. Android fournit
également la classe abstraite AsyncTask que nous allons utiliser comme exemple.
La classe abstraite AsyncTask est une classe générique, qui dépend du type de données
en entrée, du type de données pour indiquer la progression de la tâche et du type de
données produites par la tâche. Pour notre exemple, en entrée nous aurons des objets
de type URL représentant les URL et en sortie une String pour le contenu des pages.
L’indicateur de progression est généralement un Integer pour représenter un
pourcentage de progression (pour garder notre exemple simple, nous n’utiliserons pas le
système de progression). Nous devons fournir une implémentation de la méthode
abstraite doInBackground qui représente le traitement de la tâche.
Exemple d’une AsyncTask pour le téléchargement de texte
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
64
5 import [Link];
6 import [Link];
7 import [Link];
8 import [Link];
9
10 public class DownloadTextTask extends AsyncTask<URL, Integer, String> {
11
12 private static final String TAG = "DownloadTextTask";
13
14 @Override
15 protected String doInBackground(URL... urls) {
16 StringBuilder buffer = new StringBuilder();
17 for(URL url: urls) {
18 try {
19 [Link](download(url));
20 } catch (IOException e) {
21 Log.e(TAG, "Erreur lors de l'accès à Internet", e);
22 }
23 }
24 return [Link]();
25 }
26
27 private String download(URL url) throws IOException {
28 Scanner scanner = null;
29 try {
30 URLConnection connection = [Link]();
31 InputStream stream = [Link]();
32 scanner = new Scanner(stream).useDelimiter("\0");
33 return [Link]();
34 } finally {
35 if (scanner != null) {
36 [Link]();
37 }
38 }
39 }
40 }
La classe DownloadTextTask implémente la méthode privée download qui reprend le code de
téléchargement vu plus haut. Cette classe hérite de la méthode execute qui permet de
passer la tâche dans une file d’attente d’exécution. Cette file d’attente est prise en
charge par un autre thread qui appellera une à une la méthode doInBackground de
toutes les tâches. Ce mécanisme permet de garantir que le code de la tâche ne sera pas
exécuté dans le même thread que celui de l’activité.
Appel de la tâche asynchrone depuis une activité
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
65
7
8 public class InternetActivity extends AppCompatActivity {
9
10 private TextView textView;
11
12 @Override
13 protected void onCreate(Bundle savedInstanceState) {
14 [Link](savedInstanceState);
15 setContentView([Link].activity_internet);
16 textView = findViewById([Link]);
17 }
18
19 @Override
20 protected void onResume() {
21 [Link]();
22 DownloadTextTask downloadTextTask = new DownloadTextTask() {
23 @Override
24 protected void onPostExecute(String data) {
25 // ce code est appelé après le traitement asynchrone de doInBackground
26 [Link](data);
27 }
28 };
29
30 [Link]("Chargement en cours...");
31 try {
32 URL url = new URL("[Link]
33 [Link](url);
34 } catch (MalformedURLException e) {
35 [Link]("L'URL n'est pas bonne");
36 }
37 }
38 }
39
L’activité InternetActivity récupère le document à
l’adresse [Link] pour l’afficher dans un
composant TextView. La méthode onCreate se limite à mettre en place le contenu de la
vue à partir d’un layout et de récupérer le composant TextView qui contiendra le
document téléchargé.
La méthode onResume réalise l’appel asynchrone. Elle définit une classe anonyme qui
hérite de DownloadTextTask en redéfinissant la méthode onPostExecute (lignes 23 à 29). La
méthode onPostExecute est fournie par la classe AsyncTask. Elle ne réalise aucun
traitement par défaut. Elle est appelée dès que l’appel à la méthode doInBackground est
fini. AsyncTask nous garantit que la méthode onPostExecute est appelée dans
le thread de l’activité et qu’elle reçoit en paramètre l’objet qui a été retourné par l’appel
à doInBackground. Nous pouvons donc récupérer le texte téléchargé et le positionner
dans le TextView (ligne 27). Les lignes 33 et 34 créent l’URL et la passe en paramètre de
la méthode execute (ligne 34) qui déclenche l’appel asynchrone.
66
On voit qu’une requête HTTP nécessite une attention particulière à cause de la prise en
charge du traitement asynchrone. Si l’exemple précédent a la vertu de rendre apparents
les mécanismes mis en œuvre pour réaliser ce traitement asynchrone, le code produit
est complexe à comprendre et source d’erreur lorsqu’une application doit massivement
faire des appels Web. Nous allons voir que nous pouvons beaucoup simplifier notre code
en ayant recours à une bibliothèque tierce qui encapsulera une bonne partie de ces
mécanismes pour nous.
Utilisation de la bibliothèque Volley
Volley est une bibliothèque Android pour le traitement de requête HTTP. En plus d’un
support pour la création et l’analyse des réponses, elle fournit un support complet
intégrant le chiffrement pour les connexions https, la gestion d’un cache client, la gestion
concurrente dans l’émission de requêtes… Volley est également simple à mettre en
place et à utiliser.
Ajout de la dépendance
Comme il s’agit d’une bibliothèque tierce, nous devons ajouter une dépendance dans le
fichier [Link] du module qui va utiliser Volley.
Ajout de la dépendance à Volley
dependencies {
// ...
implementation '[Link]:volley:1.1.1'
// ...
}
Émission d’une requête
Toutes les requêtes HTTP sont émises par Volley en tâche de fond de manière
asynchrone. Ces requêtes sont mises dans une file d’attente et sont traitées au fur et à
mesure par Volley. Dans notre code, nous devons commencer par créer une file d’attente
de type RequestQueue pour pouvoir ensuite ajouter nos requêtes.
Création de la file d’attente
RequestQueue requestQueue = [Link](this);
Une requête est représentée par la classe Request<T> ou une des classes qui en héritent.
Dans notre premier exemple, nous allons utiliser la classe StringRequest qui attend un
résultat sous la forme d’une chaîne de caractères.
Envoi d’une requête
1 String url = "[Link]
2 StringRequest request = new StringRequest([Link], url,
3
4 new [Link]<String>() {
5 @Override
6 public void onResponse(String response) {
7 // traitement en cas de succès
8
9
}
10 }, new [Link]() {
11 @Override
12 public void onErrorResponse(VolleyError error) {
67
13 // traitement en cas d'échec
14 }
});
[Link](request);
De la ligne 2 à 13, nous créons la requête en passant quatre paramètres au constructeur
: la méthode HTTP à utiliser (dans cet exemple, GET), l’URL de la requête, un
objet listener pour le traitement du résultat de la requête en cas de succès et un
objet listener pour le traitement en cas d’échec. Volley masque pour nous l’envoi de la
requête, le traitement asynchrone et la transformation de la réponse. En cas de succès,
la méthode du listener reçoit en paramètre de la méthode onResponse le texte reçu. Pour
envoyer cette requête, il suffit de l’ajouter à la file d’attente (ligne 14).
Nous pouvons adapter ce code pour l’introduire dans une activité. Comme la file
d’attente est réutilisable, nous pouvons la créer au lancement de l’activité et la conserver
comme attribut.
Mise en place de Volley dans une activité
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8 import [Link];
9 import [Link];
10 import [Link];
11
12 public class VolleyInternetActivity extends AppCompatActivity {
13
14 private static final Object REQUEST_TAG = new Object();
15 private RequestQueue requestQueue;
16 private TextView infoView;
17
18 @Override
19 protected void onCreate(Bundle savedInstanceState) {
20 [Link](savedInstanceState);
21 setContentView([Link].activity_volley_internet);
22 infoView = findViewById([Link]);
23 requestQueue = [Link](this);
24 }
25
26 @Override
27 protected void onResume() {
28 [Link]();
29 String url = "[Link]
30 StringRequest request = new StringRequest([Link], url,
31 new [Link]<String>() {
32 @Override
33 public void onResponse(String response) {
68
34 [Link](response);
35 }
36 }, new [Link]() {
37 @Override
38 public void onErrorResponse(VolleyError error) {
39 [Link]("Erreur de téléchargement : " + [Link]());
40 }
41 });
42 [Link]("Chargement en cours...");
43 [Link](REQUEST_TAG);
44 [Link](request);
45 }
46
47 @Override
48 protected void onPause() {
49 [Link]();
50 [Link](REQUEST_TAG);
51 }
52 }
La file d’attente est créée ligne 26 au moment de la création de l’activité. La requête HTTP
est émise pour récupérer un texte qui doit être affiché dans un TextView (lignes 32 à 47).
À la ligne 46, nous ajoutons une étiquette à la requête grâce à la méthode setTag . Cela
permet de créer un groupe de requêtes qu’il est possible d’annuler. Ligne 53, lorsque
l’activité est mise en pause, nous appelons la méthode cancelAll afin d’annuler une
éventuelle requête en attente qui n’aurait pas encore aboutie.
Émission d’une requête pour une réponse JSON
Une application Android est un client parfait pour une API Web. Les API Web sont
destinées à fournir sur le Web des informations traitables par des programmes. La
plupart du temps, ces informations sont échangées sous le format de représentation
JSON car il est facilement traitable par programmation.
Volley fournit deux spécialisations de la classe Request<T> : la classe JsonObjectRequest qui
attend en objet JSON en réponse et la classe JsonArrayRequest qui attend un tableau JSON
en réponse.
À titre d’exemple, nous allons utiliser l’API Géo mise en ligne par l’administration
publique pour fournir des informations sur les communes, les départements et les
régions en France.
Par exemple, si vous émettez la requête HTTP suivante :
[Link]
Vous obtiendrez la réponse suivante :
{
"nom": "Gironde",
"code": "33",
"region": {
"code": "75",
"nom": "Nouvelle-Aquitaine"
69
}
}
Pour en savoir plus sur cette API, vous pouvez vous référer à la documentation officielle.
La classe JsonObjectRequest offre un constructeur qui attend quatre paramètres :
1. Une constante représentant la méthode HTTP à utiliser
2. L’URL de la ressource
3. Un objet de type JSONObject représentant les données à envoyer dans la
requête (ou null si on ne souhaite pas envoyer de données)
4. Un listener de type Listener<JSONObject> pour traiter une réponse en succès
5. Un listener de type ErrorListener pour traiter une réponse en échec
La classe JSONObject est fournie par l’API Android est permet de construire ou de
consulter un document JSON en Java.
Nous pouvons déclarer une activité en utilisant le layout suivant :
Le layout de l’activité de consultation d’un département
<?xml version="1.0" encoding="utf-8"?>
<[Link]
xmlns:android="[Link]
xmlns:tools="[Link]
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RegionActivity">
<EditText
android:id="@+id/codeDepartement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number"
android:maxLength="2"
android:hint="Code département" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Rechercher"
android:onClick="search"/>
<TextView
android:id="@+id/departementResultat"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</[Link]>
Ce layout très simple déclare un champ de saisi texte pour le code du département, un
bouton pour lancer la recherche et un TextView pour afficher le résultat de la recherche.
Le bouton répond à l’événement click en invoquant la méthode search de l’activité.
70
L’activité de consultation d’un département
1 package [Link];
2 import [Link];
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8 import [Link];
9 import [Link];
10 import [Link];
11 import [Link];
12 import [Link];
13 import [Link];
14 import [Link];
15
16 public class RegionActivity extends AppCompatActivity {
17 private static final Object REQUEST_TAG = new Object();
18 private static final String URL_PATTERN =
19 "[Link]
20 private RequestQueue requestQueue;
21 private EditText codeDepartementEdit;
22 private TextView departementResultatView;
23
24 @Override
25 protected void onCreate(Bundle savedInstanceState) {
26 [Link](savedInstanceState);
27 setContentView([Link].activity_region);
28 codeDepartementEdit = findViewById([Link]);
29 departementResultatView = findViewById([Link]);
30 requestQueue = [Link](this);
31 }
32 @Override
33 protected void onStop() {
34 [Link]();
35 cancelRequest();
36 }
37
38 private void cancelRequest() {
39 if (requestQueue != null) {
40 [Link](REQUEST_TAG);
41 }
42 }
43
44 public void search(View view) {
45 String codeDepartement = [Link]().toString();
46 if ([Link]()) {
47 return;
48 }
49 if ([Link]() == 1) {
50 codeDepartement = "0" + codeDepartement;
71
51 }
52 sendRequest(codeDepartement);
53 }
54
55 private void sendRequest(String codeDepartement) {
56 cancelRequest();
57 [Link]("Chargement...");
58 String url = [Link](URL_PATTERN, codeDepartement);
59 JsonObjectRequest request = new JsonObjectRequest([Link], url, null,
60 new [Link]<JSONObject>() {
61 @Override
62 public void onResponse(JSONObject response) {
63 fillDepartementResultat(response);
64 }
65 }, new [Link]() {
66 @Override
67 public void onErrorResponse(VolleyError error) {
68 if([Link] != null
69 && [Link] == 404) {
70 [Link]("Pas de département trouvé. Veuillez vérifier le
71 code saisi.");
72 } else {
73 [Link]("Erreur : " + [Link]());
74 }
75 }
76 });
77 [Link](REQUEST_TAG);
78 [Link](request);
79 }
80
81 private void fillDepartementResultat(JSONObject jsonObject) {
82 try {
83 String nomDepartement = [Link]("nom");
84 String codeDepartement = [Link]("code");
85 JSONObject jsonRegion = [Link]("region");
86 String nomRegion = [Link]("nom");
87 String msg = [Link]("%s est le département avec le code %s. Ce département fait
88 partie de la région %s.", nomDepartement, codeDepartement, nomRegion);
89 [Link](msg);
90 } catch (JSONException e) {
91 [Link]("Erreur : " + [Link]());
92 }
93 }
}
La méthode onCreate (lignes 24 à 31) récupère une référence sur les principaux
composants du layout et crée une RequestQueue pour Volley. La méthode search (lignes 45
à 54) qui est appelée lorsque l’utilisateur clique sur le bouton, récupère l’information
saisie et appelle la méthode privée sendRequest . La méthode sendRequest (lignes 56 à 79)
réalise la création de la requête de type JsonObjectRequest et l’ajoute dans la file d’attente
72
de Volley. Si la requête est un succès, le listener appelle la
méthode fillDepartementResultat (ligne 64). Enfin la méthode fillDepartementResultat (lignes 81 à
92) extrait les informations de l’objet JSON pour construire le message à afficher dans
la TextView.
Envoi de données au format JSON avec Volley
private void inscrire(String prenom, String nom) throws JSONException {
String url = "[Link]
JSONObject requestPayload = new JSONObject();
[Link]("prenom", prenom);
[Link]("nom", nom);
JsonObjectRequest request = new JsonObjectRequest([Link], url,
requestPayload,
new [Link]<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
// code en cas de succès
}
}, new [Link]() {
@Override
public void onErrorResponse(VolleyError error) {
// code en cas d'échec
}
});
[Link](request);
}
73
Chapitre 7 : Les services Android
Un service est un composant Android qui s’exécute sans interface graphique. Il dispose
de son propre cycle de vie géré par le système. Un service est idéal pour implémenter un
traitement long pour lequel on ne souhaite pas bloquer l’activité de l’utilisateur. Par
exemple, une application peut avoir besoin d’envoyer en masse des données à un
serveur. Pour cela, une activité peut démarrer un service qui aura la charge de l’envoi
pendant que l’utilisateur peut continuer à interagir avec l’application (il peut même
fermer les activités) sans interrompre le service. Un service peut également être utilisé
pour initier des traitements indépendamment de toutes interactions graphiques avec
l’utilisateur. Si une application doit interagir avec des périphériques en Bluetooth, elle
peut attendre de capter un échange Bluetooth pour une demande de connexion. Ce type
de comportement est réalisé à partir d’un service et ne nécessite pas d’interaction avec
l’utilisateur.
Lorsqu’on conçoit un service Android, on imagine la plupart du temps un service
s’exécutant en tâche de fond (background), c’est-à-dire un service qui n’interrompt pas
l’interaction entre l’utilisateur et les activités. Cependant, un service Android n’est pas
par défaut exécuté en tâche de fond. Si vous souhaitez implémenter un service qui doit
réaliser des tâches d’une longue durée, vous devez implémenter vous-même une
exécution de votre code en tâche de fond. Sinon, le système risque d’interrompre votre
service en estimant qu’il consomme trop de ressources ou qu’il est actif depuis trop
longtemps.
Un service Android peut également être appelé directement depuis un autre composant
(une activité, un broadcast receiver ou même un autre service) en exposant une
interface. On retrouve alors un modèle d’application multi-couches dans lequel l’activité
prend en charge la présentation et l’interaction avec l’utilisateur et le service réalise les
traitements métiers de l’application. Dans ce cas, on parle de service lié (bound service)
à une activité. Ce modèle permet de développer des applications plus complexes
puisqu’un même service peut être utilisé par différentes activités.
Dans ce chapitre nous verrons comment créer un service en tâche de fond (background
service) et comment permettre aux autres composants de l’application de se lier à un
service (bound service).
74
Un service Android est un composant qui hérite directement ou indirectement de la
classe Service. Puisqu’un service est un composant, il doit donc être déclaré dans le
fichier manifeste [Link] du module.
Déclaration d’un service dans le fichier [Link]
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="[Link]
3 package="[Link]">
4 <application>
5 <service android:name=".MonService" android:exported="false">
6 </service>
7 </application>
8 </manifest>
À la ligne 5, on déclare le service. On utilise l’attribut android:exported pour indiquer si le
service peut être sollicité par une autre application. Pour des raisons de sécurité, il est
conseillé de positionner cet attribut à false et de n’exporter que les services pour
lesquels l’export est souhaitable.
Structure d’une classe de service
1 package [Link];
2
3 import [Link];
4 import [Link];
5 import [Link];
6
7 public class MonService extends Service {
8
9 /*
10 * La redéfinition de cette méthode est obligatoire car elle est déclarée
11 * abstraite dans la classe Service. Néanmoins, cette méthode n'est utilisée
12 * que pour les services liés (Bound Services).
13 */
14 @Override
15 public IBinder onBind(Intent intent) {
16 return null;
17 }
18
19 @Override
20 public int onStartCommand(Intent intent, int flags, int startId) {
21 // TODO Implémenter le service
22 return START_STICKY;
23 }
24 @Override
25 public void onCreate() {
26 // TODO Implémenter les actions à la création du service
27 }
28
29 @Override
30 public void onDestroy() {
31 // TODO Implémenter les actions à la destruction du service
32 }
75
33
34 }
35
La classe ci-dessus présente une structure simple pour un service. La méthode onBind à
la ligne 15 est la seule obligatoire car elle est déclarée abstract dans la classe Service.
Cependant, la méthode onBind est utilisée uniquement pour les services liés (Bound
Service) que nous verrons plus bas. Donc, nous nous contentons de faire retourner null à
cette méthode.
Le cycle de vie
Tout comme les activités, les services ont un cycle de vie qui est géré par le système.
Lorsque le service vient d’être créé, le système appelle sa méthode onCreate. Lorsque le
système veut interrompre le service (à sa demande ou pour récupérer des ressources),
le système appelle préalablement la méthode onDestroy. Vous n’êtes pas obligé de
redéfinir les méthodes onCreate et onDestroy dans votre service (leur implémentation
par défaut est vide).
Entre ces étapes de création et de destruction, le cycle de vie varie selon l’usage du
service. Pour les services en tâche de fond (background service), le système appelle
également la méthode onStartCommand pour informer le service qu’il a reçu une
demande d’exécution.
Important
Afin de limiter l’usage des ressources, si on demande au système de démarrer une
instance d’un service alors qu’une instance existe déjà, alors le système réutilisera
l’instance existante. Il faut garder à l’esprit que la méthode onStartCommand peut être
appelée plusieurs fois pour une instance alors que les
méthodes onCreate et onDestroy ne sont appelées qu’une seule fois par instance.
76
Le cycle de vie d’un service en tâche de fond (extrait de la documentation officielle)
La méthode onStartCommand signale au service qu’il doit s’exécuter. Cette méthode
prend trois paramètres :
intent
Un intent qui a été utilisé pour démarrer le service (Cf. ci-dessous)
flag
Un indicateur pour savoir si le service a été redémarré par le système. Il peut prendre
comme valeur les constantes START_FLAG_REDELIVERY ou START_FLAG_RETRY qui
permettent de préciser les conditions de redémarrage du service.
startId
Un entier qui identifie de manière unique l’appel à la méthode onStartCommand du
service. Cet entier permet de gérer l’arrêt du service en s’assurant que toutes les
commandes ont été traitées.
La méthode onStartCommand doit également retournée un entier pour décrire au
système la stratégie à appliquer si le service est interrompu par le système. Cela peut se
produire si le système estime qu’il doit libérer des ressources (par exemple pour
économiser de l’énergie). Cet entier correspond à une constante parmi les suivantes:
START_STICKY
Si le système arrête le service, il devra le redémarrer plus tard en appelant à
nouveau la méthode onStartCommand en passant une référence null pour
l’intent.
START_STICKY_COMPATIBILITY
77
Si le système arrête le service, il devra le redémarrer plus tard et il pourra
éventuellement appeler à nouveau la méthode onStartCommand en passant une
référence null pour l’intent. Cet appel n’est pas obligatoire.
START_NOT_STICKY
Si le système arrête le service, il n’aura pas à le redémarrer.
START_REDELIVER_INTENT
Si le système arrête le service, il devra le redémarrer en passant à nouveau l’intent
en paramètre. Dans ce cas, le
paramètre flag vaudra START_FLAG_REDELIVERY lors du nouvel appel.
Un service en tâche de fond ne s’arrête que lorsqu’on appelle sa méthode stopSelf en
passant en paramètre son identifiant d’exécution (le startId reçu en paramètre
de onStartCommand). À moins de vouloir conserver un service en mémoire, il est
indispensable d’appeler la méthode stopSelf lorsqu’un traitement est terminé.
Exécution en tâche de fond
Même si un service est souvent utilisé pour réaliser des traitements en tâche de fond, la
classe Service ne fournit pas directement de mécanisme pour cela. Un service utilise,
par défaut, le même thread d’exécution que les activités de l’application (appelé
le thread principal). Si le système détecte un service qui utilise depuis trop longtemps
le thread principal, il peut décider d’interrompre ce service.
Pour réaliser un traitement en tâche de fond, il existe différents mécanismes disponibles
dans l’API Android. Pour une présentation détaillée, reportez-vous au chapitre Running
Android tasks in background threads dans la documentation officielle. Pour des
traitements qui n’ont pas besoin de recevoir des messages du système, vous pouvez
simplement utiliser l’API Java Standard [Link] et, notamment, une
implémentation de ExecutorService.
Ci-dessous un exemple pour l’implémentation d’un service utilisant des exécuteurs :
Un service s’exécutant en tâche de fond
1 package [Link];
2
3
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8
9
import [Link];
10 import [Link];
11
12 public class BackgroundService extends Service {
13
14
15 public static final String TAG = "PremierService";
16 private ExecutorService executorService;
17
18 /*
19
20
* La redéfinition de cette méthode est obligatoire car elle est déclarée
78
21 * abstraite dans la classe Service. Néanmoins, cette méthode n'est utilisée
22 * que pour les services liés (Bound Services).
23
24 */
25 @Override
26 public IBinder onBind(Intent intent) {
27 return null;
28
29
}
30
31 @Override
32 public void onCreate() {
33 Log.i(TAG, "Lancement du service");
34
35 // Création d'un service d'exécution avec cinq threads.
36 [Link] = [Link](5);
37 }
38
39
40
@Override
41 public int onStartCommand(Intent intent, int flags, int startId) {
42 Log.i(TAG, "Appel à onStartCommand");
43 String message = [Link]("message");
44 // Création de l'exécuteur et ajout dans la file d'attente du service.
45
46 // L'exécuteur sera lancé dans un thread à part dès que possible.
47 [Link](new LogMessageExecutor(message, startId));
48 return START_REDELIVER_INTENT;
49 }
50
51
52 @Override
53 public void onDestroy() {
54 Log.i(TAG, "Destruction du service");
55 // arrêt du service d'exécution
56
57 [Link]();
58 }
59
60 /**
61
62
* Classe interne fournissant le traitement à réaliser
63 * en tâche de fond
64 */
65 private class LogMessageExecutor implements Runnable {
66
67
68 private String message;
69 private int startId;
70
71 LogMessageExecutor(String message, int startId) {
72
73
[Link] = message;
74 [Link] = startId;
75 }
76
77 @Override
public void run() {
try {
Log.i(TAG, "Je suis un service de log avec comme message : " + [Link]);
// On attend 3 secondes pour simuler un traitement
[Link](3000);
79
Log.i(TAG, "Fin de l'exécution");
} catch (InterruptedException e) {
// Si le thread est interrompu par un signal (rien de spécial à faire)
} finally {
stopSelf([Link]);
}
}
}
}
Dans la méthode onCreate, on crée une instance de ExecutorService (ligne 29).
Dans la méthode onStartCommand, on crée et on ajoute un exécuteur (ligne 38).
L’exécuteur est une instance de LogMessageExecutor qui est une classe interne. Cette
classe implémente l’interface Runnable et réalise un traitement long. Pour les besoins
de l’exemple, on se contente d’endormir le thread pendant trois secondes.
À la fin de son exécution, à la ligne 73, l’exécuteur appelle la méthode stopSelf qui est en
fait la méthode de la classe englobante BackgroundService . Ainsi, dès que l’exécuteur a
terminé sa tâche, on demande au système d’arrêter le service s’il n’existe aucune
demande d’exécution postérieure au startId passé à la construction de l’exécuteur.
La méthode onDestroy est redéfinie de manière à fermer l’instance
de ExecutorService (ligne 46).
Notez qu’on peut très facilement passer des paramètres à l’exécution d’un service sous
la forme d’extras dans l’intent reçu par la méthode onStartCommand.
Interaction avec l’utilisateur
Normalement, un service en tâche de fond n’a pas d’interaction avec l’utilisateur
puisque sa fonction est précisément de réaliser un traitement en arrière plan.
Néanmoins, il est souvent très utile de pouvoir informer l’utilisateur de la progression du
traitement ou de sa fin. Un service correspond lui-même à un contexte d’exécution, il
peut donc être passé en paramètre lorsqu’une instance de Context est attendue. Il est
donc très facile de faire apparaître des messages pour l’utilisateur sous la forme de
Toast, de Snackbar ou de notification (Cf. le chapitre sur l’interface graphique).
Important
Si vous voulez invoquer une méthode utilisant un Context depuis un thread, vous devez
vous assurer que ce thread dispose d’un accès au thread principal qui gère les messages
pour l’affichage graphique. Vous devez pour cela utiliser un handler pour mettre en place
un échange de messages entre le thread principal de votre application et le thread
réalisant le traitement en tâche de fond.
Lancement du service
Pour lancer un service, il suffit d’appeler la méthode startService depuis une activité,
un broadcast receiver ou même depuis un autre service. L’appel à cette méthode attend
une instance de Intent identifiant le service voulu.
80
Exemple d’appel d’un service depuis une activité
Intent intent = new Intent(this, [Link]);
[Link](intent);
Service lié
Un service lié (Bound Service) est un service qui est attaché à un autre composant
Android (le plus souvent une activité). Il permet de fournir une interface de
communication avec le service pour appeler directement ses méthodes. Un service lié
est très adapté pour implémenter un service pour lequel on attend un résultat synchrone.
Il n’y a pas spécifiquement de différence de nature entre un service lié et un service en
tâche de fond. Un service lié n’a pas besoin d’implémenter la
méthode onStartCommand mais il doit impérativement fournir une implémentation
complète de la méthode onBind.
Déclaration du service
Un service lié doit également être déclaré dans le fichier manifeste [Link] de
l’application :
Déclaration d’un service dans le fichier [Link]
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="[Link]
3 package="[Link]">
4 <application>
5 <service android:name=".MonService" android:exported="false">
6 </service>
7 </application>
8 </manifest>
Le cycle de vie
Comme pour un service en tâche de fond, lorsque le service vient d’être créé, le système
appelle sa méthode onCreate. Lorsque le système veut interrompre le service (à sa
demande ou pour récupérer des ressources), le système appelle préalablement la
méthode onDestroy. Vous n’êtes pas obligé de redéfinir les
méthodes onCreate et onDestroy dans votre service (leur implémentation par défaut est
vide).
Entre ces étapes de création et de destruction, le cycle de vie varie selon l’usage du
service. Quand un autre composant veut se lier au service, le système appelle la
méthode onBind de ce dernier. Cette méthode doit retourner une instance de
l’interface IBinder. Lorsque le composant se délie, le système appelle la
méthode onUnbind du service.
81
Le cycle de vie d’un service lié (extrait de la documentation officielle)
82
19
20 @Override
21 public IBinder onBind(Intent intent) {
22 return [Link];
23 }
24
25 public String getRandomWord() {
26 String[] array = {"bonjour", "méchant", "loup", "maison", "pain d'épice"};
27 return array[new Random().nextInt([Link])];
28 }
29
30 }
Le service ci-dessus n’offre qu’une méthode réellement utile : getRandomWord . Cette
méthode retourne un mot au hasard parmi une liste prédéfinie. La
méthode onBind retourne une instance de RandomServiceBinder (ligne 22). La
classe RandomServiceBinder est une classe interne qui étend la classe Binder (lignes 12 à
16). Elle fournit la méthode getService qui retourne l’instance du service.
Pour cet exemple, nous n’avons pas besoin de fournir une implémentation pour la
méthode onUnbind car il n’y a pas de traitement à réaliser lorsque le service est délié.
Lier le service à une activité
Pour se lier à un service, il faut utiliser la méthode bindService. Pour se délier d’un
service, il faut utiliser la méthode unbindService. Ces méthodes ont besoin d’une
connexion vers le service qui est représentée par une implémentation de
l’interface ServiceConnection. Le lien avec un service est asynchrone, on ne peut pas
créer le service directement, on peut simplement ouvrir une connexion vers un service et
attendre que le système nous prévienne quand le service a été éventuellement démarré
et a été correctement lié.
Exemple de liaison du service depuis une activité
1 package [Link];
2
3 import [Link];
4 import [Link];
5 import [Link];
6 import [Link];
7 import [Link];
8 import [Link];
9 import [Link];
10
11 public class ClientActivity extends AppCompatActivity {
12
13private RandomService randomService;
14
15private ServiceConnection serviceConnection = new ServiceConnection() {
16 @Override
17 public void onServiceConnected(ComponentName name, IBinder service) {
18
83
19 [Link] binder = ([Link])
20
service;
21 randomService = [Link]();
22 }
23
24 @Override
25 public void onServiceDisconnected(ComponentName name) {
26 randomService = null;
27 }
28};
29
30 @Override
31 protected void onCreate(Bundle savedInstanceState) {
32 [Link](savedInstanceState);
33 setContentView([Link].activity_client);
34 }
35
36 @Override
37 protected void onStart() {
38 [Link]();
39 Intent intent = new Intent(this, [Link]);
40 bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
41 }
42
43 @Override
44 protected void onStop() {
45 [Link]();
46 unbindService(serviceConnection);
47 }
48
49 // ...
}
Dans l’exemple ci-dessus, on crée une classe interne anonyme pour fournir une
implémentation à l’interface ServiceConnection (lignes 15 à 26). Il faut pour cela
implémenter les méthodes onServiceConnected et onServiceDisconnected. Il s’agit de
méthodes de callback qui seront appelées respectivement quand le service sera lié et
quand le service sera délié. Pour notre implémentation, nous récupérons en paramètre
de onServiceConnected l’instance de IBinder que nous trans-typons
en [Link] pour pouvoir appeler la méthode getService que nous
avons précédemment implémentée (lignes 18 et 19). Nous pouvons ainsi initialiser
l’attribut randomService (ligne 19). La méthode onServiceDisconnected se contente de
remettre la valeur de l’attribut randomService à null (ligne 24).
Un service fait partie des ressources que l’activité peut acquérir. Il est donc conseillé
d’ouvrir une connexion vers le service dans la redéfinition de la méthode onStart et de
fermer la connexion vers le service dans la redéfinition de la méthode onStop.
84
Dans la redéfinition de onStart, nous appelons la méthode bindService en fournissant
un intent pour identifier le service voulu ainsi que la connexion à utiliser. Le dernier
paramètre passé à la méthode est un flag pour décrire comment la liaison doit se faire.
Pour notre exemple, nous utilisons la constante BIND_AUTO_CREATE qui est le
comportement par défaut : si le service n’existe pas encore en mémoire, il est créé. Sinon
on établit une liaison avec le service existant.
Dans la redéfinition de onStop, nous appelons la méthode unbindService pour libérer la
liaison avec le service.
Comme l’établissement de la liaison d’un service est asynchrone, partout dans le code
de l’activité où nous voulons solliciter le service, nous sommes obligés de vérifier d’abord
s’il n’est pas null .
Exemple d’utilisation du service pour l’activité
if ([Link] != null) {
String word = [Link]();
// ...
}
85
The MainActivity class in an Android application is responsible for setting up the initial user interface from an XML layout resource when its onCreate method is called. It includes configuring a RecyclerView by linking it to a LinearLayoutManager to display list items in a vertical manner. Additionally, the class sets an adapter (OccupationAdapter), which provides the data items to be displayed from a singleton class, ensuring data consistency across activity instances .
The behavior of the activity stack can be modified by using the 'singleTop' launch mode. This approach avoids creating a new instance of the detail activity each time an occupation is selected when the user is already viewing one. If the activity is already at the top of the stack, it will simply receive new data via the onNewIntent method, and the user will be returned to the main list directly upon pressing the back button, regardless of how many details were viewed .
A single activity is restarted rather than a new instance being created when it uses the 'singleTop' launch mode. In this scenario, if the activity is already at the top of the stack, the existing activity will be reused with onNewIntent being called to deliver new data, avoiding the overhead of creating and initializing a new activity .
An explicit intent in Android is used to switch between activities by specifying the target activity class directly. This is done by creating an Intent object with the current context and destination activity class, then calling startActivity with this intent. Parameters can be passed using Intent.putExtra for data sharing between activities .
The onNewIntent method facilitates updating existing activities by providing a way to handle new data designated to an activity already residing at the top of the stack in 'singleTop' mode. This method allows the activity to refresh its display and data without being recreated, ensuring efficient resource use and continuity in user experience .
Setting the launch mode to 'singleTop' affects user navigation by reusing the current instance of an activity if it is at the top of the activity stack. This prevents duplicate instances when the user performs repeated actions that normally trigger activity recreation. It also changes the back navigation experience by immediately returning the user to the previous state without navigating through multiple instances .
Declaring a service in AndroidManifest.xml with exported set to false is beneficial for security as it restricts the service from being accessed by other applications. This ensures that only the application's own components can interact with the service, preventing unauthorized access from external applications .
The primary difference between a bound service and a background service in Android is how they are utilized by components. A bound service is connected to a component like an activity, providing a communication interface (using onBind method) for calling methods directly and expecting synchronous results. In contrast, a background service runs independently to perform long-running operations without direct interaction from a component .
The Android system manages the lifecycle of a background service by calling onCreate when the service is created and onDestroy when it is stopped. During its operation, onStartCommand handles incoming start requests. If the service is destroyed, START_STICKY can be used to ensure the service is restarted automatically without a specific intent .
In Android, to ensure a service is restarted after being stopped by the system, the onStartCommand method should return an integer START_STICKY. This tells the system to recreate the service and call onStartCommand with a null Intent to maintain its running state .