Licence CC BY-NC-SA

Préambule : quelques concepts avancés

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Le Manifest est un fichier que vous trouverez à la racine de votre projet sous le nom d'AndroidManifest.xml et qui vous permettra de spécifier différentes options pour vos projets, comme le matériel nécessaire pour les faire fonctionner, certains paramètres de sécurité ou encore des informations plus ou moins triviales telles que le nom de l'application ainsi que son icône.

Mais ce n'est pas tout, c'est aussi la première étape à maîtriser afin de pouvoir insérer plusieurs activités au sein d'une même application, ce qui sera la finalité des deux prochains chapitres.

Ce chapitre se chargera aussi de vous expliquer plus en détail comment manipuler le cycle d'une activité. En effet, pour l'instant nous avons toujours tout inséré dans onCreate, mais il existe des situations pour lesquelles cette attitude n'est pas du tout la meilleure à adopter.

Généralités sur le nœud <manifest>

Ce fichier est indispensable pour tous les projets Android, c'est pourquoi il est créé par défaut. Si je crée un nouveau projet, voici le Manifest qui est généré :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="sdz.chapitreTrois"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk android:minSdkVersion="7" />

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >

    <activity
      android:name=".ManifestActivity"
      android:label="@string/app_name" >

      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

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

    </activity>

  </application>

</manifest>

Voyons un petit peu de quoi il s'agit ici.

<manifest>

La racine du Manifest est un nœud de type <manifest>. Comme pour les vues et les autres ressources, on commence par montrer qu'on utilise l'espace de noms android :

1
xmlns:android="http://schemas.android.com/apk/res/android"

Puis, on déclare dans quel package se trouve notre application :

1
package="sdz.chapitreTrois"

… afin de pouvoir utiliser directement les classes qui se situent dans ce package sans avoir à préciser à chaque fois qu'elles s'y situent. Par exemple, dans notre Manifest actuel, vous pouvez voir la ligne suivante : android:name=".ManifestActivity". Elle fait référence à l'activité principale de mon projet : ManifestActivity. Cependant, si nous n'avions pas précisé package="sdz.chapitreTrois", alors le nœud android:name aurait dû valoir android:name="sdz.chapitreTrois.ManifestActivity". Imaginez seulement que nous ayons à le faire pour chaque activité de notre application… Cela risquerait d'être vite usant.

N'oubliez pas que le nom de package doit être unique si vous voulez être publié sur le Play Store. De plus, une fois votre application diffusée, ne changez pas ce nom puisqu'il agira comme un identifiant unique pour votre application.

Toujours dans le nœud <manifest>, il est ensuite possible d'indiquer quelle est la version actuelle du logiciel :

1
2
android:versionCode="1"
android:versionName="1.0"

L'attribut android:versionCode doit être un nombre entier (positif et sans virgule) qui indique quelle est la version actuelle de l'application. Mais attention, il ne s'agit pas du nombre qui sera montré à l'utilisateur, juste celui considéré par le Play Store. Si vous soumettez votre application avec un code de version supérieur à celui de votre ancienne soumission, alors le Play Store saura que l'application a été mise à jour. En revanche, le android:versionName peut être n'importe quelle chaîne de caractères et sera montré à l'utilisateur. Rien ne vous empêche donc de mettre android:versionName="Première version alpha - 0.01a" par exemple.

<uses-sdk>

On utilise ce nœud de manière à pouvoir filtrer les périphériques sur lesquels l'application est censée fonctionner en fonction de leur version d'Android. Ainsi, il vous est possible d'indiquer la version minimale de l'API que doit utiliser le périphérique :

1
<uses-sdk android:minSdkVersion="7" />

Ici, il faudra la version 2.1 d'Android (API 7) ou supérieure pour pouvoir utiliser cette application.

Votre application ne sera proposée sur le Google Play qu'à partir du moment où l'utilisateur utilise cette version d'Android ou une version supérieure.

Il existe aussi un attribut android:targetSdkVersion qui désigne non pas la version minimale d'Android demandée, mais plutôt la version à partir de laquelle on pourra exploiter à fond l'application. Ainsi, vous avez peut-être implémenté des fonctionnalités qui ne sont disponibles qu'à partir de versions d'Android plus récentes que la version minimale renseignée avec android:minSdkVersion, tout en faisant en sorte que l'application soit fonctionnelle en utilisant une version d'Android égale ou supérieure à celle précisée dans android:minSdkVersion.

Vous pouvez aussi préciser une limite maximale à respecter avec android:maxSdkVersion si vous savez que votre application ne fonctionne pas sur les plateformes les plus récentes, mais je ne vous le conseille pas, essayez plutôt de rendre votre application compatible avec le plus grand nombre de terminaux !

<uses-feature>

Étant donné qu'Android est destiné à une très grande variété de terminaux différents, il fallait un moyen pour faire en sorte que les applications qui utilisent certains aspects hardware ne puissent être proposées que pour les téléphones qui possèdent ces capacités techniques. Par exemple, vous n'allez pas proposer un logiciel pour faire des photographies à un téléphone qui ne possède pas d'appareil photo (même si c'est rare) ou de capture sonore pour une tablette qui n'a pas de microphone (ce qui est déjà moins rare).

Ce nœud peut prendre trois attributs, mais je n'en présenterai que deux :

1
2
3
<uses-feature 
  android:name="material"
  android:required="boolean" />

android:name

Vous pouvez préciser le nom du matériel avec l'attribut android:name. Par exemple, pour l'appareil photo (et donc la caméra), on mettra :

1
<uses-feature android:name="android.hardware.camera" />

Cependant, il arrive qu'on ne cherche pas uniquement un composant particulier mais une fonctionnalité de ce composant ; par exemple pour permettre à l'application de n'être utilisée que sur les périphériques qui ont un appareil photo avec autofocus, on utilisera :

1
<uses-feature android:name="android.hardware.camera.autofocus" />

Vous trouverez sur cette page la liste exhaustive des valeurs que peut prendre android:name.

android:required

Comme cet attribut n'accepte qu'un booléen, il ne peut prendre que deux valeurs :

  1. true : ce composant est indispensable pour l'utilisation de l'application.
  2. false : ce composant n'est pas indispensable, mais sa présence est recommandée.

<supports-screens>

Celui-ci est très important, il vous permet de définir quels types d'écran supportent votre application. Cet attribut se présente ainsi :

1
2
3
<supports-screens android:smallScreens="boolean"
                  android:normalScreens="boolean"
                  android:largeScreens="boolean" />

Si un écran est considéré comme petit, alors il entrera dans la catégorie smallScreen, s'il est moyen, c'est un normalScreen, et les grands écrans sont des largeScreen.

L'API 9 a vu l'apparition de l'attribut <supports-screens android:xlargeScreens="boolean" />. Si cet attribut n'est pas défini, alors votre application sera lancée avec un mode de compatibilité qui agrandira tous les éléments graphiques de votre application, un peu comme si on faisait un zoom sur une image. Le résultat sera plus laid que si vous développiez une interface graphique dédiée, mais au moins votre application fonctionnera.

</supports-screens></uses-feature></uses-sdk></manifest>

Le nœud <application>

Le nœud le plus important peut-être. Il décrit les attributs qui caractérisent votre application et en énumère les composants de votre application. Par défaut, votre application n'a qu'un composant, l'activité principale. Mais voyons d'abord les attributs de ce nœud.

Vous pouvez définir l'icône de votre application avec android:icon, pour cela vous devez faire référence à une ressource drawable : android:icon="@drawable/ic_launcher".

Ne confondez pas avec android:logo qui, lui, permet de changer l'icône uniquement dans l'ActionBar.

Il existe aussi un attribut android:label qui permet de définir le nom de notre application.

Les thèmes

Vous savez déjà comment appliquer un style à plusieurs vues pour qu'elles respectent les mêmes attributs. Et si nous voulions que toutes nos vues respectent un même style au sein d'une application ? Que les textes de toutes ces vues restent noirs par exemple ! Ce serait contraignant d'appliquer le style à chaque vue. C'est pourquoi il est possible d'appliquer un style à une application, auquel cas on l'appelle un thème. Cette opération se déroule dans le Manifest, il vous suffit d'insérer l'attribut :

1
android:theme="@style/blackText"

Vous pouvez aussi exploiter les thèmes par défaut fournis par Android, par exemple pour que votre application ressemble à une boîte de dialogue :

1
<activity android:theme="@android:style/Theme.NoTitleBar">

Vous en retrouverez d'autres sur la documentation.

Laissez-moi maintenant vous parler de la notion de composants. Ce sont les éléments qui composeront vos projets. Il en existe cinq types, mais vous ne connaissez pour l'instant que les activités, alors je ne vais pas vous embrouiller plus avec ça. Sachez juste que votre application sera au final un ensemble de composants qui interagissent, entre eux et avec le reste du système.

<activity>

Ce nœud permet de décrire toutes les activités contenues dans notre application. Comme je vous l'ai déjà dit, une activité correspond à un écran de votre application, donc, si vous voulez avoir plusieurs écrans, il vous faudra plusieurs activités.

Le seul attribut vraiment indispensable ici est android:name, qui indique quelle est la classe qui implémente l'activité.

android:name définit aussi un identifiant pour Android qui permet de repérer ce composant parmi tous. Ainsi, ne changez pas l'attribut android:name d'un composant au cours d'une mise à jour, sinon vous risquez de rencontrer des effets de bord assez désastreux.

Vous pouvez aussi préciser un nom pour chaque activité avec android:label, c'est le mot qui s'affichera en haut de l'écran sur notre activité. Si vous ne le faites pas, c'est la String renseignée dans le android:label du nœud <application> qui sera utilisée.

Vous pouvez voir un autre nœud de type <intent-filter> qui indique comment se lancera cette activité. Pour l'instant, sachez juste que l'activité qui sera lancée depuis le menu principal d'Android contiendra toujours dans son Manifest ces lignes-ci :

1
2
3
4
<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Je vous donnerai beaucoup plus de détails dans le prochain chapitre.

Vous pouvez aussi définir un thème pour une activité, comme nous l'avons fait pour une application.</activity>

Les permissions

Vous le savez sûrement, quand vous téléchargez une application sur le Play Store, on vous propose de regarder les autorisations que demande cette application avant de commencer le téléchargement (voir figure suivante). Par exemple, pour une application qui vous permet de retenir votre numéro de carte bancaire, on peut légitimement se poser la question de savoir si dans ses autorisations se trouve « Accès à internet ».

On vous montre les autorisations que demande l'application avant de la télécharger

Par défaut, aucune application ne peut exécuter d'opération qui puisse nuire aux autres applications, au système d'exploitation ou à l'utilisateur. Cependant, Android est constitué de manière à ce que les applications puissent partager. C'est le rôle des permissions, elles permettent de limiter l'accès aux composants de vos applications.

Utiliser les permissions

Afin de pouvoir utiliser certaines API d'Android, comme l'accès à internet dans le cas précédent, vous devez préciser dans le Manifest que vous utilisez les permissions. Ainsi, l'utilisateur final est averti de ce que vous souhaitez faire, c'est une mesure de protection importante à laquelle vous devez vous soumettre.

Vous trouverez sur cette page une liste des permissions qui existent déjà.

Ainsi, pour demander un accès à internet, on indiquera la ligne :

1
<uses-permission android:name="android.permission.INTERNET" />

Créer ses permissions

Il est important que votre application puisse elle aussi partager ses composants puisque c'est comme ça qu'elle sait se rendre indispensable.

Une permission doit être de cette forme-ci :

1
2
3
4
5
<permission android:name="string"
            android:label="string resource"
            android:description="string resource"
            android:icon="drawable resource"
            android:protectionLevel=XXX />

Où :

  • android:name est le nom qui sera utilisé dans un uses-permission pour faire référence à cette permission. Ce nom doit être unique.
  • android:label est le nom qui sera indiqué à l'utilisateur, faites-en donc un assez explicite.
  • android:description est une description plus complète de cette permission que le label.
  • android:icon est assez explicite, il s'agit d'une icône qui est censée représenter la permission. Cet attribut est heureusement facultatif.
  • android:protectionLevel indique le degré de risque du composant lié à cette permission. Il existe principalement trois valeurs :
    • normal si le composant est sûr et ne risque pas de dégrader le comportement du téléphone ;
    • dangerous si le composant a accès à des données sensibles ou peut dégrader le fonctionnement du périphérique ;
    • signature pour n'autoriser l'utilisation du composant que par les produits du même auteur (concrètement, pour qu'une application se lance sur votre terminal ou sur l'émulateur, il faut que votre application soit « approuvée » par Android. Pour ce faire, un certificat est généré — même si vous ne le demandez pas, l'ADT s'en occupe automatiquement — et il faut que les certificats des deux applications soient identiques pour que la permission soit acceptée).

Gérer correctement le cycle des activités

Comme nous l'avons vu, quand un utilisateur manipule votre application, la quitte pour une autre ou y revient, elle traverse plusieurs états symbolisés par le cycle de vie des activités, schématisé à la figure suivante.

Le cycle de vie d'une activité

La transition entre chaque étape implique que votre application appelle une méthode. Cette méthode partage le même nom que l'état traversé, par exemple à la création est appelée la méthode onCreate.

Ces méthodes ne sont que des états de transition très éphémères entre les trois grands états dont nous avons déjà discuté : la période active peut être interrompue par la période suspendue, qui elle aussi peut être interrompue par la période arrêtée.

Vous le comprendrez très vite, l'entrée dans chaque état est symbolisée par une méthode et la sortie de chaque état est symbolisée par une autre méthode. Ces deux méthodes sont complémentaires : ce qui est initialisé dans la première méthode doit être stoppé dans la deuxième, et ce presque toujours.

Sous les feux de la rampe : période suspendue

Cette période débute avec onResume() et se termine avec onPause(). On entre dans la période suspendue dès que votre activité n'est plus que partiellement visible, comme quand elle est voilée par une boîte de dialogue. Comme cette période arrive fréquemment, il faut que le contenu de ces deux méthodes s'exécute rapidement et nécessite peu de processeur.

onPause()

On utilise la méthode onPause() pour arrêter des animations, libérer des ressources telles que le GPS ou la caméra, arrêter des tâches en arrière-plan et de manière générale stopper toute activité qui pourrait solliciter le processeur. Attention, on évite de sauvegarder dans la base de données au sein de onPause(), car c'est une action qui prend beaucoup de temps à se faire, et il vaut mieux que onPause() s'exécute rapidement pour fluidifier la manipulation par l'utilisateur. De manière générale, il n'est pas nécessaire de sauvegarder des données dans cette méthode, puisque Android conserve une copie fonctionnelle de l'activité, et qu'au retour elle sera restaurée telle quelle.

onResume()

Cette méthode est exécutée à chaque fois que notre activité retourne au premier plan, mais aussi à chaque lancement, c'est pourquoi on l'utilise pour initialiser les ressources qui seront coupées dans le onPause(). Par exemple, dans votre application de localisation GPS, vous allez initialiser le GPS à la création de l'activité, mais pas dans le onCreate(Bundle), plutôt dans le onResume() puisque vous allez le couper à chaque fois que vous passez dans le onPause().

Convoquer le plan et l'arrière-plan : période arrêtée

Cette fois-ci, votre activité n'est plus visible du tout, mais elle n'est pas arrêtée non plus. C'est le cas si l'utilisateur passe de votre application à une autre (par exemple s'il retourne sur l'écran d'accueil), alors l'activité en cours se trouvera stoppée et on reprendra avec cette activité dès que l'utilisateur retournera dans l'application. Il est aussi probable que dans votre application vous ayez plus d'une activité, et passer d'une activité à l'autre implique que l'ancienne s'arrête.

Cet état est délimité par onStop() (toujours précédé de onPause()) et onRestart() (toujours suivi de onResume(), puis onStart())). Cependant, il se peut que l'application soit tuée par Android s'il a besoin de mémoire, auquel cas, après onStop(), l'application est arrêtée et, quand elle sera redémarrée, on reprendra à onCreate(Bundle).

Là, en revanche, vous devriez sauvegarder les éléments dans votre base de données dans onStop() et les restaurer lorsque c'est nécessaire (dans onStart() si la restauration doit se faire au démarrage et après un onStop() ou dans onResume() si la restauration ne doit se faire qu'après un onStop()).

De la naissance à la mort

onCreate(Bundle)

Première méthode qui est lancée au démarrage de l'activité, c'est l'endroit privilégié pour initialiser l'interface graphique, pour démarrer les tâches d'arrière-plan qui s'exécuteront pendant toute la durée de vie de l'activité, pour récupérer des éléments de la base de données, etc.

Il se peut très bien que vous utilisiez une activité uniquement pour faire des calculs et prendre des décisions entre l'exécution de deux activités, auquel cas vous pouvez faire appel à la méthode public void finish () pour passer directement à la méthode onDestroy(), qui symbolise la mort de l'activité. Notez bien qu'il s'agit du seul cas où il est recommandé d'utiliser la méthode finish() (c'est-à-dire qu'on évite d'ajouter un bouton pour arrêter son application par exemple).

onDestroy()

Il existe trois raisons pour lesquelles votre application peut atteindre la méthode onDestroy, c'est-à-dire pour lesquelles on va terminer notre application :

  • Comme je l'ai déjà expliqué, il peut arriver que le téléphone manque de mémoire et qu'il ait besoin d'arrêter votre application pour en récupérer.
  • Si l'utilisateur presse le bouton Arrière et que l'activité actuelle ne permet pas de retourner à l'activité précédente (ou s'il n'y en a pas !), alors on va quitter l'application.
  • Enfin, si vous faites appel à la méthode public void finish() — mais on évite de l'utiliser en général —, il vaut mieux laisser l'utilisateur appuyer sur le bouton Arrière, parce qu'Android est conçu pour gérer le cycle des activités tout seul (c'est d'ailleurs la raison pour laquelle il faut éviter d'utiliser des task killers).

Comme vous le savez, c'est dans onCreate(Bundle) que doivent être effectuées les différentes initialisations ainsi que les tâches d'arrière-plan qu'on souhaite voir s'exécuter pendant toute l'application. Normalement, quand l'application arrive au onDestroy(), elle est déjà passée par onPause() et onStop(), donc la majorité des tâches de fond auront été arrêtées ; cependant, s'il s'agit d'une tâche qui devrait s'exécuter pendant toute la vie de l'activité — qui aura été démarrée dans onCreate(Bundle) —, alors c'est dans onDestroy() qu'il faudra l'arrêter.

Android passera d'abord par onPause() et onStop() dans tous les cas, à l'exception de l'éventualité où vous appeleriez la méthode finish() ! Si c'est le cas, on passe directement au onDestroy(). Heureusement, il vous est possible de savoir si le onDestroy() a été appelé suite à un finish() avec la méthode public boolean isFinishing().

Si à un moment quelconque votre application lance une exception que vous ne catchez pas, alors l'application sera détruite sans passer par le onDestroy(), c'est pourquoi cette méthode n'est pas un endroit privilégié pour sauvegarder des données.

L'échange équivalent

Quand votre application est quittée de manière normale, par exemple si l'utilisateur presse le bouton Arrière ou qu'elle est encore ouverte et que l'utilisateur ne l'a plus consultée depuis longtemps, alors Android ne garde pas en mémoire de traces de vos activités, puisque l'application s'est arrêtée correctement. En revanche, si Android a dû tuer le processus, alors il va garder en mémoire une trace de vos activités afin de pouvoir les restaurer telles quelles. Ainsi, au prochain lancement de l'application, le paramètre de type Bundle de la méthode onCreate(Bundle) sera peuplé d'informations enregistrées sur l'état des vues de l'interface graphique.

Mais il peut arriver que vous ayez besoin de retenir d'autres informations qui, elles, ne sont pas sauvegardées par défaut. Heureusement, il existe une méthode qui est appelée à chaque fois qu'il y a des chances pour que l'activité soit tuée. Cette méthode s'appelle protected void onSaveInstanceState(Bundle outState).

Un objet de type Bundle est l'équivalent d'une table de hachage qui à une chaîne de caractères associe un élément, mais seulement pour certains types précis. Vous pouvez voir dans la documentation tous les types qu'il est possible d'insérer. Par exemple, on peut insérer un entier et le récupérer à l'aide de :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private final static RESULTAT_DU_CALCUL = 0;

@Override
protected void onSaveInstanceState (Bundle bundle)
{
  super.onSaveInstanceState(bundle);
  bundle.putInt(RESULTAT_DU_CALCUL, 10);
  /*
   * On pourra le récupérer plus tard avec
   * int resultat = bundle.getInt(RESULTAT_DU_CALCUL);
  */
}

On ne peut pas mettre n'importe quel objet dans un Bundle, uniquement des objets sérialisables. La sérialisation est le procédé qui convertit un objet en un format qui peut être stocké (par exemple dans un fichier ou transmis sur un réseau) et ensuite reconstitué de manière parfaite. Vous faites le rapprochement avec la sérialisation d'une vue en XML, n'est-ce pas ?

En ce qui concerne Android, on n'utilise pas la sérialisation standard de Java, avec l'interface Java.io.Serializable, parce que ce processus est trop lent. Or, quand nous essayons de faire communiquer des composants, il faut que l'opération se fasse de manière rapide. C'est pourquoi on utilise un système différent que nous aborderons en détail dans le prochain chapitre.

Cependant, l'implémentation par défaut de onSaveInstanceState(Bundle) ne sauvegarde pas toutes les vues, juste celles qui possèdent un identifiant ainsi que la vue qui a le focus, alors n'oubliez pas de faire appel à super.onSaveInstanceState(Bundle) pour vous simplifier la vie.

Par la suite, cet objet Bundle sera passé à onCreate(Bundle), mais vous pouvez aussi choisir de redéfinir la méthode onRestoreInstanceState(Bundle), qui est appelée après onStart() et qui recevra le même objet Bundle.

L'implémentation par défaut de onRestoreInstanceState(Bundle) restaure les vues sauvegardées par l'implémentation par défaut de onSaveInstanceState ().

La figure suivante est un schéma qui vous permettra de mieux comprendre tous ces imbroglios.

Le cycle de sauvegarde de l'état d'une activité

Gérer le changement de configuration

Il se peut que la configuration de votre utilisateur change pendant qu'il utilise son terminal. Vous allez dire que je suis fou, mais un changement de configuration correspond simplement à ce qui pourrait contribuer à un changement d'interface graphique. Vous vous rappelez les quantificateurs ? Eh bien, si l'un de ces quantificateurs change, alors on dit que la configuration change.

Et ça vous est déjà arrivé, j'en suis sûr. Réfléchissez ! Si l'utilisateur passe de paysage à portrait dans l'un de nos anciens projets, alors il change de configuration et par conséquent d'interface graphique.

Par défaut, dès qu'un changement qui pourrait changer les ressources utilisées par une application se produit, Android détruit tout simplement la ou les activités pour les recréer ensuite. Heureusement pour vous, Android va retenir les informations des widgets qui possèdent un identifiant. Dans une application très simple, on va créer un layout par défaut :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >

  <EditText
    android:id="@+id/editText"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

  </EditText>

  <EditText
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />

</LinearLayout>

Seul un de ces EditText possède un identifiant. Ensuite, on fait un layout presque similaire, mais avec un quantificateur pour qu'il ne s'affiche qu'en mode paysage :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >

  <EditText
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

  </EditText>

  <Button
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    />

  <EditText
    android:id="@+id/editText"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />

</LinearLayout>

Remarquez bien que l'identifiant de l'EditText est passé à l'EditText du bas. Ainsi, quand vous lancez votre application, écrivez du texte dans les deux champs, comme à la figure suivante.

Écrivez du texte dans les deux champs

Puis tournez votre terminal (avant qu'un petit malin ne casse son ordinateur en le mettant sur le côté : pour faire pivoter l'émulateur, c'est CTRL + F12 ou F11) et admirez le résultat, identique à la figure suivante.

Il y a perte d'information !

Vous voyez bien que le widget qui avait un identifiant a conservé son texte, mais pas l'autre ! Cela prouve bien qu'il peut y avoir perte d'information dès qu'un changement de configuration se produit.

Bien entendu, dans le cas des widgets, le problème est vite résolu puisqu'il suffit de leur ajouter un identifiant, mais il existe des informations à retenir en dehors des widgets. Alors, comment gérer ces problèmes-là ? Comme par défaut Android va détruire puis recréer les activités, vous pouvez très bien tout enregistrer dans la méthode onSaveInstanceState(), puis tout restaurer dans onCreate(Bundle) ou onRestoreInstanceState(). Mais il existe un problème ! Vous ne pouvez passer que les objets sérialisables dans un Bundle. Alors comment faire ?

Il existe trois façons de faire :

  • Utiliser une méthode alternative pour retenir vos objets, qui est spécifique aux changements de configuration.
  • Gérer vous-mêmes les changements de configuration, auquel cas Android ne s'en chargera plus. Et comme cette technique est un peu risquée, je ne vais pas vous la présenter.
  • Bloquer le changement de ressources.

Retenir l'état de l'activité

Donc, le problème avec Bundle, c'est qu'il ne peut pas contenir de gros objets et qu'en plus la sérialisation et la désérialisation sont des processus lents, alors que nous souhaiterions que la transition entre deux configurations soit fluide. C'est pourquoi nous allons faire appel à une autre méthode qui est appelée cette fois uniquement en cas de changement de configuration : public Object onRetainNonConfigurationInstance(). L'objet retourné peut être de n'importe quel ordre, vous pouvez même retourner directement une instance de l'activité si vous le souhaitez (mais bon, ne le faites pas).

Notez par ailleurs que onRetainNonConfigurationInstance() est appelée après onStop() mais avant onDestroy() et que vous feriez mieux de ne pas conserver des objets qui dépendent de la configuration (par exemple des chaînes de caractères qui changent en fonction de la langue) ou des objets qui sont liés à l'activité (un Adapter par exemple).

Ainsi, une des façons de procéder est de créer une classe spécialement dédiée à la détention de ces informations :

1
2
3
4
5
6
7
8
@Override
public Object onRetainNonConfigurationInstance() {
  // La classe « DonneesConservees » permet de contenir tous les objets voulus
  // Et la méthode "constituerDonnees" va construire un objet
  // En fonction de ce que devra savoir la nouvelle instance de l'activité
  DonneesConservees data = constituerDonnees();
  return data;
}

Enfin, il est possible de récupérer cet objet dans le onCreate(Bundle) à l'aide de la méthode public Object getLastNonConfigurationInstance() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  DonneesConservees data = (DonneesConservees) getLastNonConfigurationInstance();
  // S'il ne s'agit pas d'un retour depuis un changement de configuration, alors data est null
  if(data == null)
    
}

Empêcher le changement de ressources

De toute façon, il arrive parfois qu'une application n'ait de sens que dans une orientation. Pour lire un livre, il vaut mieux rester toujours en orientation portrait par exemple, de même il est plus agréable de regarder un film en mode paysage. L'idée ici est donc de conserver des fichiers de ressources spécifiques à une configuration, même si celle du terminal change en cours d'utilisation.

Pour ce faire, c'est très simple, il suffit de rajouter dans le nœud des composants concernés les lignes android:screenOrientation = "portrait" pour bloquer en mode portrait ou android:screenOrientation = "landscape" pour bloquer en mode paysage. Bon, le problème, c'est qu'Android va quand même détruire l'activité pour la recréer si on laisse ça comme ça, c'est pourquoi on va lui dire qu'on gère nous-mêmes les changements d'orientation en ajoutant la ligne android:configChanges="orientation" dans les nœuds concernés :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="fr.sdz.configuration.change"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk
    android:minSdkVersion="7"
    android:targetSdkVersion="15" />

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
      android:name=".MainActivity"
      android:label="@string/title_activity_main"
      android:configChanges="orientation"
      android:screenOrientation="portrait" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>

Voilà, maintenant vous aurez beau tourner le terminal dans tous les sens, l'application restera toujours orientée de la même manière.


  • Le fichier Manifest est indispensable à tous les projets Android. C'est lui qui déclarera toute une série d'informations sur votre application.
  • Le nœud <application> listera les différents composants de votre application ainsi que les services qu'ils offrent.
  • Vous pouvez signaler que vous utiliserez des permissions par l'élément uses-permission ou que vous en créez par l'élément permission.
  • Comprendre le cycle de vie d'une activité est essentiel pour construire des activités robustes et ergonomiques.
  • Les terminaux peuvent se tenir en mode paysage ou en mode portrait. Vous vous devez de gérer un minimum ce changement de configuration puisqu'au basculement de l'appareil, votre système reconstruira toute votre interface et perdra par conséquent les données que l'utilisateur aurait pu saisir.