Développement sous Android

Chapitres traités   

Lors de l'étude précédente, nous avons passé pas mal de temps à découvrir toute la philosophie ainsi que l'architecture globale d'un environnement sous Android en découvrant, les principales ressources, des widgets de base et plus sophistiqués ainsi qu'un gros chapitre sur la disposition de ces composants au moyen de layouts adaptés.

Je vous propose maintenant d'aller un peu plus loin et de connaître plus en détail les notions que nous avons laissé en suspend, comme la gestion des listes, la création de nouveaux composants, les notifications, gérer plusieurs activités dans la même application, les intentions et la persistance des données.

Par contre, le partage des données, les services, la communication réseau, la prise en compte des capteurs intégrés, la géolocalisation, etc. seront traités lors de la prochaine étude.

Lors de l'étude précédente, nous avons surtout travaillé sur la vue, c'est-à-dire nous avons passé beaucoup de temps sur les fichiers de description XML. Lors de cette étude, nous passerons cette fois-ci plus de temps sur le codage Java lui-même.

Choix du chapitre Les différents types de sélection

Lors de l'étude précédente, nous avons découvert que les champs de saisie pouvaient imposer des contraintes sur leur contenu possible. Ce type de contrainte aide l'utilisateur à faire ce qu'il faut lorsqu'il entre des informations, notamment lorsqu'il s'agit d'un terminal mobile avec un clavier exigu.

La contrainte de saisie ultime consiste, évidemment, à ne proposer qu'une option possible parmi un ensemble de choix, ce qui peut être réalisé à l'aide des boutons radio que nous avons déjà présentés. Android dispose également de listes déroulantes avec en plus des widgets particulièrement adaptés aux dispositifs mobiles (Gallery, par exemple, permet d'exéminer les photographies stockées sur le terminal).

En outre, Android permet de connaître aisément les choix qui sont proposés dans ces widgets. Plus précisément, il dispose d'un ensemble d'adaptateurs permettant de fournir une interface commune à toutes les listes de choix, que ce soitent des tableaux statiques ou des bases de données. Les vues de sélection - les widgets pour présenter les listes de choix - sont transmises à un adaptateur pour fournir les choix possibles.

Les adaptateurs

Dans l'absolu, les adaptateurs offrent une interface commune pour différents API. Dans le cas d'Android, ils fournissent une interface commune au modèle de données sous-jacent d'un widget de sélection comme une liste déroulante.

Les adaptateurs d'Android se chargent de fournir la liste des données d'un widget de sélection et de convertir les différents éléments en vues spécifiques pour qu'elles s'affichent dans ce widget de sélection.

L'adaptateur le plus simple est ArrayAdapter puisqu'il suffit d'encapsuler un tableau ou une instance de java.util.List pour disposer d'un adaptateur prêt à fonctionner. Le constructeur d'ArrayAdapter attend trois paramètres :

  1. Le contexte d'utilisation (généralement, il s'agit de l'instance de l'activité).
  2. L'identifiant de ressource de la vue à utiliser.
  3. Le tableau ou la liste d'éléments à afficher.

Par défaut, ArrayAdapter appelera la méthode toString() des objets de la liste et enveloppera chaque chaine ainsi obtenue dans la vue désignée par la ressource indiquée. Voici d'autres adaptateurs dont vous aurez certainement besoin :

  1. CursorAdapter : convertit un Cursor, généralement fourni par un content provider, en un objet pouvant s'afficher dans la vue de sélection.
  2. SimpleAdapter : convertit les données trouvées dans les ressources XML.

Les listes

Le widget classique d'Android pour les listes s'appelle ListView. Pour disposer d'une liste complète fonctionnelle, il suffit d'inclure un objet ListView dans votre présentation, d'appeler setAdapter() pour fournir les données et les vues filles, puis d'attacher un écouteur de type OnItemSelectedListener afin d'être prévénu de toute modification de la sélection.

Cependant, si votre activité est pilotée par une seule liste, il peut être préférable que cette activité soit une sous-classe de ListActivity plutôt que de la classe de base Activity traditionnelle. Si votre vue principale est uniquement constituée de la liste, vous n'avez même pas besoin de fournir de layout - ListActivity construira pour vous une liste qui occupera tout l'écran.

Vous pouvez toutefois personnaliser cette présentation à condition d'identifier cette ListView par @android:id/list afin que ListActivity sache quelle est la liste principale de l'activité.

Afin de valider ces différentes notions, je vous propose de réaliser un projet qui permet de choisir un jour de la semaine. La vue montre simplement une liste surmontée d'un label qui devra afficher en permanence la sélection courante :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <ListView
android:id="@android:id/list" android:layout_width="fill_parent"
android:layout_height="fill_parent" /> </LinearLayout>

Vu que nous allons prendre une activité de type ListActivity, la vue est vraiment très simple à mettre en oeuvre. En réalité, nous passons beaucoup plus de temps à régler le TextView par rapport au ListView.

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends ListActivity {
private TextView selection;
private String[] semaine = {"Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, semaine));
selection = (TextView) findViewById(R.id.selection);
}

@Override protected void onListItemClick(ListView l, View v, int position, long id) {
selection.setText(semaine[position]);
}
}
  1. Vous pouvez configurer l'adaptateur d'une ListActivity par un appel à setListAdapter() - ici, nous fournissons un ArrayAdapter qui enveloppe un tableau de chaînes quelconques.
  2. Afin d'être prévenu des changements dans la liste de sélection, nous redéfinissons onListItemClick() pour qu'elle agisse de façon appropriée en tenant compte de la vue fille et de la position qui lui sont passée en paramètre.
  3. Le second paramètre android.R.layout.simple_list_item_1 (prédéfini par Android) de notre ArrayAdapter - contrôle l'aspect des lignes. La constante utilisée ici fournit une ligne Android standard : grande police, remplissage important et texte en blanc.
Proposer d'autres vues avec la même liste

Par défaut, ListView est simplement configurée pour recevoir les clics sur les entrées de la liste. Cependant, nous avons parfois besoin d'une liste qui mémorise un ou plusieurs choix de l'utilisateur ; ListView permet également de le faire, au prix de quelques modifications :

  1. Dans le code Java, appelez la méthode setChoiceMode() de l'objet ListView afin de configurer le mode de sélection en lui passant en paramètre la constante CHOICE_MODE_SINGLE ou CHOICE_MODE_MULTIPLE (pour obtenir l'objet ListView, il suffit d'appeler la méthode getListView() à partir d'une ListActivity).
  2. Puis, au lieu de passer en paramètre android.R.layout.simple_list_item_1 au constructeur d'ArrayAdapter, utilisez soit android.R.layout.simple_list_item_single_choice, soit android.R.layout.simple_list_item_multiple_choice pour mettre en place, respectivement, une liste à choix unique ou à choix multiple.
  3. Pour connaître quel est l'élément choisi par l'utilisateur, appelez la méthode getCheckedItemPositions() du widget ListView.
fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends ListActivity {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<Semaine>(this, android.R.layout.simple_list_item_single_choice, Semaine.values()));
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
selection = (TextView) findViewById(R.id.selection);
}

@Override protected void onListItemClick(ListView l, View v, int position, long id) {
Semaine semaine = (Semaine) getListAdapter().getItem(position);
selection.setText(semaine.name());
}
}

Dans le code Java ci-dessus, je précise cette fois-ci que le choix effectué sera unique. Au niveau de la vue, cela se traduit par une liste de bouton radio. Cela n'était pas indispensable, mais j'ai profité de l'occasion pour implémenter une énumération (un seul choix parmi plusieurs possibles). Nous n'avons absolument rien modifié du côté du layout (fichier de description XML).

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends ListActivity {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<Semaine>(this, android.R.layout.simple_list_item_multiple_choice, Semaine.values()));
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
selection = (TextView) findViewById(R.id.selection);
}

@Override protected void onListItemClick(ListView liste, View v, int position, long id) {
String resultat = "";
Semaine semaine;
SparseBooleanArray tableau = liste.getCheckedItemPositions();
for (int i=0; i<tableau.size(); i++) {
if (tableau.valueAt(i)) {
semaine = (Semaine) getListAdapter().getItem(tableau.keyAt(i));
resultat += semaine.name() + " ";
}
}
selection.setText("["+resultat+"]");
}
}

  1. La méthode getCheckedItemPositions() retourne un tableau particulier représenté par la classe SparseBooleanArray.
  2. Il s'agit en réalité d'une carte correspondant à l'ensemble des éléments déjà sélectionnés.
  3. Cette carte est une relation entre une valeur entière correspondant à l'indice de la position de l'élément dans la ListView avec une valeur booléenne qui précise si cet élément est actuellement sélectionné.
  4. Attention, il est possible d'avoir sélectionné dans un premier temps une valeur et ensuite de l'avoir inhiber. Cette carte mémorise tous les éléments qui ont déjà étaient pris en compte.
  5. Ainsi, avant de récupérer l'indice de l'élément, au moyen de la méthode keyAt(), vous devez évaluer si cet élément est actuellement sélectionné, au moyen de la méthode valueAt().

Listes déroulantes

Android propose également des listes déroulantes, à l'image de JComboBox en Java/Swing, nommée Spinner. Lorsque l'utilisateur agit sur cet élément, le terminal fait surgir une boîte de sélection permettant à l'utilisateur de faire son choix.

Grâce à ce système, nous pouvons réaliser un choix sans occuper tout l'écran comme avec une ListView, mais au prix d'un clic supplémentaire ou d'un pointage sur l'écran.

Comme pour ListView, nous fournissons l'adaptateur pour les données et les vues filles via setAdapter() et nous accrochons un écouteur de type OnItemSelectedListener.

Si vous souhaitez personnaliser la vue d'affichage de la boîte déroulante, il est nécessaire de configurer l'adaptateur, pas le widget Spinner. Pour ce faire, nous avons donc besoin de la méthode setDropDownViewRessource() afin de fournir l'identifiant de la vue concernée.

Je vous propose de reprendre le projet précédent en prenant cette fois-ci une liste déroulante pour réaliser le choix :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" /> <Spinner
android:id="@+id/choix" android:layout_width="fill_parent"
android:layout_height="wrap_content" /> </LinearLayout>
  1. Nous retrouvons pratiquement la même vue que précédemment avec toutefois le remplacement de la balise <ListView> par la balise <Spinner>.
  2. Une autre différence importante concerne l'identification. Cette fois-ci l'activité est représenté de façon plus classique par la classe Activity et non plus ListActivity (cela n'a plus aucun sens avec la classe Spinner).
  3. Du coup, il faut également remplacer @android/list par une identification classique et personnalisé, ici @+id/choix.
fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends Activity implements AdapterView.OnItemSelectedListener {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
Spinner choix = (Spinner) findViewById(R.id.choix);
ArrayAdapter<Semaine> semaine = new ArrayAdapter<Semaine>(this, android.R.layout.simple_spinner_item, Semaine.values());
semaine.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
choix.setOnItemSelectedListener(this);
choix.setAdapter(semaine);
}

public void onItemSelected(AdapterView<?> parent, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}

public void onNothingSelected(AdapterView<?> parent) {
selection.setText("Votre sélection ?");
}
}
  1. Comme nous nous trouvons avec une activité classique, nous devons implémenter l'interface AdapterView.OnItemSelectedListener afin de gérer l'événement relatif à la sélection.
  2. Elle possède deux méthodes spécifiques onItemSelected() et onNothingSelected() que vous complétez en conséquence.
  3. Nous configurons l'adaptateur, non seulement avec une énumération, mais également avec une ressource spécifique qui sert à la vue déroulante, au travers de la méthode setDropDownViewRessource().
  4. Vous remarquez également que nous utilisons la constante prédéfinie afin d'afficher les éléments du Spinner.

Listes sous forme de grille

Comme son nom l'indique, GridView vous offre une grille dans laquelle vous pouvez disposer vos choix. Vous avez un contrôle limité sur le nombre et la taille des colonnes ; le nombre de lignes est déterminé dynamiquement en fonction du nombre des éléments rendus disponibles par l'adaptateur fourni. Voici les propriétés qui, combinées déterminent le nombre et la taille des colonnes :

  1. android:numColumns indique le nombre de colonnes ; si la valeur est auto_fit, Android calculera ce nombre en fonction de l'espace disponible et de la valeur des autres propriétés.
  2. android:verticalSpacing et son homologue android:horizontalSpacing précisent l'espace séparant les éléments de la grille.
  3. android:stretchMode indique, pour les grilles dont la valeur d' android:numColumns est auto_fit, ce qui devra se passer lorsqu'il reste de l'espace non occupé par des colonnes ou des espaces de séparation : si la valeur est columnWidth, cet espace disponible sera pris par les colonnes ; si elle vaut spacingWidth, il sera absorbé par l'espacement entre les colonnes.

Supposons, par exemple, que l'écran fasse 320 pixels de large, que la valeur d'android:columnWidth soit 100px et celle d'android:horizontalSpacing, de 5px : trois colonnes occuperaient donc 310 pixels (trois colonnes de 100 pixels et deux séparations de 5 pixels). Si android:stretchMode vaut columnWidth, les trois colonnes s'élargissent de 3-4 pixels pour utiliser les 10 pixels restants ; si android:stretchMode vaut spacingWidth, les deux espacements s'élargiront chacun de 5 pixels pour absorber ces 10 pixels.

Pour le reste, GridView fonctionne exactement comme n'importe quel autre widget de sélection, nous utilisons setAdapter() pour fournir les données et les vues filles, nous accrochons un écouteur de type OnItemSelectedListener, etc.

Je vous propose de reprendre le projet précédent en prenant cette fois-ci une grille pour réaliser le choix :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <GridView android:id="@+id/choix" android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:horizontalSpacing="5px" android:numColumns="auto_fit" android:columnWidth="100px" android:stretchMode="columnWidth" android:gravity="center" /> </LinearLayout>
fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends Activity implements AdapterView.OnItemSelectedListener {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
GridView choix = (GridView) findViewById(R.id.choix);
choix.setOnItemSelectedListener(this);
choix.setAdapter(new ArrayAdapter<Semaine>(this, android.R.layout.simple_list_item_1, Semaine.values()));
}

public void onItemSelected(AdapterView<?> parent, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}

public void onNothingSelected(AdapterView<?> parent) {
selection.setText("Votre sélection ?");
}
}

Auto-completion

Le widget AutoCompleteTextView est une sorte d'hybride d'un EditText (champ de saisie) et d'un Spinner. Avec l'auto-completion, le texte saisi par l'utilisateur est traité comme un préfixe de filtrage : il est comparé à une liste de préfixes candidats et les différentes correspondances s'affichent dans une liste de choix qui ressemble à un Spinner. L'utilisateur peut alors continuer sa saisie (si le mot n'est pas dans la liste) ou choisir une entrée de celle-ci pour qu'elle devienne la valeur du champ.

AutoCompleteTextView étant une sous-classe de EditText, vous pouvez utiliser toutes les propriétés de cette dernière pour contrôler son aspect - la police et la couleur du texte, notamment.

Vous pouvez fournir à AutoCompleteTextView un adaptateur contenant la liste des valeurs candidates à l'aide de setAdapter() mais, comme l'utilisateur peut très bien saisir un texte qui n'est pas dans cette liste, AutoCompleteTextView ne permet pas d'utiliser les écouteurs de sélection. Il est donc préférable d'enregistrer un TextWatcher, exactement comme n'importe quel EditText, afin d'être prévenu lorsque le texte est modifié. Ce type d'événement est déclenché par une saisie manuelle ou par une sélection dans la liste des propositions.

Je vous propose de reprendre le projet précédent en prenant cette fois-ci la saisie avec auto-complétion pour réaliser le choix :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <AutoCompleteTextView android:id="@+id/choix" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:completionThreshold="1" /> </LinearLayout>

L'attribut android:completionThreshold permet de spécifier à partir de quel nombre de lettres saisies les choix automatiques, donnés par la liste déroulante, sont proposés. Ici, nous demandons à visualiser tous les choix possibles dès que la première lettre est proposée.

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.*;
import android.widget.*;

public class ListeChoix extends Activity implements TextWatcher {
private TextView selection;
private AutoCompleteTextView choix;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
choix = (AutoCompleteTextView) findViewById(R.id.choix);
choix.addTextChangedListener(this);
choix.setAdapter(new ArrayAdapter<Semaine>(this, android.R.layout.simple_dropdown_item_1line, Semaine.values()));
}

public void beforeTextChanged(CharSequence chaine, int debut, int apres, int nombre) { }

public void onTextChanged(CharSequence chaine, int debut, int fin, int nombre) {
selection.setText(choix.getText());
}

public void afterTextChanged(Editable chaine) { }
}

Cette fois-ci, notre activité implémente l'interface TextWatcher, ce qui signifie que nos méthodes de rappel doivent se nommer beforeTextChanged(), onTextChanged() et afterTextChanged(). Remarquez également le changement de la constante au niveau de l'adaptateur qui s'appelle ici android.R.layout.simple_dropdown_item_1line.

Galeries

Le widget Gallery gère également les listes, où chaque choix défile cette fois-ci selon l'axe horizontal et où l'élément sélectionné est mis en surbrillance. Sur un terminal Android, l'utilisateur peut parcourir les différents choix en faisant glisser son doigt vers la droite ou vers la gauche.

Gallery prend moins de place à l'écran que ListView, tout en montrant plusieurs choix à la fois (pour autant qu'ils soient suffisamment courts). Par rapport à Spinner, Gallery montre également plusieurs choix simultanément.

L'exemple canonique d'utilisation de Gallery consiste à parcourir une galerie de photos - l'utilisateur peut ainsi prévisualiser les vignettes correspondant à une collection de photos ou d'icônes afin d'en choisir une.

D'un point de vue du code, un objet Gallery fonctionne quasiment comme un Spinner ou un GridView. Il dispose de plusieurs propriétés :

  1. android:spacing indique le nombre de pixels séparant les différents éléments de la liste.
  2. android:spinnerSelector précise ce qui indiquera une sélection - il peut s'agir d'une référence à un objet Drawable ou une valeur RGB de la forme #AARRGGBB ou équivalente.
  3. android:drawSelectorOnTop indique si la barre de sélection (ou le Drawable) doit être dessiné avant (false) ou après (true) le dessin du fils sélectionné. Si cette propriété vaut true, assurez-vous que le sélecteur soit suffisamment transparent pour que nous puissions appercevoir le fils derrière lui ; sinon les utilisateurs ne pourront pas voir ce qu'ils ont choisi.
Encore une dernière fois, reprenons le projet précédent en prenant cette fois-ci la sélection sous forme de galerie :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <Gallery android:id="@+id/choix" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:spacing="10px" /> </LinearLayout>
fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.*;
import android.widget.*;
import android.widget.AdapterView.OnItemClickListener;

public class ListeChoix extends Activity implements OnItemClickListener {
private TextView selection;
private Gallery choix;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
choix = (Gallery) findViewById(R.id.choix);
choix.addTextChangedListener(this);
choix.setAdapter(new ArrayAdapter<Semaine>(this, android.R.layout.simple_gallery_item, Semaine.values()));
choix.setOnItemClickListener(this);
}

public void onItemClick(AdapterView<?> parent, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}
}

Cette fois-ci, notre activité implémente l'interface OnItemClickListener, ce qui signifie que la méthode de rappel à prendre en compte se nomme onItemClick(). Remarquez également le changement de la constante au niveau de l'adaptateur qui s'appelle ici android.R.layout.simple_gallery_item.

 

Choix du chapitre Création de nouveaux composants (nouvelles vues et listes personnalisées)

Si les différentes vues standard ne suffisent plus à réaliser tous vos projets, si vous avez des besoins plus complexes en matière d'interface utilisateur ou avec un style graphique radicalement différent, vous n'aurez d'autre choix que de les créer vous-même.

Retour sur les notions de widgets ou de vues standards

Nous reprendrons cet exemple afin de fabriquer un composant personnalisé adapté à ce genre de situation. Avant cela, je vous propose de faire un tout petit rappel sur la définition et la constitution des widgets.

Le mot widget désigne l'ensemble des vues standards incluses dans la plate-forme Android. Elles font parties du paquetage android.widget. Les vues héritant toutes de la classe View, chaque widget hérite aussi de cette classe. Ainsi l'élément Button hérite-t-il de TextView, qui lui-même hérite de la classe View. L'élément CheckBox, quant à lui, hérite de la vue Button. Android tire profit das méthodes de chaque vue et de chaque widget pour former une plate-forme paramétrable et modulaire. Voici une liste non exhaustive des contrôles actuellement disponibles :

  1. Button : un simple bouton poussoir.
  2. CheckBox : contrôle à deux états, coché et décoché.
  3. EditText : boîte d'édition permettant de saisir du texte ;
  4. TextView : contrôle de base pour afficher un simple texte.
  5. ListView : conteneur permettant d'afficher des données sous forme de liste.
  6. ProgressBar : affiche une barre de progression ou une animation.
  7. RadioButton : bouton à deux états s'utilisant en groupe.

Vous pouvez décrire une interface graphique de deux façons : soit via une définition XML, soit directement depuis le code en créant les objets graphiques correspondant. La construction d'une interface au sein du code d'une activité, plutôt qu'en utilisant une description XML, permet par exemple de créer des interfaces dynamiquement.

Il peut arriver qu'une vue, malgré la richesse que propose Android, ne réponde pas totalement aux besoins du développeur. Dans ce cas, il est possible, au même titre que les autres widgets, d'hériter d'une vue existante pour former un contrôle personnalisé.

Créer un composant personnalisé

Pour créer un composant personnalisé, la première étape est de déterminer quelle est la classe la plus adaptée pour former la base de votre composant.

Dans l'exemple qui suit, nous allons créer un nouveau composant qui est capable de saisir et d'afficher directement des valeurs monétaires. Nous décidons donc d'étendre la vue EditText afin de permettre une saisie, et nous appelons notre nouveau composant Monnaie :
fr.manu.monnaie.Monnaie.java
package fr.btsiris.monnaie;

import android.content.Context;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.EditText;
import java.text.*;

public class Monnaie extends EditText {
private NumberFormat formatMonnaie;

public Monnaie(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

public Monnaie(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public Monnaie(Context context) {
super(context);
init();
}

private void init() {
setSymbole('€');
setValeur(0.0);
setGravity(Gravity.RIGHT);
setInputType(InputType.TYPE_CLASS_NUMBER);
}

public void setSymbole(char symbole) {
formatMonnaie = new DecimalFormat("#,##0.00 "+symbole);
}

public double getValeur() {
try {
Number valeur = (Number) formatMonnaie.parse(getText().toString());
setText(formatMonnaie.format(valeur));
return valeur.doubleValue();
}
catch (ParseException ex) { return 0.0; }
}

public void setValeur(double valeur) {
setText(formatMonnaie.format(valeur));
}
}
  1. Vous noterez la déclaration des trois constructeurs de base d'une vue. Ils servent notamment à créer une vue à partir d'un gabarit d'interface au format XML. De cette façon, la création du composant et son utilisattion dans un fichier de description XML sont semblables à tout autre composant de la plate-forme Android.
  2. Vous remarquez également la présence de méthodes supplémentaires accesseurs (setXxx() notamment) permettant de mettre à jour les propriétés du composant.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:stretchColumns="1"> <TableRow> <fr.btsiris.monnaie.Monnaie
android:id="@+id/euro" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_span="3"/> <Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Franc" android:onClick="calculFranc" /> </TableRow> <TableRow> <fr.btsiris.monnaie.Monnaie
android:id="@+id/franc" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_span="3"/> <Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="€uro" android:onClick="calculEuro" /> </TableRow>
</TableLayout>

Pour faire référence à notre nouveau composant dans le fichier de description XML concernant le layout, il suffit d'utiliser la balise qui porte son nom. La seule petite contrainte est de préciser le nom complet avec son chemin, ici donc <fr.btsiris.monnaie.Monnaie>.

fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;

public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}
}

L'humble ListView est l'un des widgets les plus importants et les plus utilisé d'Android. Que nous choisissions un contact téléphonique, un courrier à faire suivre ou un livre électronique à lire, c'est de ce widget spécialement que nous nous servirons le plus souvent. Mais il serait évidemment plus agréable d'énumérer autre chose que du simple texte. C'est ce que nous tenterons de faire dans la suite de ce chapitre.

Les adpatateurs pour accéder aux données de l'interface

Comme nous l'avons découvert au tout début de cette étude, les adaptateurs servent à gérer les sources des données qui seront notamment affichées dans les différentes listes de l'interface utilisateur. Ils réalisent la liaison entre vos sources de données (un simple tableau de chaînes de caractères, une base de données, un fournisseur de contenu, etc.) et les contrôles de votre interface utilisateur.

Techniquement, un adaptateur - représenté par un objet implémentant l'interface Adapter - n'est ni plus ni moins qu'une interface entre vos données et leur affichage à l'écran. En d'autres termes, c'est un tout-en-un vous permettant de séparer vos données brutes sous diverses formes (array, cursor, bitmap, etc.) en éléments visuels manipulables (navigation sélection, clic).

Le rôle de l'adaptateur peut paraître ambigu, à mi-chemin entre un générateur de vue et un agrégateur de données. Mais bien comprendre son fonctionnement permet d'ouvrir de nombreuses perspectives. Pour afficher une liste d'éléments cliquables, nous avons besoin de trois choses :

  1. Des données : Array, ArrayList, Cursor, etc.
  2. Un Adpater qui fera l'interface entre les données et la vue ;
  3. Une ListView qui fera l'interface entre l'adaptateur et l'utilisateur.














Utiliser le bon adaptateur

Puisque Adapter est une interface, il n'existe pas d'objet Adapter utilisable directement. La raison est, comme bien souvent avec les interfaces, qu'il pourraient exister autant de type d'adaptateurs qu'il existe de type de données. Bien évidemment, la plate-forme Android contient plusieurs implémentations permettant de traiter les types les plus courants :

  1. ArrayAdapter<T> : pour tous les types de tableaux ;
  2. BaseAdapter : pour générer des adaptateurs totalement personnalisés ;
  3. CursorAdapter : représente le résultat de requêtes adressée à une base de données ou à un fournisseur de contenu ;
  4. HeaderViewListAdapter : permet d'ajouter des en-têtes et des pieds de page aux ListView.
  5. ResourceCursorAdapter : pour la création de vues à partir d'une disposition XML ;
  6. SimpleAdapter : malgré son nom, cet adaptateur s'emploie pour des données complexes ;
  7. SimpleCursorAdapter : sur-couche du CursorAdapter permettant de lier un modèle XML aux données.





Afin de bien maîtriser tous ces concepts, je vous propose de reprendre le tout premier exemple que nous avons mis en oeuvre qui affiche la liste des jours de la semaine afin de choisir celui qui nous intéresse. Je rappelle les codes que nous commenterons par la suite :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <ListView
android:id="@android:id/list" android:layout_width="fill_parent"
android:layout_height="fill_parent" /> </LinearLayout>

Vu que nous allons prendre une activité de type ListActivity, la vue est vraiment très simple à mettre en oeuvre. En réalité, nous passons beaucoup plus de temps à régler le TextView par rapport au ListView.

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class ListeChoix extends ListActivity {
private TextView selection;
private String[] semaine = {"Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, semaine));
selection = (TextView) findViewById(R.id.selection);
}

@Override protected void onListItemClick(ListView l, View v, int position, long id) {
selection.setText(semaine[position]);
}
}
  1. Nous commençons par déclarer le tableau que nous associerons à un ArrayAdapter.
  2. Ensuite, afin de simplifier la gestion des vues, nous appliquons cet adaptateur dans une activité de type ListActivity. Ce type d'activité embarque en effet une ListView et des méthodes simplifiant la gestion des adaptateurs.

Le constructeur utilisé pour instancier un nouvel objet de type ArrayAdapter prend trois paramètres :

  1. Le contexte : grâce à ce paramètre, l'adaptateur est capable de créer seul les objets nécessaires à la transcription des fichiers XML en ressources ;
  2. L'identifiant : du fichier XML à utiliser comme modèle pour la liste (en général contenu dans le dossier /res/layout du projet). Le modèle est la vue qui sera utilisée par chaque entrée pour s'afficher dans la liste associée avec ArrayAdapter ;
  3. La collection : les données sous forme d'un tableau de chaînes de caractères.

Amélioration des listes sans création d'un nouveau composant spécifique personnalisé

Généralement, un widget ListView d'Android est une simple liste de texte - robuste mais austère. Cela est dû au fait que nous nous contentons de lui fournir un tableau de mots et que nous demandons à Android d'utiliser une disposition simple pour afficher ces mots sous forme de liste.

Cependant, il est également possible de créer une liste d'icônes, d'icônes et de texte, de cases à cocher et de texte, etc. Tout dépendant des données que vous fournissez à l'adaptateur et de l'aide que vous lui apportez pour créer un ensemble plus riche d'objets View pour chaque ligne.

Supposons, par exemple, que vous désiriez produire une liste dont chaque ligne est constituée d'une icône suivie d'un texte. Vous pourriez utiliser alors une disposition en lignes, dans un fichier de description spécifique appelé ligne.xml :
res/layout/ligne.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> <ImageView
android:id="@+id/icone" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5px" android:paddingLeft="3px" android:src="@drawable/validation" />
// fichier image validation.png représentant l'icône placé dans le répertoire res/drawable.
<TextView
android:id="@+id/texte" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp" android:textStyle="bold|italic" android:textColor="#00BB00" /> </LinearLayout>

Nous utilisons ici un conteneur LinearLayout afin de créer une ligne contenant une icône à gauche et un texte droite avec une grande police de caractères agréable à lire.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <ListView
android:id="@android:id/list" android:layout_width="fill_parent"
android:layout_height="fill_parent" /> </LinearLayout>

Dans le fichier de description principal, nous retrouvons l'ossature générale d'une liste classique avec, d'une part la balise <ListView>, et d'autre part la valeur spécifique"@android:id/list" attribuée à l'identification de la liste. Cela sous-entend, rappelez-vous, que l'activité soit une ListActivity.

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.*;
import android.widget.*; public class ListeChoix extends ListActivity {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
setListAdapter(new ArrayAdapter<Semaine>(this, R.layout.ligne, R.id.texte, Semaine.values()));
}

@Override public void onListItemClick(ListView liste, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}
}

Par défaut, Android ne sait pas que vous souhaitez utiliser cette disposition avec votre ListView. Afin d'établir cette connexion, vous devez donc indiquer à l'adaptateur l'identifiant de ressource de cette disposition personnalisée.

  1. Le point essentiel de cet exemple est que nous avons indiqué à notre adaptateur ArrayAdapter que nous voulions utiliser notre propre disposition de ligne (R.layout.ligne) et que le TextView contenant la valeur de la semaine est désignée par R.id.texte dans cette disposition.
  2. N'oubliez pas que, pour désigner une disposition ligne.xml, il faut préfixer le nom de base du fichier de description par R.layout (R.layout.ligne).
  3. Il est aussi important de souligné, pour éviter tout disfonctionnement, que vous êtes obligé d'identifier chaque composant constituant la ligne, même si à priori vous n'y faites pas référence dans le code.

Personnaliser un adaptateur en proposant une présentation dynamique

La technique de l'exemple précédent - fournir une disposition personnalisée pour les lignes - permet de traiter très élégamment les cas simples, mais elle ne suffit pas pour les scénarii plus complexes comme ceux qui suivent :

  1. Chaque ligne utilise une disposition différente (certaines possèdent une seule ligne de texte, d'autres deux, par exemple).
  2. Vous devez configurer chaque ligne différemment (par exemple pour mettre des icônes différentes en fonction des cas).

Dans ces situations, la meilleure solution consiste à créer une sous-classe de l'adaptateur désiré, à redéfinir getView() et à construire sois-même les lignes. La méthode getView() doit renvoyer un objet View représentant la ligne située à la position fournie par l'adaptateur.

Nous allons reprendre l'idée du projet précédent. Cette fois-ci toutefois, nous remplaçons la petite icône par un petit commentaire textuel placé sous le jour de la semaine. Voici ci-dessous les différents codes que nous commenterons par la suite :
res/layout/ligne.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView
android:id="@+id/semaine" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp" android:textStyle="bold" android:textColor="#00BB00"/> <TextView
android:id="@+id/commentaire" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold|italic"/> </LinearLayout>


res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView
android:id="@+id/selection" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#FF9900" android:textColor="#FFFF00" android:textSize="24sp" android:paddingLeft="7dp" android:text="Votre choix ?" /> <ListView
android:id="@android:id/list" android:layout_width="fill_parent"
android:layout_height="fill_parent" /> </LinearLayout>

Dans le fichier de description principal, nous retrouvons l'ossature générale d'une liste classique avec, d'une part la balise <ListView>, et d'autre part la valeur spécifique"@android:id/list" attribuée à l'identification de la liste. Cela sous-entend, rappelez-vous, que l'activité soit une ListActivity.

fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.*;
import android.widget.*; import android.view.* public class ListeChoix extends ListActivity {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
setListAdapter(new SemaineAdapter(this));
}

@Override public void onListItemClick(ListView liste, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}

class SemaineAdapter extends ArrayAdapter {

SemaineAdapter(Context context) {
super(context, R.layout.ligne, Semaine.values());
}

@Override public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater xml = ListeChoix.this.getLayoutInflater();
View ligne = xml.inflate(R.layout.ligne, null);
TextView semaine = (TextView) ligne.findViewById(R.id.semaine);
TextView commentaire = (TextView) ligne.findViewById(R.id.commentaire);
Semaine jour = Semaine.values()[position];
semaine.setText(jour.name());
switch (jour) {
case Lundi :
case Mardi :
case Jeudi :
case Vendredi : commentaire.setText("Bon travail à l'école"); break;
case Mercredi : commentaire.setText("Bon repos"); break;
case Samedi :
case Dimanche : commentaire.setText("Bon Week-end"); break;
}
return ligne;
}
}
}
  1. Comme choix d'adaptateur, il est souvent judicieux de choisir un ArrayAdapter comme classe de base. Dans ce cas là, seule la méthode getView() est à redéfinir. Si vous désidez de prendre l'adaptateur BaseAdapter, en plus de la méthode getView(), vous devez redéfinir les méthodes getCount(), getItem() et getItemId().
  2. Le principe consiste donc à redéfinir la méthode getView() afin qu'elle renvoie une ligne dépendant de l'objet à afficher, qui est stipulé par l'indice position dans l'adaptateur.
  3. Grâce à la classe LayoutInflater, nous mettons en oeuvre la technique d'inflation qui consiste à convertir une description XML dans l'arborescence d'objets View qu'elle représente, ce qui nous permet de récupérer toutes les valeurs utiles au traitement Java.
  4. Ainsi, dans le traitement de la méthode getView() nous transformons la description R.layout.ligne. Ceci nous donne un objet View qui, en réalité, n'est autre que notre LinearLayout contenant deux TextView, exactement comme cela est spécifié par R.layout.ligne.
  5. Cependant, au lieu de créer nous-même tous ces objets et de les lier ensemble, le code XML et la classe LayoutInflater gèrent pour nous les "détails scabreux".
  6. Nous utilisons donc le LayoutInflater afin d'obtenir un objet View représentant la ligne. Cette ligne est "vide" car le fichier de description ne sait pas quelles sont les données qu'elle recevra. Il nous appartient donc de la personnaliser et de la remplir comme nous le souhaitons avant de la renvoyer.

Il s'agit bien sûr d'un exemple assez artificiel, mais cette technique peut servir à personnaliser les lignes en fonction de n'importe quel critère - le contenu des colonnes d'un Cursor par exemple.

Utilisation de convertView

L'implémentation de getView() que nous venons de mettre en oeuvre fonctionne très bien, mais malheureusement elle est aussi peu efficace. En effet, à chaque fois que l'utilisateur fait défilé l'écran, nous devons créer tout un lot de nouveaux objets View pour les nouvelles lignes qui s'affichent.

Le framework d'Android ne mettant pas automatiquement en cache les objets View existant, il est nécessaire de les recréer de nouveaux, même pour des lignes que nous venions de créer très peu de temps auparavant.

Ce traitement supplémentaire est par ailleurs aggravé par la charge qui est imposé au ramasse-miettes puisque celui-ci doit détruire tous les objets que nous créons.

  1. La méthode getView() reçoit en paramètre un objet View nommé, par convention, convertView. Parfois, cet objet est null, auquel cas vous devez créer une nouvelle instance de View pour la ligne (par inflation), comme nous l'avons expliqué plus haut.
  2. Si convertView n'est pas null, en revanche, il s'agit en fait de l'une des View que vous avez déjà créées. Ce sera notamment le cas lorsque l'utilisateur fait défiler la ListView : à mesure que de nouvelles lignes apparaissent, Android tentera de réutiliser les vues des lignes qui ont disparues à l'autre extrémité, vous évitant ainsi de devoir les reconstruire totalement.
  3. Vous avez ci-dessous la modification du code qui tient compte des vues déjà construites :
fr.manu.test.ListeChoix.java
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.*;
import android.widget.*; import android.view.* public class ListeChoix extends ListActivity {
private TextView selection;
private enum Semaine {Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selection = (TextView) findViewById(R.id.selection);
setListAdapter(new SemaineAdapter(this));
}

@Override public void onListItemClick(ListView liste, View vue, int position, long id) {
Semaine semaine = Semaine.values()[position];
selection.setText(semaine.name());
}

class SemaineAdapter extends ArrayAdapter {

SemaineAdapter(Context context) {
super(context, R.layout.ligne, Semaine.values());
}

@Override public View getView(int position, View convertView, ViewGroup parent) {
View ligne = convertView;
if (ligne==null) {
LayoutInflater xml = ListeChoix.this.getLayoutInflater();
ligne = xml.inflate(R.layout.ligne, null);
} TextView semaine = (TextView) ligne.findViewById(R.id.semaine);
TextView commentaire = (TextView) ligne.findViewById(R.id.commentaire);
Semaine jour = Semaine.values()[position];
semaine.setText(jour.name());
switch (jour) {
case Lundi :
case Mardi :
case Jeudi :
case Vendredi : commentaire.setText("Bon travail à l'école"); break;
case Mercredi : commentaire.setText("Bon repos"); break;
case Samedi :
case Dimanche : commentaire.setText("Bon Week-end"); break;
}
return ligne;
}
}
}

Ainsi, si convertView est null, nous créons une ligne par inflation ; dans le cas contraire, nous nous contentons de la réutiliser. Le code pour remplir les contenus des deux TextView est identique dans les deux cas de figure. Nous évitons ainsi une étape d'inflation potentiellement coûteuse lorsque convertView n'est pas null.

 

Choix du chapitre Créer des menus pour vos activités

Tous les modèles d'Android possèdent un bouton Menu. Grâce à ce bouton, il vous est possible de proposer à vos utilisateurs des fonctionnalités supplémentaires n'apparaissant pas par défaut à l'écran afin de mieux gérer la taille limitée de l'écran d'un appareil mobile. Ces menus sont utiles pour effectuer quelques actions subsidiaires qui interviennent de façon très épisodiques.

Chaque menu est propre à une activité, c'est pourquoi toutes les opérations que nous allons traiter dans cette partie se réfère à une activité. Pour proposer plusieurs menus vous aurez donc besoin de le faire dans chaque activité de votre application.

Un menu se déclenche en appuyant sur le bouton Menu du terminal et possède deux modes de fonctionnement : icône et étendu.

  1. Lorsque l'utilisateur appuie sur le bouton Menu, le menu est en mode icône et n'affiche que les six premiers choix sous la forme de gros boutons faciles à sélectionner, disposés en ligne en bas de l'écran.
  2. Si ce menu compte plus de six choix, le sixième bouton affiche "Plus" - cliquez sur cette option fait passer le menu en mode étendu, qui affiche tous les choix restants. L'utilisateur peut bien sûr faire défiler le menu afin d'effectuer n'importe quel choix.

Création d'un menu

Pour créer un menu, il vous suffit de redéfinir la méthode de rappel, prévue à cette effet, onCreateOptionsMenu() de la classe Activity. Cette méthode est effectivement appelée la première fois que l'utilisateur appuie sur le bouton Menu de son téléphone. Elle reçoit en paramètre un objet de type Menu dans lequel nous ajoutons nos différents choix.

Une bonne pratique non obligatoire consiste à établir un chaînage vers la méthode de la super classe (super.onCreateOptionsMenu()), afin qu'Android puisse lui ajouter les choix nécessaires, puis vous pouvez ajouter les vôtres comme nous allons le découvrir ci-dessous.

Attention, cette méthode de rappel, comme son nom l'indique, n'est appelée qu'une seule fois, pendant la phase de création. Si vous avez besoin d'ajouter, de supprimer ou de modifier un élément du menu après coup, il suffit de manipuler l'instance de Menu que vous avez bien pris soin d'enregistrer. Une seconde solution consiste à implémenter une autre méthode de rappel qui s'occupe de ce cas précis et qui s'appelle onPrepareOptionsMenu(). Elle est systématiquement appelée avant chaque affichage de menu.

Ajout des différents options dans votre menu

Pour ajouter toutes les sélections possibles à votre menu, utilisez la méthode add() de la classe Menu. Celle-ci est surchargée afin de recevoir les combinaisons des paramètres suivants :

  1. Un identifiant de groupe (un entier) qui doit valoir NONE (ou tout simplement la valeur entière 0), sauf si vous désirez créer un ensemble d'options de menu qui regroupe un même type de sélection, au moyen de la méthode setGroupCheckable() .
  2. Un identifiant de choix (également un entier) servant à identifier la sélection dans la méthode de rappel onOptionsItemSelected() lorsqu'une option du menu a été choisie.
  3. Un identifiant d'ordre (encore un entier), indiquant l'emplacement du choix dans le menu lorsque ce dernier contient des options ajoutées par Android. Sans cela, contentez-vous d'utiliser la constante NONE.
  4. Le texte du choix, sous la forme d'une chaîne de caractères de type String ou d'un identifiant de ressource.

Les éléments MenuItem

Toutes les méthodes add() renvoient une instance de MenuItem qui vous permet ensuite de modifier tous les réglages de choix concerné (son texte, par exemple).

Votre activité sera prévenue d'un choix de l'utilisateur par la méthode de rappel onOptionsItemSelected(). Vous recevrez alors l'objet MenuItem correspondant à ce choix. Un motif de conception classique consiste à utiliser une instruction switch() avec l'identifiant de menu item.getItemId(), afin d'exécuter l'action appropriée. Notez que la méthode onOptionsItemSelected() est utilisée indépendamment du fait que l'option choisie soit un choix de menu de base ou d'un sous-menu.

Vous avez également la possibilité de mettre en place un raccourci pour un choix - un mnémonique d'un seul caractère, permettant de sélectionner ce choix lorsque le menu est visible. Android permet d'utiliser des raccourcis alphabétiques et numériques, mis en place respectivement par setAlphabeticShortCut() et setNumericShortCut(). Le menu est placé en mode raccourci alphabétique en passant le paramètre true à sa méthode setQwertyMode().

Les identifiants de choix et de groupe sont des clés servant à dévérouiller certaines fonctionnalités supplémentaires des menus :

  1. Un appel à MenuItem.setCheckable() avec un identifiant de choix ajoute une case à cocher à côté du titre de ce choix. La valeur de cette case bascule lorsque l'utilisateur sélectionne ce choix.
  2. Un appel à Menu.setGroupCheckable() avec un identifiant de groupe permet de rendre un ensemble de choix mutuellement exclusifs en leur associant des boutons radios, afin de ne pouvoir cocher qu'une seule option du groupe à un instant donné.
  3. Un appel à MenuItem.setIcon() avec un identifiant de choix ajoute une petite icône à côté du titre de ce choix.
Nous allons reprendre l'exemple qui réalise des conversions monétaires dans les deux sens en reprenant le nouveau composant Monnaie que nous avons généré. Cette fois-ci, il sera possible de choisir le nombre de chiffres pour les centimes, soit deux, soit trois (des millièmes) ou tout simplement sans montrer les centimes. Toutefois, rien n'est changé au niveau du fichier de description XML (la vue) :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:stretchColumns="1"> <TableRow> <fr.btsiris.monnaie.Monnaie
android:id="@+id/euro" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_span="3"/> <Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Franc" android:onClick="calculFranc" /> </TableRow> <TableRow> <fr.btsiris.monnaie.Monnaie
android:id="@+id/franc" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_span="3"/> <Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="€uro" android:onClick="calculEuro" /> </TableRow>
</TableLayout>

Nous allons maintenant modifier notre composant personnalisé afin qu'il puisse accepter différentes présentations de la monnaie suivant le nombre de centimes que nous désirons voir apparaître :

fr.manu.monnaie.Monnaie.java
package fr.btsiris.monnaie;

import android.content.Context;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.EditText;
import java.text.*;

public class Monnaie extends EditText {
private NumberFormat formatMonnaie;
private String nombreDecimales = "0.00 ";
private char symbole;

public Monnaie(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

public Monnaie(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public Monnaie(Context context) {
super(context);
init();
}

private void init() {
setSymbole('€');
setValeur(0.0);
setGravity(Gravity.RIGHT);
setInputType(InputType.TYPE_CLASS_NUMBER);
}

public void setSymbole(char symbole) {
this.symbole = symbole;
formatMonnaie = new DecimalFormat("#,##"+nombreDecimales+symbole);
}

public double getValeur() {
try {
Number valeur = (Number) formatMonnaie.parse(getText().toString());
setText(formatMonnaie.format(valeur));
return valeur.doubleValue();
}
catch (ParseException ex) { return 0.0; }
}

public void setValeur(double valeur) {
setText(formatMonnaie.format(valeur));
}

public void setNombreDecimales(int nombre) {
switch(nombre) {
case 0 : nombreDecimales = "0 "; break;
case 2 : nombreDecimales = "0.00 "; break;
case 3 : nombreDecimales = "0.000 "; break;
}
formatMonnaie = new DecimalFormat("#,##"+nombreDecimales+symbole);
getValeur();
}
}
  1. La grande nouveauté par rapport au code précédent, c'est la présence d'une nouvelle propriété nombreDécimales qui va servir au formatage du texte suivant la valeur monétaire désirée et bien sûr suivant le nombre de décimales après la virgule.
  2. Pour que tout se passe correctement, il est aussi nécessaire cette fois-ci d'enregistrer le symbole de la monnaie.

Pour l'activité principale, nous devons maintenant mettre en oeuvre notre menu au moyen des deux méthodes importantes, savoir onCreateOptionsMenu() et onOptionsItemSelected().

fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.Activity;
import android.os.Bundle;
import android.view.*;

public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}

@Override public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, 0, Menu.NONE, "Pas de centimes");
menu.add(Menu.NONE, 2, Menu.NONE, "2 chiffres");
menu.add(Menu.NONE, 3, Menu.NONE, "3 chiffres");
return super.onCreateOptionsMenu(menu); // ou bien return true; } @Override public boolean onOptionsItemSelected(MenuItem item) {
euro.setNombreDecimales(item.getItemId());
franc.setNombreDecimales(item.getItemId());
return super.onOptionsItemSelected(item); // ou bien return true; } }

Les sous-menus

Créer des sous-menus peut se révéler utile afin de proposer plus d'options sans encombrer l'interface utilisateur. Pour ajouter un sous-menu, vous devrez ajouter un menu de type SubMenu et des éléments le composant.

Le paramétrage est le même que pour un menu, à l'exception de l'image : vous ne pourrez pas ajouter d'image sur les éléments de vos sous-menus. Néanmoins, il sera possible d'ajouter une image dans l'en-tête du sous-menu au moyen de la méthode setHeaderIcon() de la classe SubMenu.

Pour créer des sous-menus à la volée, vous appelez la méthode addSubMenu() de la classe Menu avec les mêmes paramètres que ceux de la méthode add(). Android appelera alors automatiquement la méthode de rappel onCreatePanelMenu() en lui passant l'identifiant du choix du sous-menu, ainsi qu'une autre instance représentant le sous-menu lui-même. Comme avec onCreateOptionsMenu(), vous devez établir un chaînage avec la méthode de la superclasse, puis ajouter les choix au sous-menu.

Une autre restriction est que vous ne pouvez pas imbriquer sans fin les sous-menus : un menu peut avoir un sous-menu, mais ce dernier ne peut pas contenir lui-même de sous-sous-menu.

A titre d'exemple, sur le projet précédent, je propose cette fois-ci d'avoir un menu avec simplement deux options "Pas de centimes" et "Avec des centimes". Lorsque l'utilisateur choisi l'option "Avec des centimes" un sous-menu apparaît qui propose de visualiser les centimes ou les millièmes, avec deux ou trois chiffres :

Voici ci-dessous les modifications à apporter dans le code de Conversion.java afin d'obtenir le lancement de ce sous-menu :

fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.Activity;
import android.os.Bundle;
import android.view.*;

public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}

@Override public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, 0, Menu.NONE, "Pas de centimes");
SubMenu sousMenu = menu.addSubMenu(Menu.NONE, -1, Menu.NONE, "Avec des centimes");
sousMenu.add(Menu.NONE, 2, Menu.NONE, "2 chiffres");
sousMenu.add(Menu.NONE, 3, Menu.NONE, "3 chiffres");
sousMenu.setHeaderIcon(R.drawable.icon);
return super.onCreateOptionsMenu(menu); // ou bien return true; } @Override public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == -1) return true;
euro.setNombreDecimales(item.getItemId());
franc.setNombreDecimales(item.getItemId());
return super.onOptionsItemSelected(item); // ou bien return true; } }

Attention, lorsque l'utilisateur sélectionne le sous-menu, c'est un choix comme un autre, et du coup, la méthode de rappel onOptionsItemSelected() est également sollicité. Il faut donc prendre en compte ce choix particulier afin de faire la différence avec un véritable choix. C'est la raison pour laquelle, le sous-menu prend la valeur -1 pour l'identifiant de choix que nous devons évaluer avant de soumettre la valeur au monnaies euro et franc.

Les menus contextuels

La plate-forme Android autorise également la création des menus contextuels, autrement dit de menus dont le contenu change en fonction du contexte. Les menus contextuels fonctionnent d'ailleurs de façon similaire aux sous-menus.

Rappelons que sous Android, l'appel d'un menu contextuel se fait lorsque l'utilisateur effectue un touché prolongé, de quelques secondes, sur un élément de l'interface graphique, sur un widget.

Le fonctionnement des menus contextuels est quasiment identique à celui des menus d'options. Les deux différences principales concernent leur remplissage et la façon dont vous serez informé des choix effectués par l'utilisateur.

Procédure à suivre pour mettre en oeuvre un menu contextuel
  1. Vous devez d'abord indiquer le ou les widgets de votre activité qui disposerons de menus contextuels. Pour cela, il suffit d'appeler la méthode registerForContextMenu() à partir de l'activité, en fournissant en paramètre la vue concernée (type View), le widget qui a besoin d'un menu contextuel.
  2. Puis vous devez redéfinir la méthode onCreateContextMenu(), qui, entre autres choses, reçoit l'objet View que vous aviez fourni à registerForContextMenu(). Si votre activité doit construire plusieurs menus contextuels, ceci permet d'identifier le menu concerné.
  3. La méthode onCreateContextMenu() reçoit en paramètre le ContextMenu lui-même, la View à laquelle est associé le menu contextuel et un objet ContextMenu.ContextMenuInfo, qui indique l'élément de la liste qui a été touché et maintenu par l'utilisateur au cas où vous souhaiteriez personnaliser ce menu contextuel en fonction de cette information.
  4. Il est également important de remarquer que la méthode onCreateContextMenu() est appelée à chaque fois que le menu contextuel est sollicité. A la différence d'un menu d'options (qui n'est construit qu'une seule fois par activité), les menus contextuels sont supprimés après utilisation : vous ne pouvez donc pas compter sur l'objet ContextMenu fourni ; il faut reconstruire le menu pour qu'il corresponde aux besoins de votre activité.
  5. Afin d'être prévenu de la sélection d'un choix de menu contextuel, redéfinissez la méthode onContextItemSelected() de l'activité. Dans cette méthode de rappel, vous n'obtiendrez que l'instance du MenuItem qui a été choisi : si l'activité possède plusieurs menus contextuels, il est nécessaire que les identifiants des éléments de menus soient uniques afin de pouvoir les traiter de façon appropriée.
  6. Vous pouvez également appeler la méthode getMenuInfo() du MenuItem afin d'obtenir l'objet ContextMenu.ContextMenuInfo que vous aviez reçu dans la méthode onCreateContextMenu(). Pour le reste, cette méthode de rappel se comporte comme onOptionsItemSelected(), que nous avons décrite dans la section précédente.
Toujours avec le même type de projet, je vous propose de mettre en oeuvre un menu contextuel associé à chacune des monnaies. Ainsi, il sera maintenant possible de personnaliser chacune des monnaies pour choisir le nombre de chiffres après la virgule :

Voici ci-dessous les modifications à apporter dans le code de Conversion.java afin d'obtenir le lancement de ce menu contextuel :

fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.Activity;
import android.os.Bundle;
import android.view.*;
import android.view.ContextMenu.ContextMenuInfo;

public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;
private View monnaie;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
registerForContextMenu(euro);
registerForContextMenu(franc);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}

@Override public void onCreateContextMenu(ContextMenu menu, View vue, ContextMenuInfo menuInfo) {
menu.clear();
menu.setHeaderTitle("Nombre de décimales");
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, "Pas de centimes");
menu.add(Menu.NONE, 2, Menu.NONE, "2 chiffres");
menu.add(Menu.NONE, 3, Menu.NONE, "3 chiffres");
monnaie = vue;
}

@Override public boolean onContextItemSelected(MenuItem item) {
if (monnaie.equals(euro)) euro.setNombreDecimales(item.getItemId());
if (monnaie.equals(franc)) franc.setNombreDecimales(item.getItemId());
return super.onOptionsItemSelected(item);
}
}
  1. Dans la méthode onCreate() de l'activité, il est nécessaire d'enregistrer les vues qui lancent le menu contextuel au travers de la méthode registerForContextMenu().
  2. La méthode onCreateContextMenu() est appelée à chaque fois que le menu contextuel est appelé. Nous constituons donc notre menu en effaçant au préalable les choix proposés par défaut par Android qui permettent, comme il s'agit d'une zone de saisie, de réaliser par exemple du copier-coller.
  3. Afin de connaître quelle zone de saisie propose ce sous-menu, il est préférable de l'enregistrer au travers d'un attribut adapté, ici monnaie.
  4. Dès que l'utilisateur à fait son choix, la méthode onContextItemSelected() est lancée. Vous devez maintenant tester qu'elle est la zone de saisie qui désire changer le nombre de chiffres après la virgule et de faire le changement uniquement sur le bon composant.

 

Choix du chapitre Affichage de messages surgissants

Dans certaines situations, vous pouvez avoir besoin de communiquer avec l'utilisateur par des messages d'alerte ou de confirmation en vous affranchissant des limites de l'interface classique. Android dispose de plusieurs moyens d'alerter les utilisateurs par d'autres systèmes que ceux des activités classiques. Deux principales méthodes permettent de faire surgir des messages, les toasts et les alertes.

Les toasts

Un toast est un message transitoire, ce qui signifie qu'il s'affiche et disparaît de lui-même, sans intervention de l'utilisateur. En outre, il ne modifie pas le focus de l'activité courante ; si l'utilisateur est en train de réaliser une saisie, ses frappes au clavier ne seront pas capturées par le message surgissant.

Un toast étant transitoire, vous n'avez aucun moyen de savoir si l'utilisateur l'a remarqué. Vous ne recevrez aucun accusé de réception de sa part et le message ne restera pas affiché suffisamment longtemps pour ennuyer l'utilisateur. Un toast est donc essentiellement conçu pour diffuser des messages d'avertissement - pour annoncer qu'une longue tâche en arrière-plan s'est terminée, que la batterie est presque vide, etc.

La création d'un toast est assez simple. Il existe pour cela la classe Toast qui fournit une méthode statique makeText() qui prend un objet String (ou un identifiant d'une ressource textuelle) en paramètre et qui renvoie une instance de Toast. Les autres paramètres de cette méthode sont l'activité (ou tout autre contexte) et une durée valant LENGTH_SHORT ou LENGTH_LONG pour exprimer la durée relative pendant laquelle le message restera visible. Lorsque le toast est configuré, il suffit d'appeler sa méthode show() pour qu'il s'affiche.

Personnaliser un toast
  1. Si vous préférez créer un toast à partir d'une View au lieu d'une simple chaîne ennuyeuse, il suffit de créer une instance de Toast à l'aide du constructeur (qui attend un contexte) puis d'appeler setView() pour lui indiquer la vue à utiliser et setDuration() pour fixer sa durée.
  2. Par défaut, Android affiche le message en bas de l'écran de l'utilisateur. Ce comportement peut être redéfini pour en changer la position. Vous pouvez ainsi spécifier la disposition d'un toast en spécifiant son ancrage sur l'écran ainsi qu'un décalage sur l'axe horizontal et vertical au moyen de la méthode setGravity() :
    toast.setGravity(Gravity.TOP, 0, 40);
Je vous propose de reprendre tout simplement le projet précédent qui affiche un message surgissant circonstancié suivant le nombre de chiffres choisi sur la monnaie qui vous intéresse :

fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.Activity;
import android.os.Bundle;
import android.view.*;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.Toast;

public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;
private View monnaie;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
registerForContextMenu(euro);
registerForContextMenu(franc);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}

@Override public void onCreateContextMenu(ContextMenu menu, View vue, ContextMenuInfo menuInfo) {
menu.clear();
menu.setHeaderTitle("Nombre de décimales");
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, "Pas de centimes");
menu.add(Menu.NONE, 2, Menu.NONE, "2 chiffres");
menu.add(Menu.NONE, 3, Menu.NONE, "3 chiffres");
monnaie = vue;
}

@Override public boolean onContextItemSelected(MenuItem item) {
String nom = "";
if (monnaie.equals(euro)) {
euro.setNombreDecimales(item.getItemId());
nom = "€uro";
}
if (monnaie.equals(franc)) {
franc.setNombreDecimales(item.getItemId());
nom = "Franc";
}
if (nom.length()!=0) {
String decimales = item.getItemId() == 0 ? " sans décimale" : " utilise "+ item.getItemId() + " décimales";
Toast.makeText(this, nom + decimales, Toast.LENGTH_SHORT).show();
}
return super.onOptionsItemSelected(item);
}
}

Les alertes

Si vous préférez utiliser le style plus classique des boîtes de dialogue, choisissez plutôt AlertDialog. Comme toute boîte de dialogue modale, un AlertDialog s'ouvre, prend le focus et reste affiché tant que l'utilisateur ne le ferme pas.

Ce type d'affichage convient donc bien aux erreurs critiques, aux messages de validation qui ne peuvent pas être affichés correctement dans l'interface de base de l'activité ou à toute autre information dont vous voulez vous assurer la lecture immédiate par l'utilisateur.

Pour créer un AlertDialog, le moyen le plus simple consiste à utiliser la classe Builder qui offre un ensemble de méthodes permettant de configurer un AlertDialog. Chacune de ces méthodes renvoie une instance de Builder afin de faciliter le chaînage des appels. A la fin, il suffit d'appeler la méthode show() de l'objet Builder pour afficher la boîte de dialogue. Après l'appel de show(), la boîte de dialogue s'affiche et attend une intervention de l'utilisateur. Voici les méthodes de configuration de Builder les plus utilisées :

  1. setMessage() : permet de définir le corps de la boîte de dialogue à partir d'un simple message de texte. Son paramètre est un objet String ou un identifiant de ressource textuelle.
  2. setTitle() et setIcon() : permettent de configurer le texte et/ou l'icône qui apparaîtra dans la barre de titre de la boîte de dialogue.
  3. setPositiveButton(), setNeutralButton() et setNegativeButton() : permettent d'indiquer les boutons qui apparaîtrons en bas de la boîte de dialogue, leur emplacement latéral (respectivement, à gauche, au centre ou à droite), leur texte et le code qui sera appelé lorsque nous cliquons sur un bouton (en plus de refermer la boîte de dialogue).

Si vous devez faire d'autres configurations que celes proposées par Builder, appelez la méthode create() à la place de show() : vous obtiendrez ainsi une instance de AlertDialog partiellement construite que vous pourrez configurer avant d'appeler l'une des méthodes show() de l'objet AlertDialog lui-même.

Sur le projet précédent, même si cela n'est pas du tout judicieux, je vous invite à enlever le toast et de proposer maintenant une boîte d'alerte :
fr.manu.monnaie.Conversion.java
package fr.btsiris.monnaie;

import android.app.*;
import android.os.Bundle;
import android.view.*;
import android.view.ContextMenu.ContextMenuInfo; public class Conversion extends Activity {
private Monnaie euro, franc;
private final double TAUX = 6.55957;
private View monnaie;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
euro = (Monnaie) findViewById(R.id.euro);
franc = (Monnaie) findViewById(R.id.franc);
franc.setSymbole('F');
franc.setValeur(0.0);
registerForContextMenu(euro);
registerForContextMenu(franc);
}

public void calculFranc(View vue) {
franc.setValeur(euro.getValeur() * TAUX);
}

public void calculEuro(View vue) {
euro.setValeur(franc.getValeur() / TAUX);
}

@Override public void onCreateContextMenu(ContextMenu menu, View vue, ContextMenuInfo menuInfo) {
menu.clear();
menu.setHeaderTitle("Nombre de décimales");
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, "Pas de centimes");
menu.add(Menu.NONE, 2, Menu.NONE, "2 chiffres");
menu.add(Menu.NONE, 3, Menu.NONE, "3 chiffres");
monnaie = vue;
}

@Override public boolean onContextItemSelected(MenuItem item) {
String nom = "";
if (monnaie.equals(euro)) {
euro.setNombreDecimales(item.getItemId());
nom = "€uro";
}
if (monnaie.equals(franc)) {
franc.setNombreDecimales(item.getItemId());
nom = "Franc";
}
if (nom.length()!=0) {
String decimales = item.getItemId() == 0 ? "Sans décimale" : item.getItemId() + " décimales";
new AlertDialog.Builder(this)
.setTitle(nom)
.setIcon(R.drawable.icon)
.setMessage(decimales)
.setNeutralButton("Fermeture", null)
.show();
}
return super.onOptionsItemSelected(item);
}
}

La méthode setNeutralButton(), comme les autres méthodes connexes, prend en deuxième paramètre un écouteur de type OnClickListener que vous devez implémenter afin de réaliser le traitement souhaité lorsque l'utilisateur clique sur le bouton. Ici, il ne fallait rien faire de spécial, la valeur de ce paramètre est donc null.

 

Choix du chapitre Utilisation des threads

Vous vous devez de livrer une application qui réagisse rapidement aux actions de l'utilisateur (un objectif inférieur à 1/4 de seconde est une bonne référence). Au-delà de 5 secondes d'attente, le gestionnaire d'activité ActivityManager peut prendre la décision de tuer votre activité en la considérant comme une application sans réponse.

Mais, votre programme peut devoir accomplir une tâche qui s'effectue en un temps non négligeable, ce qui implique invariablement d'utiliser un thread. Android dispose d'une véritable panoplie de moyens pour mettre en place des threads en arrière-plan tout en leur permettant d'interagir proprement avec l'interface graphique, qui, elle, s'exécute dans un thread qui lui est dédié.

Cette "interaction propre" est cruciale car l'interface graphique ne peut être modifiée que par son thread et non par un thread en arrière-plan. Ceci signifie généralement qu'il devra donc exister une certaine coordination entre les threads en arrière-plan, qui effectue le traitement, et le thread de l'interface graphique, qui affiche le résultat de ce traitement.

Ces mécanisme se trouvent implémentés au moyen de l'interface Runnable, que nous connaissons bien, ou mieux encore au travers de la classe Handler qui fait partie du paquetage android.os.

Exécution et communication entre threads avec la classe Handler

Le moyen le plus souple de créer une tâche en arrière-plan avec Android consiste à créer une instance d'une classe dérivant de Handler. Vous n'avez besoin que d'un seul objet Handler par activité.

Cette classe Handler vous rendra la vie relativement facile puisqu'il suffit simplement de l'instancier pour qu'elle s'enregistre d'elle-même dans la gestion des threads du système Android.

Le thread ainsi créé, exécutant votre travail d'arrière-plan, communique avec l'instance de Handler du thread de l'interface utilisateur. Cela permet au thread de l'interface utilisateur, qui exécute le travail courant, de pouvoir mettre à jour l'interface en fonction de l'activité réalisée en arrière-plan.

C'est important car les changements de cette interface - la modification de ses widgets, par exemple - ne doivent intervenir que dans le thread de l'interface de l'activité. Vous avez deux possibilités pour communiquer avec le Handler : les messages et les objets Runnable.

Utiliser les messages

Pour communiquer entre les threads d'un Handler, vous pouvez utiliser un système de messages. Ce système permet d'ajouter des messages dans une file et de les traiter dans leur ordre d'arrivée.

Pour envoyer un message à l'objet de type Handler, il est d'abord nécessaire de récupérer une instance de type Message en appelant la méthode obtainMessage(). Cet objet représente alors le support même du message qui sert de communication dans le Handler.

Cette méthode obtainMessage() est surchargée pour vous permettre de créer un message vide ou contenant des identifiants et des paramètres de messages. Plus le traitement que doit effectuer le Handler est compliqué, plus il y a de chances que vous deviez placer des données dans le message pour aider le Handler à distinguer les différents événements.

Une fois le message créé et configuré, nous pouvons effectivement l'envoyer, en passant par la file d'attente, à l'aide de l'une des méthodes de la famille sendMessageXXX() :

  1. sendMessage() : place immédiatement le message dans la file.
  2. sendMessageAtFrontOfQueue() : place immédiatement le message en tête de file ( au lieu de le mettre à la fin, ce qui est le comportement par défaut). Votre message aura donc priorité par rapport à tous les autres.
  3. sendMessageAtTime() : place le message dans la file à l'intant indiqué, exprimé en millisecondes par rapport au temps système SystemClock.uptimeMillis().
  4. sendMessageDelayed() : place le message dans le file après un certain délai, exprimé en millisecondes.

Il existe aussi des variantes de ces méthodes pour envoyer des messages vides : il s'agit des méthodes sendEmptyMessage(), sendEmptyMessageAtTime() et sendEmptyMessageDelayed() qui possèdent le même comportement que leurs grandes soeurs décrites ci-dessus. La seule différence est que vous n'avez pas à fournir d'objet Message mais un entier comme seule donnée de communication.

Ensuite, pour traiter ces messages au niveau de l'interface utilisateur, votre Handler doit implémenter la méthode handleMessage() qui sera automatiquement appelée pour chaque message. C'est effectivement là que le Handler peut modifier l'interface utilisateur s'il le souhaite. Cependant, il doit le faire le plus rapidement possible car les autres opérations de l'interface sont suspendues tant qu'il n'a pas terminé.

Un exemple typique de l'utilisation de la classe Handler pour gérer les threads est l'intégration d'une barre de progression dans l'interface. Voici ci-dessous le fichier de mise en page de l'activité :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/progression" android:layout_width="fill_parent"
android:layout_height="wrap_content"
style="@android:style/Widget.ProgressBar.Horizontal" />

Voici également le code associé utilisant le Handler pour communiquer entre le thread d'arrière-plan et celui de l'interface utilisateur :

fr.btsiris.tache.Tache.java
package fr.btsiris.tache;

import android.app.Activity;
import android.os.*;
import android.widget.ProgressBar;

public class Tache extends Activity {
private ProgressBar progression;
private boolean enExecution = false;

private Handler communication = new Handler() {
@Override public void handleMessage(Message msg) {
progression.incrementProgressBy(1);
}
};

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
progression = (ProgressBar) findViewById(R.id.progression);
}

@Override protected void onStart() {
super.onStart();
progression.setProgress(0);
enExecution = true;
new Thread(new TacheDeFond()).start();
}

@Override protected void onStop() {
super.onStop();
enExecution = false;
}

private class TacheDeFond implements Runnable {
@Override public void run() {
try {
for (int i=0; i<100 && enExecution; i++) {
Thread.sleep(100);
communication.sendMessage(communication.obtainMessage());
}
}
catch (Throwable t) {}
}
}
}
  1. Une partie de la construction de l'activité passe par la création d'une instance de Handler, communication, contenant l'implémentation de handleMessage(). Pour chaque message reçu, nous nous contentons de faire avancer la barre de progression de 1 point puis nous sortons du gestionnaire de message le plus vite possible.
  2. Dans la méthode onStart(), nous configurons un thread en arrière-plan. Dans une vraie application, celui-ci effectuerait une tâche significative mais, ici, nous nous mettons simplement en pause pendant 1/10 de seconde, nous postons le Message au Handler et nous répétons ces deux opérations 100 fois. Combiné avec la progression d'une unité de la position de la barre de progression, ce traitement fera donc avancer la barre à travers tout l'écran puisque la valeur maximale par défaut de la barre est 100.
  3. Notez que nous devons ensuite quitter la méthode onStart(). C'est un point crucial : la méthode onStart() est appelée dans le thread qui gère l'interface utilisateur de l'activité afin qu'elle puisse modifier les widgets et tout ce qui affecte cette interface - la barre de titre, par exemple. Ceci signifie donc que nous devons sortir de la méthode onStart(), à la fois pour laisser le Handler faire son travail et pour qu'Android ne pense pas que notre activité est bloquée.

Notez que, bien que le code de cet exemple s'arrange pour mettre à jour la barre de progression dans le thread de l'interface graphique, ce n'est pas nécessaire. A partir d'Android 1.5, en effet, ProgressBar est thread safe, ce qui signifie que vous pouvez la modifier à partir de n'importe quel thread et qu'elle gèrera les détails de la modification de l'interface graphique dans le thread de celle-ci.

Vous remarquez que le message envoyé n'est pas du tout utiliser par la méthode handleMessage() de l'objet communication. Du coup, il est possible d'utiliser éventuellement la méthode sendEmptyMessage() au lieu de la méthode sendMessage() dans notre tâche de fond :

fr.btsiris.tache.Tache.java
...
   private class TacheDeFond implements Runnable {
@Override public void run() {
try {
for (int i=0; i<100 && enExecution; i++) {
Thread.sleep(100);
communication.sendEmptyMessage(0);
}
}
catch (Throwable t) {}
}
}
}
Les objets Runnable

Si vous préférez ne pas vous ennuyer avec les objets Message, vous pouvez également passer des objets Runnable au Handler, qui les exécutera dans le thread de l'interface utilisateur. La classe Handler fournit un ensemble de méthodes post(), postAsFrontOfQueue(), postDelayed() et postAtTime() pour faire passer les objets Runnable afin qu'ils soient traités.

 

Choix du chapitre Communication entre applications : la classe Intent

Pour l'instant, nous ne nous sommes intéressés qu'aux activités ouvertes directement par l'utilisateur à partir du lanceur du terminal, ce qui est évidemment, le moyen le plus évident de lancer une activité et de la rendre disponible à l'utilisateur. Dans la plupart des cas, c'est de cette façon que l'utilisateur commencera à utiliser votre application.

Cependant, le système Android repose sur un grand nombre de composants étroitement liés. Ce que vous pouvez obtenir dans une interface graphique via des boîtes de dialogue, des fenêtres filles, etc. est généralement traité par des activités indépendantes. Bien que l'une d'elles puisse être "spécifique" puisqu'elle apparaît dans le lanceur, les autres doivent toutes être accessibles... d'une façon ou d'une autre.

Elles le sont grâce aux intentions. Au coeur du système Android, les intentions forment un mécanisme sophistiqué et complet permettant aux activités et aux applications d'interagir entre elles.

La communication interne du système Android est basée sur l'envoi et la réception de messages exprimant l'intention d'une action. Représentant une description abstraite d'une opération à réaliser, chacun des messages peut être émis à destination d'un autre composant de la même application (une activité, un service, etc.) ou même celui d'une toute autre application.

Issu de la classe Intent, ce message permet de véhiculer toutes les informations nécessaires à la ralisation de l'action :

  1. informations à destination du composant qui le réceptionnera (actions à effectuer et les données avec lesquelles agir) ;
  2. informations nécessaires au système pour son traitement (catégorie du composant cible du message et instructions d'exécution de l'action).

Le démarrage des composants d'une application (activités, services, etc.) est réalisé au moyen d'un objet Intent. L'utilisation d'un composant externe peut ainsi être décidée au moment de l'exécution de l'application et non lors de la compilation de l'application. Ce mécanisme permet d'éviter les dépendances et de rendre beaucoup plus dynamique et pertinente l'utilisation des applications dans un contexte donné.

Par exemple, si vous souhaitez utiliser un navigateur web pour afficher une page particulière depuis votre application, vous n'allez pas cibler un navigateur en particulier, mais demander au système d'ouvrir un navigateur qui peut être celui fourni par défaut par le système ou un navigateur alternatif que l'utilisateur préfère utiliser.

Vous pouvez envoyer des intentions au système de deux façons : soit en ciblant un composant précis d'une application (on parle alors de mode explicite), soit en laissant le système déléguer le traitement (ou non) de cette demande au composant le plus approprié (on parle alors de mode implicite).

Un système de filtres permet à chaque application de filtrer et de gérer uniquement les intentions qui sont pertinents pour celle-ci. Une application peut ainsi être dans un état d'inactivité, tout en restant à l'écoute des intensions circulant dans le système.

Principe de fonctionnement

Les objets Intent ont essentiellement trois utilisations : ils permettent de démarrer une activité au sein de l'application courante, de solliciter d'autres applications ou d'envoyer des informations.

  1. Le démarrage d'une activité au sein d'une même application est utilisée pour la navigation entre écrans d'une interface graphique et l'appel d'une boîte de dialogue. C'est d'ailleurs le seul cas où vous devrez démarrer une activité en mode explicite.
  2. Lorsqu'un besoin ne peut être satisfait par l'application elle-même, elle peut solliciter une autre application pour y répondre (le besoin peut être l'exécution d'une action ou la transmission d'informations). Elle peut se contenter de transmettre son intention au système qui, lui, va se charger de trouver l'application et le composant le plus approprié puis démarrer ce dernier et lui transmettre l'Intent correspondant. On parle alors de résolution de l'intention : à un contexte donné, le système choisit au mieux l'application correspondant à un objet Intent donné.
  3. Les intentions ont aussi d'autres utilisations, dont le démarrage d'un service. Le mécanisme relatif aux objets Intent et leur utilisation sont en effet indispensables pour les applications fonctionnant en arrière-plan (telles que les services que nous découvrirons dans un prochain chapitre) afin de recevoir des actions à effectuer (par exemple, pour un service de lecteur multimédia, les actions pourront être de lire une musique, passer à la chanson suivante ou encore mettre la lecture en pause en cas d'appel entrant) mais également pour pouvoir communiquer avec d'autres applications.
  4. Il est aussi possible de vouloir diffuser un objet Intent à plusieurs applications (par exemple pour informer l'ensemble des applications ouvertes que la batterie est défaillante). Le système pouvant très vite devenir verbeux, les applications peuvent mettre en place un filtre permettant de ne conserver que les Intents que l'application juge nécessaires.

Composantes des intentions

Les deux parties les plus importantes d'une intention sont l'action et ce qu'Android appelle les "données". Elles sont quasiment analogues aux verbes et aux URL de HTTP - l'action est le verbe et les "données" sont une URI comme content://contacts/people/1, représentant un contact dans la base de données des contacts.

Les actions sont des constantes, comme ACTION_VIEW (pour afficher la ressource), ACTION_EDIT (pour la modifier) ou ACTION_PICK (pour choisir un élément disponible dans une URI représentant une collection, comme content://contacts/people).

Par exemple, si vous créez une intention combinant ACTION_VIEW avec l'URI content://contact/people/1 et que vous le passez à Android, ce dernier saura comment trouver et ouvrir une activité capable d'afficher cette ressource.

L'objet Intent

Comme nous venons de le découvrir, un objet Intent véhicule toutes les informations nécessaires à la réalisation d'une action (ou à la réception d'information) :

  1. Le nom du composant ciblé : Cette information facultative permet de spécifier de façon non ambigüe le nom du composant qui sera utilisé pour réaliser l'opération. Si le nom du composant n'est pas renseigné, le système déterminera la composant le plus approprié.
  2. L'action : Une chaîne de caractères définissant l'action à réaliser. Dans le cadre d'un récepteur d'intention, il s'agira de l'action qui s'est produite et pour laquelle le système ou l'application informe toutes les autres.
  3. Les données : Le type de contenu MIME sous la forme d'une chaîne de caractères et le contenu ciblé sous la forme d'un URI. Par exemple, si vous souhaitez afficher une page web, vous utiliserez l'action ACTION_VIEW et une URI de la forme http://<adresse du site>.
  4. Les données supplémentaires : Sous la forme d'une collection de paire clé/valeur, vous pouvez spécifier des paramètres supplémentaires. Par exemple, si l'intention est une demande d'envoi d'un courriel, l'utilisation de la constante EXTRA_EMAIL permet de préciser les adresses de messagerie des destinataires ;
  5. La catégorie : Cette information complémentaire permet de cibler plus précisément qui devra gérer l'intention émise. Par exemple, l'utilisation de la constante CATEGORY_BROWSABLE demande le traitement par un navigateur alors que la constante CATEGORY_LAUNCHER spécifie que l'activité est le point d'entrée de l'application.
  6. Les drapeaux : Principalement utilisés pour spécifier comment le système doit démarrer une activité. Par exemple, l'utilisation de la constante FLAG_ACTIVITY_NO_ANIMATION spécifie que l'activité doit être démarrée sans jouer l'animation de transition entre écrans.

 Naviguer entre écrans au sein d'une application (lancement d'activités et de sous-activités)

Une application est souvent composée de plusieurs écrans qui s'enchaînent les uns à la suite des autres en fonction de l'utilisateur et chaque écran est représenté par une activité définissant son interface utilisateur et sa logique. La principale utilisation d'une intention est le démarrage de ces activités (une à la fois) permettant cet enchaînement. de façon plus générale, chaque composant de votre application nécessitera l'emploi d'une intention pour être démarré.

La théorie sous-jacente de l'architecture de l'interface utilisateur d'Android est que les développeurs devraient décomposer leurs applications en activités distinctes, chacune étant implémentée par une Activity accessible via des intentions, avec une activité principale lancée à partir du menu d'android. Une application de calendrier, par exemple, pourrait avoir des activités permettant de consulter le calendrier, de visualiser un simple événement, d'en modifier un (et d'en ajouter un), etc.

Ceci implique, bien sûr, que l'une de vos activités ait un moyen d'en lancer une autre. Si, par exemple, l'utilisateur clique sur un événement à partir de l'activité qui affiche tout le calendrier, vous voudrez montrer l'activité permettant d'afficher cet événement. Ceci signifie que vous devez pouvoir lancer cette activité en lui faisant afficher un événement spécifique (celui sur lequel l'utilisateur a cliqué).

Création d'intention

Pour démarrer une activité, il est nécessaire d'avoir une intention et de choisir comment la lancer. Comme nous venons de le voir, les intentions encapsulent une requête pour une activité ou une demande adressée à un autre récepteur d'intention, afin qu'il réalise une certaine tâche.

Démarrer une activité

Maintenant que nous disposons de l'intention, il faut la fournir à Android afin de récupérer l'activité fille à lancer.

Il existe deux méthodes pour démarrer une activité, en fonction de la logique de l'interface : parfois, nous avons besoin de savoir comment s'est déroulé l'activité (et obtenir un retour lors de son arrêt), parfois non.

Deux choix sont alors possibles :

  1. Le plus simple consiste à appeler la méthode startActivity() en lui passant l'intention. Android recherchera l'activité qui correspond le mieux et lui passera l'intention pour qu'elle la traite. Dans cette situation, votre activité ne sera jamais prévenue de la fin de l'activité fille, qui dans beaucoup de cas ne pose pas de problème.
  2. Vous pouvez également appeler startActivityForResult() en lui passant l'intention et un identifiant (unique pour l'activité appelante). Android recherchera l'activité qui correspond le mieux et lui passera l'intention. Cette fois-ci, votre activité sera prévenue par la méthode de rappel onActivityResult() de la fin de l'activité fille.
Démarrer une activité fille sans tenir compte du retour

Après tout ce préambule, je vous propose de prendre le premier cas, qui se présente assez souvent, et qui consiste à posséder plusieurs activités dans une même application. Ainsi, une activité principale lancera, au travers des intentions, plusieurs activités filles, dans un premier temps, sans attendre une valeur quelconque de leur part.

Afin de mieux comprendre le lancement des activités filles, je vous propose de réaliser un projet qui réalise des conversions entre les radians et les degrés :

Attention, avant de pouvoir démarrer une activité, il est nécessaire au préalable de la déclarer dans le fichier de configuration AndroidManifest.xml de l'application. Sans cela, une erreur de type ActivityNotFoundException sera générée lors de l'appel de la méthode startActivity().

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.radian"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Angles" android:icon="@drawable/icon">
        <activity android:name="Choix" android:label="Conversion des angles">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Radian" android:label="Recherche du radian" />
        <activity android:name="Degre" android:label="Recherche du degré" />
    </application>
</manifest>

Nous remarquons au travers de ce manifeste que l'activité principale de cette application se nomme Choix. Par ailleurs, deux autres activités sont déclarées, respectivement Radian et Degre qui sont les activités filles de Choix.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px">
   <Button  
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="Radian"
       android:onClick="calculRadian" />
   <Button  
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="Degré"      
       android:onClick="calculDegre" />       
</LinearLayout>



fr.btsiris.radian.Choix.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class Choix extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    
    public void calculRadian(View vue) {
       startActivity(new Intent(this, Radian.class));
    }

    public void calculDegre(View vue) {
       startActivity(new Intent(this, Degre.class));       
    }
}

Cette activité principale est très simple. Elle possède juste les deux méthodes déjà déclarées dans le fichier de description principal et qui s'occupent de lancer les activités filles au travers de la méthode startActivity() et des Intentions respectives.

Maintenant, pour chacune des activités filles, vous devez créer un fichier de description spécifique ainsi que la classe correspondante. Comme beaucoup d'éléments sont en commun, nous allons même créer une classe de base qui factorise tous ces éléments. Ainsi chacune de ces sous-activités héritera de cette classe.

res/layout/radian.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px">
   <EditText
       android:id="@id/degre"
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:inputType="number" />
   <Button  
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="Radian"      
       android:onClick="calcul" />
    <EditText
       android:id="@id/radian"       
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:editable="false" />      
</LinearLayout>
res/layout/degre.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px">
   <EditText
       android:id="@+id/radian"         
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:inputType="numberDecimal" />
   <Button  
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="Degré"      
       android:onClick="calcul" />
   <EditText
       android:id="@+id/degre"       
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:editable="false" />      
</LinearLayout>

Ces deux fichiers se ressemblent beaucoup. Toutefois, remarquez bien que dans le descripteur degre.xml, nous avons la déclaration de nouveaux identifiants, respectivement radian et degre, alors que dans le descripteur radian.xml, nous utilisons simplement ces déclarations, sachant que degre.xml va être compilé en premier (ordre alphabétique).

Nous décrivons ensuite la classe de base abstraite Conversion qui représente la partie commune des deux activités filles, suivi des deux activités qui héritent de cette classe abstraite et qui redéfinissent la méthode calcul().

fr.btsiris.radian.Conversion.java
package fr.btsiris.radian;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import java.text.*;

public abstract class Conversion extends Activity {
   EditText degre, radian;
   NumberFormat formatRadian = new DecimalFormat("0.00");
   NumberFormat formatDegre= new DecimalFormat("0 °");   
   
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      degre = (EditText) findViewById(R.id.degre);
      radian =  (EditText) findViewById(R.id.radian);  
      degre.setText(formatDegre.format(0));
      radian.setText(formatRadian.format(0));       
   }
   
   public abstract void calcul(View vue);
}
fr.btsiris.radian.Radian.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.view.View;
import java.text.*;

public class Radian extends Conversion {

   @Override
   public void onCreate(Bundle icicle) {
      setContentView(R.layout.radian);
      super.onCreate(icicle);    
   }
 
   @Override  
   public void calcul(View vue) {
      try {
         Number valeur = formatDegre.parse(degre.getText().toString());
         radian.setText(formatRadian.format(valeur.doubleValue() * Math.PI / 360));
      } 
      catch (ParseException ex) { }   
   }
}
fr.btsiris.radian.Degre.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.view.View;
import java.text.*;

public class Degre extends Conversion {

   @Override
   public void onCreate(Bundle icicle) {
      setContentView(R.layout.degre); 
      super.onCreate(icicle);     
   }
   
   @Override
   public void calcul(View vue) {
      try {
         Number valeur = formatRadian.parse(radian.getText().toString());
         degre.setText(formatDegre.format(valeur.doubleValue() * 360 / Math.PI));
      } 
      catch (ParseException ex) { }         
   }
}
Lancement simplifiée des sous-activités avec des onglets

Les onglets peuvent contenir une vue ou une activité. Lors de l'étude précédente, nous avons mis en oeuvre les onglets avec des vues distinctes. C'était d'ailleurs relativement complexe à mettre en place, d'une part côté descripteur XML et d'autre part dans le code Java. Toutefois, dans le cas où chaque onglet représente une activité fille, c'est beaucoup plus simple à réaliser.

Vous n'avez plus à générer de descripteur, il suffit de fournir l'intention qui lancera l'activité souhaitée ; le framework de gestion des onglets placera alors automatiquement l'interface utilisateur de cette activité dans l'onglet.

Je vous propose de modifier le projet précédent, en supprimant la première page avec les deux boutons qui réalisent le choix de conversion, et de la remplacer par une gestion des onglets qui effectueront le même type de choix :

Dans ce cas de figure, nous n'avons plus du tout besoin du fichier de description principal main.xml. Toute la mise oeuvre des onglets se fait uniquement dans le code Java au travers de l'activité principale Choix.java.

fr.btsiris.radian.Choix.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TabHost;

public class Choix extends TabActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TabHost onglets = getTabHost();
        onglets.addTab(onglets.newTabSpec("radian")
                                            .setIndicator("Radian")
                                            .setContent(new Intent(this, Radian.class)));
        onglets.addTab(onglets.newTabSpec("degre")
                                            .setIndicator("Degré")
                                            .setContent(new Intent(this, Degre.class)));       
    }
}

Comme vous pouvez le constater, notre classe hérite de TabActivity : nous n'avons donc pas besoin de créer de fichier de description XML car TabActivity s'en occupe pour nous. Nous nous contentons d'accéder au TabHost et de lui ajouter deux onglets, chacun précisant une intention qui fait directement référence à une autre classe. Ici, nos deux onglets hébergeront respectivement un Radian et un Degre.

Vous remarquez également que le bouton de soumission se nomme maintenant tout simplement calcul puisque l'onglet nous précise sur quel traitement nous sommes. Du coup cette description du bouton est identique pour les deux fichiers de description. Je préfère du coup en faire un fichier à part entière que je nomme calcul.xml. Enfin, il n'est plus nécessaire de proposer un intitulé particulier pour chacune de ces sous-activité, je vais donc simplifier le manifeste en conséquence.

res/layout/calcul.xml
<?xml version="1.0" encoding="UTF-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="0px" 
    android:layout_height="wrap_content" 
    android:layout_weight="1"
    android:text="Calcul"      
    android:onClick="calcul" 
 />
res/layout/radian.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px"
>
   <EditText
       android:id="@id/degre"
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:inputType="number"
     />
    <include layout="@layout/calcul" />
    <EditText
       android:id="@id/radian"       
       android:layout_width="0px"
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:editable="false"
    />      
</LinearLayout>
res/layout/degre.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px"
>
   <EditText
       android:id="@+id/radian"         
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:inputType="numberDecimal"
    />    
   <include layout="@layout/calcul" />
   <EditText
       android:id="@+id/degre"       
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:editable="false"
    />      
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.radian"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Angles" android:icon="@drawable/icon">
        <activity android:name="Choix" android:label="Conversion des angles">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Radian"  />
        <activity android:name="Degre"  />
    </application>
</manifest>
Démarrer une activité et obtenir un retour

Comme nous venons de le voir, la méthode startActivity() de la classe Activity permet de démarrer une autre activité "activité enfant". Cette méthode, bien que très utile dans son usage courant, ne propose cependant aucun mécanisme de retour d'infromation vers l'activité "parent" qui a démarré l'activité enfant. En d'autres termes, vous ne serez jamais averti de l'état de l'activité enfant que vous aurez lancée.

Prenons comme exemple une activité enfant proposant à l'utilisateur un formulaire de réponse sur le nombre de chiffres après la virgule pour le calcul des radians. Comment récupérer la valeur saisie par l'uilisateur dans l'activité enfant depuis l'activité principale ?

Pour gérer ce type de scénario, à savoir lancer une activité enfant et en connaître sa valeur de retour, il est possible d'utiliser la méthode startActivityForResult() prévue à cet effet. Avec cette méthode, lorsque l'activité enfant aura terminé sa tâche, elle en avertira l'activité parent.

  1. Spécificité de la méthode d'appel : La méthode startActivityForResult() a besoin de deux paramètres. Le premier correspond à l'intention, le deuxième à une valeur numérique correspondant au code de l'activité parente qui est à votre libre choix. Si la valeur est inférieure à 0, l'activité parent n'attend pas de retour, ce qui équivaut à utiliser la méthode startActivity().
  2. Renvoyer une valeur de retour : Pour renvoyer la valeur de retour à l'activité principale, appelez la méthode setResult() de la classe Activity en indiquant en paramètre le code de retour. Android prévoit plusieurs valeurs par défaut telles que RESULT_OK et RESULT_CANCELED, etc.
  3. Arrêter l'activité enfant : Pour arrêter explicitement une activité enfant afin de revenir sur l'activité parent qui l'a démarré, il suffit de faire appel à la méthode finish() de la classe Activity.
  4. Récupérer la valeur de retour : Pour récupérer la valeur de retour envoyé par une activité enfant, utiliser la méthode onActivityResult() de l'activité parent, depuis laquelle vous avez appelé la méthode startActivityForResult().

La méthode onActivityResult() utilise trois paramètres pour identifier l'activité et ses valeurs de retour :

  1. int requestCode : valeur identifiant quelle activité a appelé la méthode. Cette même valeur a été spécifiée comme deuxième paramètre de la méthode startActivityForResult() lors du démarrage de la sous-activité.
  2. int resultCode : représente la valeur de retour envoyée par la sous-activité pour signaler normalement son état à la fin de la transaction. C'est une constante définie dans la class Activity (RESULT_OK, RESULT_CANCELED, etc.) ou par le développeur.
  3. Intent data : cet objet permet d'échanger des données comme nous le verrons bientôt.

Encore une fois, je vous propose de modifier le projet précédent. Cette fois-ci, nous disposons d'une activité principale qui permet de faire la conversion des angles dans les deux sens et dispose d'un bouton supplémentaire qui permet de choisir le nombre de chiffres après la virgule pour la visualisation des radians. Ce réglage est effectué au moyen d'une activité enfant spécifique :

Dans ce cas de figure, nous disposons maintenant de deux fichiers de descriptions, respectivement main.xml et reglage.xml. Par ailleurs, les deux activités sont représentées par les classes Conversion pour l'activité principale et Reglage pour l'activité enfant. Vous devez donc modifier le manifeste afin de respecter tous ces changements.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="3px">
    <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:padding="2px">   
         <EditText
             android:id="@+id/radian"         
             android:layout_width="0px" 
             android:layout_height="wrap_content" 
             android:layout_weight="2"
             android:inputType="numberDecimal"/>
         <Button 
             android:layout_width="0px" 
             android:layout_height="wrap_content" 
             android:layout_weight="1"
             android:text="Degré"      
             android:onClick="calculDegre" />
    </LinearLayout> 
    <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:padding="2px">        
         <EditText
             android:id="@+id/degre"       
             android:layout_width="0px" 
             android:layout_height="wrap_content" 
             android:layout_weight="2"
             android:inputType="number" />      
         <Button 
             android:layout_width="0px" 
             android:layout_height="wrap_content" 
             android:layout_weight="1"
             android:text="Radian"      
             android:onClick="calculRadian" />       
    </LinearLayout>  
    <Button 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="Nombre de décimales"      
       android:onClick="reglage" />  
</LinearLayout>
res/layout/reglage.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="3px">
   <EditText
       android:id="@+id/reglage"
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="2"
       android:selectAllOnFocus="true"
       android:inputType="number" />
   <Button 
       android:layout_width="0px" 
       android:layout_height="wrap_content" 
       android:layout_weight="1"
       android:text="Valider"      
       android:onClick="valider" 
    />    
</LinearLayout>

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.radian"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Angles" android:icon="@drawable/icon">
        <activity android:name="Conversion" android:label="Conversion des angles">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Reglage"  android:label="Nombre de décimales (Radian)" />
    </application>
</manifest>
fr.btsiris.radian.Conversion.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import java.text.*;

public class Conversion extends Activity {
   EditText degre, radian;
   NumberFormat formatRadian = new DecimalFormat("0.00");
   NumberFormat formatDegre= new DecimalFormat("0 °");   
   
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      setContentView(R.layout.main);
      degre = (EditText) findViewById(R.id.degre);
      radian =  (EditText) findViewById(R.id.radian);  
      degre.setText(formatDegre.format(0));
      radian.setText(formatRadian.format(0));       
   }
   
   public void calculRadian(View vue) {
      try {
         Number valeur = formatDegre.parse(degre.getText().toString());
         radian.setText(formatRadian.format(valeur.doubleValue() * Math.PI / 360));
      } 
      catch (ParseException ex) { }         
   }
   
   public void calculDegre(View vue) {
      try {
         Number valeur = formatRadian.parse(radian.getText().toString());
         degre.setText(formatDegre.format(valeur.doubleValue() * 360 / Math.PI));
      } 
      catch (ParseException ex) { }         
   }
   
   public void reglage(View vue) {
      startActivityForResult(new Intent(this, Reglage.class), 1); // 1 correspond au numéro d'identification de l'activité
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (resultCode>0 && resultCode<=5) {
         try {
            Number valeur = formatRadian.parse(radian.getText().toString());
            String motif = "0";
            for (int i=1; i<resultCode; i++) motif +=  "0";
            formatRadian = new DecimalFormat("0."+motif);           
            radian.setText(formatRadian.format(valeur));
         } 
         catch (ParseException ex) { }
      }  
   }
}
fr.btsiris.radian.Reglage.java
package fr.btsiris.radian;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;

public class Reglage extends Activity {
   private EditText reglage;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.reglage);
       reglage = (EditText) findViewById(R.id.reglage);
   }
   
   public void valider(View vue) {
      setResult(Integer.parseInt(reglage.getText().toString()));
      finish();
   }
}

Comme nous venons de l'évoquer, au travers de la méthode setResult(), nous devons spécifier si l'utilisateur accepte les réglages ou pas en donnant une valeur entière prédéfinie (RESULT_OK, RESULT_CANCELED, etc.). Comme, nous pouvons proposer une valeur entière avec cette méthode, je m'en sers pour envoyer le nombre de chiffres après la virgule pour les radians.

Solliciter d'autres applications

Nous avons déjà utilisé plusieurs fois l'objet Intent pour démarrer des activités (via le nom du type d'activité) au sein d'une même application. Cependant, qu'en est-il si vous désirez faire appel à un composant d'une autre application, par exemple pour ouvrir une page web ? La réponse est : toujours en utilisant une intention.

En effet, l'envoi d'une intention permet également de demander à un composant d'une autre application que la vôtre de traiter l'action que vous souhaitiez réaliser. C'est le système qui décide alors de l'application à utiliser pour accomplir votre souhait. Pour décider du composant le plus approprié, le système se base sur les informations que vous spécifiez dans votre objet Intent : action, données, catégorie, etc.

Ainsi, vous exprimez l'intention au système et le système se chargera de résoudre votre intention pour vous proposer le composant de l'application le plus approprié. Ce mécanisme permet d'éviter les dépendances vers des applications puisque l'association entre votre application et le composant nécessaire se fait au moment de l'exécution et non de la compilation. Cela permet de lancer des activités en fonction du contexte et de ne pas se soucier de l'application qui sera réellement utilisée.

Ce système d'utilisation d'Intent implicite, puisque le système doit résoudre celle-ci en fonction de son environnement, recourt à des filtres (voir plus loin) comme points d'entrée pour distribuer les intentions aux composants les plus appropriés. Les informations qui sont utilisées pour la résolution sont : l'action, les données (L'URI et le type de contenu MIME) et la catégorie. Les autres données ne jouent pas de rôle dans la résolution et la distribution des Intent aux applications.

Routage des intentions

Comme nous l'avons mentionné précédemment, si le composant cible a été précisé dans l'intention, Android n'aura aucun doute sur sa destination - il lancera l'activité en question.

Ce mécanisme peut convenir si l'intention cible se trouve dans votre application, mais n'est pas vraiment recommandé pour envoyer des intentions à d'autres applications car les noms des composants sont globalement considérés comme privés à l'application et peuvent donc être modifiés. Il est donc préférable d'utiliser les modèles d'URI et les types MIME pour identifier les services auxquels vous souhaitez accéder.

Si vous ne précisez pas de composant cible, Android devra trouver les activités (ou les autres récepteurs d'intentions) éligibles pour cette intention. Vous aurez remarqué qu'activités est bien au pluriel, car une action peut très bien se résoudre en plusieurs activités. Cette approche du routage est préférable au routage implicite. Essentiellement, trois conditions doivent être vérifiées pour qu'une activité soit éligible pour une intention donnée :

  1. L'activité doit supporter l'action indiquée.
  2. L'activité doit supporter le type MIME indiqué (s'il a été fourni).
  3. L'activité doit supporter toutes les catégories nommées dans l'intention.

La conclusion est que vous avez intérêt à ce que vos intentions soient suffisamment spécifiques pour trouver le ou les bons récepteurs, mais pas plus. Tout ceci deviendra plus clair à mesure que nous étudierons quelques exemples.

Déléguer au système le choix de l'application

Vous pouvez donc envoyer une intention et demander au système de choisir le composant le plus approprié pour exécuter l'action transmise. Prenons un exemple : nous souhaitons composer un numéro de téléphone. Nous allons utiliser le type d'action ACTION_DIAL (voir la liste des actions natives dans ce chapitre) permettant de demander au système de composer un numéro.

Pour pouvoir demander au système de réaliser cette action, nous créons simplement un nouvel objet Intent dont nous spécifions le type d'action et les valeurs complémentaires dans le constructeur ; et cela sans préciser le type de la classe ciblée. Puis, comme d'habitude, démarrez une nouvelle activité en spécifiant cet objet Intent. A la charge du système, en fonction des filtres d'Intent déclarés par les applications, de déterminer à quel composant d'une quelconque application envoyer cette demande d'action.

  1. Par exemple, pour un numéro de téléphone, l'intention que nous enverrons au système sera composée d'une action et d'une URI comportant le numéro à appeler. Le système déterminera quelle application ouvrir, par défaut l'interface de composition de numéro ou même votre application si vous avez spécifié le filtre d'Intent adéquat :
    Uri téléphone = Uri.parse("tel:0612345678");
    Intent composerNuméro = new Intent(Intent.ACTION_DIAL, téléphone);
    startActivity(composerNuméro);
    
  2. Une URI (Uniform Ressource Identifier), comme son nom l'indique, est un identifiant unique permettant d'identifier une ressource de façon non ambigüe sur un réseau. Le W3C a mis au point une recommandation afin d'en spécifier la syntaxe.

    Le format des URI spécifiés comme données dépendent du type d'action. Quoiqu'il en soit, la composition d'une URI se découpe toujours selon le format : schéma://hote:port/chemin. Le schéma 'tel' permet d'émettre un numéro de téléphone, 'geo' faire de la géolocalisation, 'http' d'ouvrir une page web, etc.

  3. Là aussi, le constructeur de la classe Intent prend deux paramètres. Le premier est une chaîne de caractères (String) correspondant au type d'action à réaliser. Le second paramètre permet de spécifier exactement l'action sous forme d'une Uri.

En plus des actions natives d'Android pouvant être gérées par des applications présentes par défaut sur la plate-forme (comme "appeler un correspondant" ou "ouvrir une page web"), vous pouvez créer vos propres Intents pour étendre les possibilités du système. Pour créer une intention personnalisée, héritez de la classe Intent et redéfinissez les constructeurs et méthodes nécessaires.

Les actions natives

Le système Android propose plusieurs actions natives dont voici une liste non exhaustive :

Action Définition
ACTION_ANSWER Prend en charge un appel entrant. Ceci est couramment géré par l'écran natif des appels.
ACTION_CALL Appeler un numéro de téléphone. Cette action lance une activité affichant l'interface pour composer un numéro puis appelle le numéro contenu dans l'URI spécifié en paramètre. On considère généralement qu'il est préférable d'utiliser plutôt ACTION_DIAL lorsque c'est possible.
ACTION_DELETE Démarrer une activité permettant de supprimer une donnée identifiée par l'URI spécifiée en paramètre.
ACTION_DIAL Afficher l'interface de composition des numéros. Celle-ci peut être pré-remplie par des données contenues dans l'URI spécifiée en paramètre. Le composeur de numéros Android natif est utilisé par défaut. Il peut normaliser la plupart des schémas de numérotation : tel:01 56 60 12 34 et tel:+33 1 56 60 12 34 sont par exemple tous deux valides.
ACTION_EDIT Editer une donnée.
ACTION_INSERT Ouvre une activité capable d'insérer de nouvelles entrées dans le Cursor spécifié dans l'URI de l'Intent. Lorsqu'elle est appelée comme sous-activité, elle doit envoyer une URI vers la nouvelle entrée insérée.
ACTION_PICK Lance une nouvelle sous-activité qui vous permet de choisir un élément du Content Provider (voir dans un prochain chapitre) spécifié par l'URI de l'Intent. Une fois fermée, elle doit renvoyer une URI vers l'élément choisi. L'activité lancée dépend des données à choisir : content://contacts/people invoquera par exemple la liste des contacts natifs.
ACTION_SEARCH Démarre une activité de recherche. L'expression de recherche devra être spécifiée au moyen de la clé SearchManager.QUERY envoyé en extra de l'action.
ACTION_SEND Envoyer des données textuelles ou binaires par courriel ou SMS. Les paramètres dépendront du type d'envoi. Les données elles-mêmes doivent être stockées comme des compléments à l'aide d'EXTRA_TEXT ou EXTRA_STREAM en fonction de leur type. Dans le cas d'un e-mail, les applications Android acceptent également les compléments via les clés EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC et EXTRA_SUBJECT. N'utilisez l'action ACTION_SEND que pour envoyer des données à un destinataire externe et non à une autre application sur l'appareil.
ACTION_SENDTO Lance une activité capable d'envoyer un message au contact défini par l'URI spécifié en paramètre.
ACTION_VIEW Démarre une action permettant de visualiser l'élément identifié par l'URI spécifié en paramètre. C'est l'action la plus commune. Par défaut, les adresses commençant par http: lanceront un navigateur web, celles commençant par tel: lanceront l'interface de composition de numéro et celles débutant par geo: lanceront Google Map.
ACTION_WEB_SEARCH Effectue une recherche sur Internet avec l'URI spécifiée en paramètre comme requête.
Exemple de mise en oeuvre

Dans ce type de scénario, l'activité lancée est plutôt un "pair" de l'activité qui l'a lancée. Elle sera donc lancée comme une activité classique. Votre activité ne sera pas informée de la fin de sa "fille" mais, encore une fois, elle n'a pas vraiment besoin de le savoir.

Afin de mieux comprendre le lancement d'une activité paire, je vous propose de réaliser un projet qui affiche la carte d'un lieu en spécifiant sa localisation à l'aide de la latitude et de la longitude.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="3px">
    <LinearLayout
       android:layout_width="fill_parent"
       android:layout_height="wrap_content">
       <TextView  
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:text="Lieu : "
          android:textStyle="bold"
          android:layout_weight="1"  />
       <EditText
          android:id="@+id/latitude"
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_weight="2"
          android:inputType="numberDecimal"  />          
       <EditText
          android:id="@+id/longitude"
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_weight="2"
          android:inputType="numberDecimal"   />                  
    </LinearLayout>
    <Button
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"  
        android:text="Visualiser !"
        android:onClick="visualiser"  />
</LinearLayout>

Ce fichier de description est assez simple puisqu'il contient deux champs pour la latitude et la longitude, ainsi qu'un bouton pour lancer l'activité paire.

fr.btsiris.geo.Geolocalisation.java
package fr.btsiris.geo;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;

public class Geolocalisation extends Activity {
   private EditText latitude, longitude;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       latitude = (EditText) findViewById(R.id.latitude);
       longitude = (EditText) findViewById(R.id.longitude);
   }
   
   public void visualiser(View vue) {
      String lat = latitude.getText().toString();
      String lon = longitude.getText().toString();
      Uri coord = Uri.parse("geo:"+lat+","+lon);
      startActivity(new Intent(Intent.ACTION_VIEW, coord));
   }
}

Lorsque nous cliquons sur le bouton de visualisation, l'activité de cartographie intégrée à Android est automatiquement lancée avec les coordonnées de géolocalisation (latitude et longitude). Pour cette application, nous n'avons pas eu besoin de créer une activité fille pour afficher cette carte. Nous utilisons les compétences de l'environnement.

Accorder les permissions liées aux actions

Certaines actions requièrent des privilèges spéciaux qui vous amènerons à ajouter des permissions dans le fichier de configuration de votre application.

  1. Par exemple, pour émettre un appel depuis votre application, il ne suffira pas uniquement de créer un objet Intent avec l'action ACTION_CALL :
    Uri téléphone = Uri.parse("tel:0612345678");
    Intent composerNuméro = new Intent(Intent.ACTION_CALL, téléphone);
    startActivity(composerNuméro);
  2. l vous faudra également ajouter la permissiondans la partie <manifest> du fichier de configuration de l'application :
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:label="@string/app_name" android:icon="@drawable/icon"> <activity android:name="Test" android:label="@string/app_name"> ... </activity> </application> <uses-permission android:name="android.permision.CALL_PHONE" /> </manifest>

Filtrer les actions

Android permet aux applications de spécifier quelles sont les actions qu'elles gèrent : le système peut ainsi choisir le composant le mieux adapté au traitement d'une action (véhiculée dans un objet Intent).

Tous les composants Android qui souhaitent être prévenus par des intentions doivent déclarer des filtres d'intention afin qu'Android sache quelles intentions doivent aller vers quel composant. Pour ce faire, vous devez ajouter des éléments <intent-filter> au manifeste de l'application.

Le script de création des applications Android (activityCreator ou son équivalent dans un IDE comme NetBeans) fournit des filtres d'intention à tous les projets. Voici ci-dessous le manifeste du dernier projet que nous avons mis en oeuvre :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="fr.btsiris.tache" android:versionCode="1" android:versionName="1.0"> <application android:label="@string/app_name" android:icon="@drawable/icon"> <activity android:name="Tache" 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>
  1. Notez la présence de l'élément <intent-filter> sous l'élément <activity>.
  2. Il annonce que cette activité est l'activité principale de cette application.
  3. De plus, elle appartient à la catégorie LAUNCHER, ce qui signifie qu'elle aura une icône dans le menu principal d'Android.

Cette activité étant l'activité principale de l'application, Android sait qu'elle est le composant qu'il devra lancer lorsqu'un utilisateur choisit cette application à partir du menu principal.

Vous pouvez bien sûr indiquer plusieurs actions ou catégories dans vos filtres d'intention afin de préciser que le composant associé (l'activité) gère plusieurs sortes d'intentions différentes.

Il est fort probable que vous voudrez également que vos activités secondaires (non principales) précisent le type MIME des données qu'elles manipulent. Ainsi, si une intention est destinée à ce type MIME, directement ou indirectement via une URI référençant une ressource de ce type, Android saura que le composant sait gérer ces données. Chaque élément de la balise <intent-filter> est important puisqu'il détermine le niveau de filtrage :

<activity ...>
    <intent-filter>
         <action android:name="android.intent.action.VIEW" />
         <category android:name="android.intent.category.DEFAULT" />
         <category android:name="android.intent.category.BROWSABLE" />
         <data android:scheme="demo" />
    </intent-filter>
</activity>
  1. action : identifiant unique sous forme de chaîne de caractères. Il est d'usage d'utiliser la convention de nom Java. Utilise l'attribut android:name pour spécifier le nom de l'action à laquelle répondre.
  2. category : premier niveau de filtrage de l'action. Cette balise indique dans quelle circonstance l'action va s'effectuer ou non. Il est possible d'ajouter plusieurs balises de catégorie. Utilise l'attribut android:name pour spécifier dans quelles circonstances une réponse doit être donnée à l'action.
  3. data : filtre l'objet Intent au niveau des données elles-mêmes. Par exemple, en jouant avec l'attribut android:host, nous pouvons répondre à une action comportant un nom de domaine particulier, comme www.site.com. Cette balise vous permet ainsi de spécifier sur quels types de données vos composants peuvent agir. Vous pouvez inclure plusieurs balises <data> selon vos besoin.

Grâce à la définition de ce filtre, l'activité déclarée par le fichier de configuration de l'application ci-dessus réagira à l'action du code suivant :
.

Uri valeurs = Uri.parse("demo://Ceci est une chaîne de caractères");
Intent composerNuméro = new Intent(Intent.ACTION_VIEW, valeurs);
startActivity(composerNuméro);

Vous noterez qu'encore une fois, nous n'avons pas spécifié la classe du composant à utiliser pour cette action, ni explicitement quelle activité démarrer. C'est le système qui détermine seul le composant le mieux adapté en interrogeant les filtres définis dans les différents fichiers de configuration des applications installées.

Toutes les catégories associéesà la balise <category>
  1. ALTERNATIVE : Cette catégorie spécifie que l'action doit être disponible comme une alternative à l'action par défaut effectuée sur un élément de ce type de données. Par exemple, si l'action par défaut sur un contact est de l'affiché, l'alternative pourraît être de l'éditer.
  2. SELECTED_ALTERNATIVE : Semblable à la catégorie précédente mais, alors que cette dernière effectuera toujours la résolution en une seule action en utilisant la résolution de l'Intent décrite à la suite, SELECTED_ALTERNATIVE sera utilisée lorsque plusieurs possibilités seront requises. Comme nous le verrons ultérieurement, l'un des usages est d'aider à remplir dynamiquement les menus contextuels à l'aide d'actions.
  3. BROWSABLE : Spécifie une action disponible dans le navigateur. Lorsqu'un Intent est déclenché depuis celui-ci, il comprend toujours cette catégorie. Vous devez inclure cette catégorie si vous voulez que votre application réponde aux actions déclenchées dans le navigateur (intercepter les liens vers un site web particulier, par exemple).
  4. DEFAULT : Fait d'un composant l'action par défaut pour le type de données spécifié. Ceci est nécessaire pour les activités lancées en utilisant un Intent explicite.
  5. GADGET : En utilisant cette catégorie, vous spécifiez que l'activité peut être exécutée au sein d'une autre activité.
  6. HOME : En utilisant cette catégorie sans spécifier d'action, vous la présentez comme une alternative à l'écran d'accueil natif.
  7. LAUNCHER : L'utilisation de cette catégorie fait apparaître l'activité dans le lanceur d'applications.
Tous les attributs possibles associés à la balise <data>
  1. android:host : Spécifie un nom d'hôte valide (www.site.fr, par exemple).
  2. android:mimetype : Vous permet de spécifier le type de données que votre composant est capable de prendre en charge.
  3. android:path : Spécifie les valeurs du chemin valides pour l'URI.
  4. android:port : Spécifie les ports valides pour l'hôte.
  5. android:scheme : En utilisant cette catégorie, vous spécifiez que l'activité peut être exécutée au sein d'une autre activité.
Exploiter l'objet Intent de l'activité

Une fois le filtre de l'Intent défini et opérationnel, quelques lignes de code suffiront pour traiter l'Intent transmis. Si le composant n'est pas démarré, le système s'occupe de créer automatiquement une nouvelle instance du composant pour traiter l'intention. La classe Intent possède deux méthodes intéressantes pour récupérer les données envoyées sous forme d'URI :

  1. La méthode getDataString() où nous pouvons consulter la chaîne de caractères au complet représentant l'URI.
  2. La méthode getData() qui nous renvoie cette fois-ci un objet de type Uri. Cette classe est généralement plus intéressante à utiliser puisqu'elle même dispose de méthodes plus spécifique pour récupérer une partie de l'URI soumise. Elle ressemble à ce sujet à la classe URL du Java standard que nous connaissons bien.
Exemple de mise en oeuvre

Afin de valider cette notion de filtre, je vous propose d'en créer un sur une sous-activité. Nous allons reprendre le projet sur le calcul des angles et nous allons élaborer un filtre uniquement sur l'activité Radian. Cette sous-activité sera donc automatiquement affichée avec la valeur en degré soumise par une autre application qui servira au calcul et qui sera également automatiquement lancée.

Vous avez ci-dessous un exemple très rudimentaire d'une autre application qui veut utiliser cette sous-activité pour calculer la valeur en radian d'un angle exprimé en degré.

fr.btsiris.test.LancerAutreActivite.java
package fr.btsiris.test;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

public class LancerAutreActivite extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Uri commande = Uri.parse("radian://?degre=60");
        Intent radian = new Intent(Intent.ACTION_EDIT, commande);
        startActivity(radian);
    }
}  
  1. Lorsque je soumets ma nouvelle intention dans cette application au travers de l'activité LancerAutreActivite, je demande à lancer l'action d'édition à l'aide de la constante Intent.ACTION_EDIT.
  2. J'aurais tout-à-fait pu demander plutôt une action de simple visualisation à l'aide de la constante Intent.ACTION_VIEW.
  3. L'Uri radian://?degre=60 proposée pour soumettre les données est à la fois standard, mais également personnalisée. Effectivement le schéma est un schéma personnel et se nomme "radian". Pour le reste de l'Uri, nous retrouvons une écriture plus classique avec le point d'intérogation, un paramètre (degre) ainsi que sa valeur (60).
  4. La particularité dans cette Uri, c'est que nous n'avons aucune action proposée avant le point d'interrogation. Il est tout-à-fait possible de proposer alors l'Uri suivante en ayant exactement la même réaction au niveau de la sous-activité : radian://calcul?degre=60.
  5. A ce sujet, nous aurions pu prendre un autre exemple où c'est l'activité principale, le calcul des angles, qui prend en compte l'intention et qui redirige la requête vers la bonne sous-activité pour réaliser le traitement souhaité. Dans ce cas, il aurait été judicieux de proposer une Uri de la forme suivante pour effectuer un calcul qui permet de connaître une valeur en radian en partir d'un angle en degré : angle://radian?degre=60.
  6. Cela vous montre toutes les possibilités. Android est vraiment très riche à ce sujet et ouvre plein de perspectives intéressantes.

Voici toutes les modifications à apporter, d'une part dans le manifeste de l'application concernant le calcul des angles et dans le code Java propre à la sous-activité Radian.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.radian"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Angles" android:icon="@drawable/icon">
        <activity android:name="Choix" android:label="Conversion des angles">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Radian" android:label="Recherche du radian">
            <intent-filter>
                <action android:name="android.intent.action.EDIT" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="radian" />
            </intent-filter>           
        </activity>
        <activity android:name="Degre"  />
    </application>
</manifest>

Remarquez bien la présence du filtre d'intention sur la sous-activité Radian. Ce filtre stipule que la sous-activité sera automatiquement démarrée qu'à partir d'action de type Edit avec une catégorie par défaut (vous devez proposer impérativement une catégorie) et un schéma personnalisé nommé radian.

fr.btsiris.radian.Radian.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.view.View;
import java.text.*;

public class Radian extends Conversion {

   @Override
   public void onCreate(Bundle icicle) {
      setContentView(R.layout.radian);
      super.onCreate(icicle);  
      Uri requete = getIntent().getData();     
      if (requete!=null) {
         String valeur = requete.getQueryParameter("degre");
         degre.setText(valeur+" °");
         calcul(degre);
      }  
   }
 
   @Override  
   public void calcul(View vue) {
      try {
         Number valeur = formatDegre.parse(degre.getText().toString());
         radian.setText(formatRadian.format(valeur.doubleValue() * Math.PI / 360));
      } 
      catch (ParseException ex) { }   
   }
}
  1. Cette activité peut être appelée normalement par son activité principale ou même par une autre activité d'une autre application. Il est donc nécessaire de savoir comment cette sous-activité a été générée.
  2. Il faut donc connaître l'intention de l'activité, au moyen de la méthode getIntent() et aussi savoir si une Uri a été proposée, au moyen de la méthode getData(). Dans l'affirmative, c'est une autre application extérieure qui réclame son service.
  3. Dans ce dernier cas de figure, il faut récupérer la valeur de l'angle en degré donné par l'Uri au moyen de la méthode getQueryParameter() et soumettre cette valeur pour le calcul, après avoir formaté correctement la zone de saisie des degrés.

Démarrer un service

Les services seront traités dans un des chapitres suivants. Ceci dit, comme pour une activité, pour démarrer un service, utilisez la méthode startService() d'un objet de type Context. L'unique paramètre de cette méthode est un objet Intent représentant l'intention ciblant soit un service précis, soit une action :

Intent intention = new Intent(this, ServiceDemarrer.class);
startService(intention);

Embarquer des données supplémentaires

Lorsque vous demandez à un autre composant de réaliser une action, vous devrez bien souvent spécifier d'autres données supplémentaires. Par exemple, si vous souhaitez ouvrir une activité contenant un navigateur web, il y a de grande chance que cette dernière attende de vous une adresse web.

A cette fin, la classe Intent dispose de méthodes grâce auxquelles un objet de type Bundle véhiculera vos données d'une activité à l'autre. Ainsi, l'insertion des données dans ce conteneur se fait au moyen de la méthode putExtra() et l'extraction au moyen de la méthode getExtras().

  1. L'échange des données se fait par l'association d'une clé (représentée par une chaîne de caractères) à une donnée à échanger.
  2. Pour ajouter chaque donnée à échanger, il faut appeler la méthode putExtra() avec la clé et la donnée à échanger. La méthode putExtra() dispose d'une surcharge pour pratiquement tous les types de données primitifs : int, double, String, etc.
  3. Ainsi, pour définir les données à échanger lors du démarrage d'une activité, la méthode putExtra() permet d'ajouter une information à l'action qui sera ensuite transmise à l'activité cible :
    Intent intention = new Intent(this, ServiceDemarrer.class);
    intention.putExtra("maclé", valeur);
    startService(intention);
  4. Une fois l'intention reçue par l'application, via une instance de la classe Intent, vous pourrez récupérer les données contenues dans l'objet Bundle transmis à l'aide de la méthode getExtras().
  5. Une fois l'objet Bundle récupéré, appelez l'une des méthodes getXXX() correspondant au type de la donnée recherchée - getString(), getInt(), getDouble(), etc. - en spécifiant le nom de la clé associée pour récupérer les informations :
    Bundle données = getIntent().getExtras();     
    if (données!=null) {
       int valeur = données.getInt("maclé");
       ...
    }  
  6. Il existe des méthodes encore plus rapides (getIntExtra(), getDoubleExtra(), getStringExtra(), etc.) qui permettent de récupérer une valeur à partir d'une clé sans passer par la classe Bundle, et qui permettent en même temps de proposer une valeur par défaut dans le cas où la valeur n'a pas été envoyée :
    int valeur = getIntent().getIntExtra("maclé", 18);  
Je vous propose de reprendre un des projets sur le calcul des angles en faisant en sorte que lorsque nous sortons d'un type de calcul, par exemple les radians, nous récupérons automatiquement la valeur calculée pour la soumettre à l'autre type de calcul, par exemple les degrées :

Beaucoup de fichiers ne change pas. Ceux qui sont à prendre en compte pour respecter ce nouveau cahier des charges concerne l'activité principale Choix et les deux sous-activités permettant de réaliser les conversions, savoir Radian et Degre.

fr.btsiris.radian.Choix.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class Choix extends Activity {
   private Intent radian, degre;
   private final int CALCUL_RADIAN =  1;
   private final int CALCUL_DEGRE = 2;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);   
      radian = new Intent(this, Radian.class);
      degre = new Intent(this, Degre.class);        
    }
    
    public void calculRadian(View vue) {
       radian.putExtra("degre", degre.getIntExtra("degre", 0));
       startActivityForResult(radian, CALCUL_RADIAN);    
    }

    public void calculDegre(View vue) {
       degre.putExtra("radian", radian.getDoubleExtra("radian", 0.0)); 
       startActivityForResult(degre, CALCUL_DEGRE);           
    }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (resultCode==RESULT_OK) 
         switch (requestCode) {
            case CALCUL_RADIAN : radian = data; break;
            case CALCUL_DEGRE  : degre = data;  break;
         }
   }
}    
  1. Dans les méthodes calculRadian() et calculDegre(), nous envoyons dans l'intention concernée la valeur supplémentaire, au moyen de la méthode putExtra(), qui tient compte du calcul effectué dans l'autre type de calcul, au moyen des méthodes respectives getIntExtra() et getDoubleExtra().
  2. Pour ces deux dernières méthodes, si aucun calcul antérieur n'a pas encore été proposé, une valeur par défaut est alors fournie.
  3. Toujours à l'intérieur de ces deux méthodes de calcul, le lancement de la sous-activité requise est ensuite effectué avec une attente d'un résultat possible, au moyen de la méthode startActivityForResult().
  4. La méthode onActivityResult() est alors redéfinie afin de permettre la récupération de la valeur de retour de la sous-activité qui se traduit tout simplement par la prise en compte de la nouvelle intention qui sera générée dans la sous-activité.
fr.btsiris.radian.Radian.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.view.View;
import java.text.*;

public class Radian extends Conversion {
   
   @Override
   public void onCreate(Bundle icicle) {
      setContentView(R.layout.radian);
      super.onCreate(icicle);    
   }

   @Override
   protected void onResume() { 
      super.onResume();
      int valeur =getIntent().getIntExtra("degre", 0);
      degre.setText(formatDegre.format(valeur));
      radian.setText(formatRadian.format(valeur * Math.PI / 360)); 
   }
   
   @Override  
   public void calcul(View vue) {
      try {
         Number valeur = formatDegre.parse(degre.getText().toString());
         double resultat = valeur.doubleValue() * Math.PI / 360;
         radian.setText(formatRadian.format(resultat));       
         getIntent().putExtra("radian", resultat);
         setResult(RESULT_OK, getIntent());    
      } 
      catch (ParseException ex) { setResult(RESULT_CANCELED);  }   
   }
}
fr.btsiris.radian.Degre.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.view.View;
import java.text.*;

public class Degre extends Conversion {

   @Override
   public void onCreate(Bundle icicle) {
      setContentView(R.layout.degre); 
      super.onCreate(icicle);     
   }

   @Override
   protected void onResume() {   
      super.onResume();
      double valeur = getIntent().getDoubleExtra("radian", 0.0);
      radian.setText(formatRadian.format(valeur));
      degre.setText(formatDegre.format(valeur * 360 / Math.PI));
   }

   @Override
   public void calcul(View vue) {
      try {
         Number valeur = formatRadian.parse(radian.getText().toString());
         int resultat = (int) (valeur.doubleValue() * 360 / Math.PI);
         degre.setText(formatDegre.format(resultat));
         getIntent().putExtra("degre", resultat);
         setResult(RESULT_OK, getIntent());        
      } 
      catch (ParseException ex) { setResult(RESULT_CANCELED); }         
   }
}  
  1. A chaque fois que la sous-activité doit repassée en premier plan, la méthode onResume(), intégrée dans le cycle de vie d'une activité, est appelée.
  2. Nous profitons de l'occasion pour récupérer la valeur supplémentaire proposée avec l'intention, au moyen des méthodes respectives getDoubleExtra() et getIntExtra().
  3. A chaque fois qu'un calcul est demandé, nous mettons à jour l'intention avec la prise en compte du nouveau calcul au moyen de la méthode putExtra(), et nous soumettons cette intention comme résultat de la sous-activité au moyen de la méthode setResult().

Transférer une intention

Si votre application réceptionne un Intent comportant une action que vous ne traitez pas ou que vous ne souhaitez pas traiter dans un contexte particulier, vous pouvez décider de transmettre celui-ci à une autre activité plus adapté pour gérer l'action. Pour passer la main à une autre activité, utilisez la méthode startNextMatchingActivity() :

Intent intention = getIntent();     
Uri données = intention.getData();     
if (données!=null) {
   startNextMatchingActivity(intention);
   ...
}  

Diffuser et recevoir des intentions

Au-dela de demander la réalisation d'une action par une activité précise, l'échange d'intention forme un moyen de communication évolué capable de gérer des actions au niveau de l'ensemble du système.

Un Intent peut être diffusé à l'ensemble des applications du système pour transmettre des informations à destination des autres applications, que ce soit pour demander la réalisation d'actions spécifiques, comme nous l'avons vu, ou pour fournir des informations sur l'environnement (appel entrant, réseau Wi-Fi connecté, etc.).

Ce mécanisme de diffusion permet à chaque processus de communiquer un état spécifique, une information ou une action de manière homogène à l'ensemble de ses pairs. Dans ce contexte, ces intentions sont appelés des Broadcast Intents alors que ses récepteurs sont appelés les Broadcast Receiver ou "récepteur d'intentions".

Android utilise très largement ce type d'intention pour communiquer des informations système (niveau de la batterie, état du réseau, etc.) aux applications. Le système de filtrage quant à lui reste identique mais nous allons voir que la manière de traiter l'information est quelque peu différente. Cela dit, elle ouvre des possibiltés intéressantes pour la création des services.

Diffuser des intentions à but informatif

La première étape pour comprendre cette mécanique de diffusion consiste à envoyer un Intent au système grâce à la méthode sendBroadcast() d'une activité. L'essentiel ici est de bien définir cet objet, sinon vous risquez de parasiter le système ou d'obtenir des comportements non désirés (ouverture d'applications non sollicitées, etc.).

Intent intention = new Intent(this, Récepteur.VIEW);
intention.putExtra("maclé", valeur);
sendBroadcast(intention);

Recevoir et traiter des intentions diffusées

La seconde étape consiste à définir un filtre d'intentions dans le fichier de configuration de l'application puis à écouter le flux de messages grâce à un objet BroadcastReceiver. A chaque fois que cet écouteur recevra un message, le système appelera sa méthode onReceive().

Création d'un BroadcastReceiver
public final class Récepteur extends BroadcastReceiver {
   private static final String VIEW = "btsiris.intent.action.VIEW";

   @Override
   public void onReceive(Context contexte, Intent intention) {
		...
   }
}  

L'application n'a pas besoin d'être en cours d'exécution pour que le filtre fonctionne. Si l'application n'est pas lancée mais qu'un Intent correpondant au filtre est diffusé, une nouvelle instance de l'application sera automatiquement démarrée par le système.

De la même manière que pour les activités, vous devez déclarer un filtre correspondant à l'action définie dans le BroadcastReceiver dans le fichier de configuration de l'application.

<application ...>
   <receiver android:name="android.intent.action.VIEW">
      <intent-filter>
         <action android:name="btsiris.intent.action.VIEW" />
         <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
   </receiver>
</application>

Notez la présence de la balise <receiver> indiquant que l'application dispose d'un écouteur "permanent". Permanent car il est également possible d'ajouter et de supprimer un écouteur d'intentions dynamiquement pendant l'exécution d'une application. Cela offre l'opportunité de filtrer à la volée les messages et ainsi d'étendre les capacités de l'application.

Créer un récepteur d'intentions dynamiquement

Pour créer un récepteur d'intentions pendant l'exécution d'une application, créez tout d'abord une classe dérivant de BrocastReceiver. Une fois cette classe créée et instanciée, vous devez encore enregistrer le récepteur auprès du système via la méthode registerReceiver() de la classe Activity.

Cette méthode requiert deux arguments qui reprennent les paramètres de la balise <receiver> dans le fichier de configuration de l'application, à savoir spécifier une nouvelle instance de l'objet BroadcastReceiver ainsi que le filtre d'intentions associé.

BroadcastReceiver récepteur = new Récepteur();
IntentFilter filtre = new IntentFilter(Récepteur.CALL);
registerReceiver(récepteur, filtre);

Lorsque l'activité a fini son exécution ou bien que le récepteur n'est plus utilisé, vous devez libérer les ressources correspondantes en appelant la méthode unregisterReceiver(récepteur).

Les messages d'informations natifs

Android, à l'instar des actions, diffuse plusieurs actions préfinies et utilisées pour communiquer sur l'état d'un service ou le changement d'un composant système. Vous pouvez utiliser ces messages pour ajouter des fonctionnalités à vos propres projets en vous fondant sur des événements du système comme le changement de fuseau horaire, le status de la connexion de données, les messages SMS entrants ou les appels téléphoniques.

La liste qui suit présente quelques-unes des actions natives exposées sous forme de constantes dans la classe Intent. Ces actions sont principalement utilisées pour tracer les changements dans l'état de l'appareil.

Action Définition
ACTION_BOOT_COMPLETED Est diffusée lorsque le système a fini de démarrer.
ACTION_CAMERA_BUTTON Déclenchée lorsque le bouton de l'appareil photo est appuyé.
ACTION_DATE_CHANDED
et
ACTION_TIME_CHANDED
Ces actions sont diffusés si la date et l'heure sont manuellement modifié (et non par le cours inexorable du temps).
ACTION_MEDIA_BUTTON Déclenchée si le bouton média est enfoncé.
ACTION_MEDIA_EJECT Si l'utilisateur décide d'éjecter le périphérique de stockage externe, cet événement est d'abord déclenché. Si votre application lit ou écrit sur ce priphérique, vous devez être à l'écoute de cet événement pour sauvegarder et fermer tous les fichiers ouverts.
ACTION_MEDIA_MOUNTED
et
ACTION_MEDIA_UMOUNTED
Ces deux événements sont diffusés à chaque fois qu'un nouveau périférique de stockage externe est ajouté avec succès ou supprimé de l'ppareil.
ACTION_NEW_OUTGOING_CALL Diffusée lorsqu'un nouvel appel sortant est sur le point d'être effectué. Soyez à l'écoute de cet événement pour intercepter les appels sortants. Le numéro en cours de composition est stocké dans le complément EXTRA_PHONE_NUMBER, et, dans l'Intent envoyé, resultData contiendra le numéro effectivement composé. Pour enregistrer un BroadcastReceiver pour cette action, votre application doit déclarer la permission ACTION_OUTGOING_CALLS.
ACTION_SCREEN_OFF
et
ACTION_SCREEN_ON
Diffusées respectivement lorsque l'écran s'éteint ou s'allume.
ACTION_TIMEZONE_CHANGED Cette action est diffusée à chaque fois que le fuseau horaire du téléphone change. L'Intent inclut un complément time-zone qui renvoie l'identifiant de la nouvelle java.util.TimeZone.
ACTION_POWER_CONNECTED
et
ACTION_POWER_DISCONNECTED
Diffusées lorsque l'alimentation électrique a été branchée / débranchée.
ACTION_PACKAGE_ADDED
et
ACTION_PACKAGE_REMOVED
Permet de prendre en charge un appel entrant.
ACTION_SHUTDOWN Indique que le système est en train de s'éteindre. Seul le système peut envoyer ce type de message.

 

Choix du chapitre Alerter les utilisateurs avec des notifications

Les notifications représentent un moyen standard et efficace de prévenir l'utilisateur d'un événement. Chaque notification est ajoutée comme une nouvelle entrée dans le menu de la barre de statut étendue, avec pour chacune une icône associée. Les notifications permettent d'alerter et de garder un historique des dernières notifications dont l'utilisateur n'a pas encore pris connaissance. Afin d'être plus efficace, une notification sera souvent combinée à un effet sonore : émission d'un son, utilisation d'un vibreur du téléphone, etc.

Les possibilités sont donc nombreuses mais l'objectif des notifications reste d'alerter l'utilisateur et non, comme le propose le système des toasts, de lui communiquer des informations. L'utilisation des notifications permet d'attirer l'attention d'un utilisateur ayant placé son téléphone à l'écart de son bureau ou dans sa poche. C'est pourquoi le mécanisme des notifications est souvent le moyen privilégié utilisé par la plupart des services qui ne gère pas de l'information (batterie faible, alarme programmée, notification de rendez-vous, réception SMS ou de courier éléctronique), pour alerter l'utilisateur d'un quelconque événement.

Les notifications sont gérées par un gestionnaire central de notifications de type NotificationManager. Les applications utilisent la barre de status du système pour afficher les notifications. Cette barre est par défaut rétractée, mais l'utilisateur peut la dérouler pour afficher toutes les notifications.

Types d'avertissements

Un service qui s'exécute en arrière plan doit pouvoir attirer l'attention des utilisateurs lorsqu'un événement survient - la réception d'un courrier, par exemple. En outre, le service doit également diriger l'utilisateur vers une activité lui permettant d'agir en réponse à cet événement - lire le message reçu, par exemple. Pour ce type d'action, Android fournit des icônes dans la barre d'état, des avertissements lumineux et d'autres indicateurs que nous désignons globalement par le terme de notification.

Votre téléphone actuel peut posséder ce type d'icône pour indiquer le niveau de charge de la batterie, la force du signal, l'activation de Bluetooth, etc. Avec Android, les applications peuvent ajouter leurs propres icônes dans la barre d'état et faire en sorte qu'elles n'apparaissent que lorsque cela est nécessaire (lorsqu'un message est arrivé, par exemple).

Comme nous l'avons indiqué dans l'introduction de ce chapitre, vous pouvez lancer des notifications via le NotificationManager, qui est un service du système. Pour l'utiliser, vous devez obtenir l'objet service via un appel à la méthode getSystemeService(Context.NOTIFICATION_SERVICE) de votre activité.

  1. Le gestionnaire de notifications NotificationManager vous offrent trois méthodes : une pour avertir notify() et deux pour arrêter d'avertir : cancel() et cancelAll().
  2. La méthode notify() prend en paramètre un objet Notification, qui est une structure décrivant la forme que doit prendre l'avertissement. Les possibilités de cet objet sont décrites dans les sections qui suivent.

Créer une notification

  1. La création d'une notification se fait tout d'abord en créant une instance de type Notification. Une notification possède trois caractéristiques : une icône, un texte défilant et une heure (sous la forme d'un timestamp) :
    Notification notification = new Notification(R.drawable.CALL, "Mon message défilant", System.currentTimeMillis());

    Bonne pratique : Limiter l'utilisation d'un texte défilant : Dans la pratique, l'utilisation d'une icône attire suffisamment l'attention de l'utilisateur, ne cedez pas à la tentation d'y ajouter inutilement le défilement du texte. De façon générale, mieux vaut changer l'icône uniquement et mettre le paramètre de défilement à null.

  2. L'instanciation d'un objet de type Notification ne fait que créer la notification et lui affecter une îcone et une heure : cette opération ne configure pas le contenu de la notification qui sera affichée à l'utilisateur. Chaque notification possède en effet un titre et un détail en plus des autres attributs. Pour configurer la notification avec ces informations, utilisez la méthode setLatestEventInfo().
  3. Lorsque l'utilisateur visualisera la notification, il doit pouvoir revenir sur l'activité émettrice de la notification. Pour cela, vous devez utiliser un type spécifique d'intention, le PendingIntent. Ce type d'intention sera lancé en différé dès que l'utilisateur aura cliqué sur la notification, ce qui entraînera le retour au premier plan de l'activité qui a soumise le message même si l'application de cette dernière n'est même plus en fonctionnement. PendingIntent permet donc de lancée une activité après coup, si besoin, d'où cette notion de différé (qui peut être d'ailleurs utilisé dans d'autres contextes bien entendu) :
    PendingIntent appelDifféré = PendingIntent.getActivity(this, 0, MonActivité.class, 0);
    notification.setLatestEventInfo(this, "Titre de la notification", "Message de la notification", appelDifféré);

    MonActivité.class correspond à l'activité qui soumet la notification où éventuellement à une activité annexe qui permet de réaliser des actions spécifiques après coup pour résoudre l'alerte proposée par la notification.

  4. Pour envoyer la notification, vous devez appeler la méthode notify() du NotificationManager. Cette méthode prend en argument un identifiant unique de référence, permettant d'identifier par la suite une notification, qui sera à nouveau utilisée pour annuler ultérieurement la notification.
    NotificationManager gestionnaire = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);  // Accès à l'instance du gestionnaire de notifications
    final int NOTIFICATION_ID = 1234;
    ...
    gestionnaire.notify(NOTIFICATION_ID, notification);

Supprimer une notification

La suppression/annulation d'une notification entraîne la disparition de l'entrée dans le menu de la barre de statut ainsi que l'icône associée. Pour annuler une notification, appelez simplement la méthode cancel() du gestionnaire de notification en spécifiant l'identifiant de référence de la notification que vous avez communiqué à la méthode notify().

gestionnaire.cancel(NOTIFICATION_ID);

Modifier et gérer les notifications

De façon générale, mettez à jour les notifications existantes plutôt que d'en recréer systématiquement de nouvelles. Si vous créez de nouvelles notifications, cela ne rendra que plus confuse votre application.

Vous pouvez également utiliser une numérotation pour spécifier à l'utilisateur que votre application a plusieurs notifications en attente. Pour cela, utiliser la propriété number de la notification et incrémentez-la. De cette façon, l'icône que vous avez spécifiée dans le constructeur de la notification sera complétée par un nombre précisant à l'utilisateur le nombre de messages en attente. Si vous spécifiez une valeur de 0, la valeur sera retirée de l'icône.

notification.number = 2;

Comme pour toute modification d'une notification, notifiez à nouveau le système avec la méthode notify() du NotificationManager.
.

Enrichir les notifications

Créer des notifications ou des toasts suffit rarement à attirer l'attention de l'utilisateur, il faut souvent y ajouter un effet sonore. Ces effets sont plus adaptés aux notifications, puisque l'objectif des toasts est justement de rester discret et de perturber l'utilisateur le moins possible dans son utilisation.

Il existe trois moyens de notifier un utilisateur avec un téléphone Android : proposer une sonnerie, utiliser le vibreur ou encore un clignotement de la LED de votre téléphone.

L'émulateur actuel d'android n'indique pas de façon visible ou audible que l'appareil vibre ou clignote.
.

Alerte sonore

Utiliser ce type d'alerte pour notifier l'utilisateur d'un événement (comme un appel entrant) est une technique antérieure à la téléphonie mobile et qui résiste à l'épreuve du temps. La plupart des événements natifs du téléphone, depuis les appels entrants jusqu'aux messages et au signal d'une batterie faible, sont annoncés par des sonneries.

Android vous permet d'exécuter n'importe quel morceau de musique que vous pouvez copier sur l'appareil ou incluez-le comme une ressource brute dans votre projet.

Android vous permet ainsi de jouer n'importe quel fichier audio de votre téléphone, spécifiez l'URI vers celui-ci à la propriété sound, pour lire ce son lors de l'émission de la notification via le NotificationManager.

Uri sonnerie = Uri.parse(getResources().getResourceName(R.raw.bart_hi_caramba)); 
notification.sound = sonnerie;
Faire vibrer le téléphone

La vibration représente un bon compromis : elle n'émet pas de son ou très peu, peut se sentir dans la main ou dans une poche et, à l'extrême, faire se déplacer visuellement le téléphone sur votre bureau.

Vous pouvez effectuer un type de vibration spécifique à votre notification. Android permet de déterminer la séquence de vibration comme vous l'entendez en spécifiant un délai de vibration et un délai de pause de façon successive.

Par exemple, si vous souhaitez définir deux foix une seconde de vibration avec une demi-seconde de pause intermédiaire, vous allez spécifier la séquence suivante (en millisecondes), sachant que cela commence par la pause : 500, 1000, 500, 1000. Au niveau du code, cette succession de nombres est représentée par un tableau d'entiers longs qui sera affecté à la propriété vibrate de la notification :

notification.vibrate = new long[] {500, 1000, 500, 1000};
Attention, avant de pouvoir utiliser les vibrations dans votre application, vous devez demander l'autorisation. Ajoutez un <uses-permission> à votre application pour cela.
<uses-permission android:name="android.permission.VIBRATE" />
Alerte par clignotement lumineux

Les notifications ont également des propriétés permettant de configurer la couleur et la fréquence des flashs de la diode de l'appareil.

Chaque appareil peut avoir différentes limites quant au contrôle de la diode. Si la couleur que vous spécifiez n'est pas disponible, une couleur approchante sera utilisée. Ayez cette limitation en tête si vous utilisez la diode pour transmettre une information à l'utilisateur et évitez d'en faire le seul moyen de rendre cette information disponible.

  1. La propriété ledARGB peut être utilisée pour paramétrer la couleur de la diode et les propriétés ledOnMS et ledOffMS pour régler la fréquence et le modèle de clignotement. Vous pouvez ainsi allumer la diode en affectant la valeur 1 à ledOnMS et 0 à ledOffMS et l'éteindre en affectant 0 aux deux.
    notification.ledARGB = Color.RED;
    notification.ledOffMS = 0;
    notification.ledOnMS = 1;
    notification.flags |= Notification.FLAG_SHOW_LIGHTS; 
  2. Il est également possible de spécifier les durées d'allumage et d'extinction en millisecondes dans chacune de ces propriétés. C'est souvent cette dernière alternative qui est choisie.
    notification.ledARGB = Color.RED;
    notification.ledOffMS = 500;
    notification.ledOnMS = 500;
    notification.flags |= Notification.FLAG_SHOW_LIGHTS; 
  3. Une fois les paramètres de la diode configurée, vous devez également ajouter le drapeau FLAG_SHOW_LIGHTS à la propriété flags de la notification.
Utiliser les paramètres par défaut

Ceci dit, la façon la plus simple et la plus cohérente d'ajouter son, lumière ou vibrations à vos notifications est d'utiliser les paramètres par défaut de l'utilisateur. A l'aide de la propriété defauts, vous pouvez combiner :

  1. Notification.DEFAULT_LIGHTS ;
  2. Notification.DEFAULT_SOUND ;
  3. Notification.DEFAULT_VIBRATE.

L'extrait de code suivant assigne le son par défaut et les réglages du vibreur à une notification. Si vous désirez utiliser toutes les valeurs par défaut, vous pouvez spécifier la constante Notification.DEFAULT_ALL :

notification.ledARGB = Color.RED;
notification.ledOffMS = 500;
notification.ledOnMS = 500;
notification.defaults = Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE; 
A titre d'exemple, je vous propose de réaliser une application simple dont l'activité principale fournit deux boutons, un pour lancer une notification après un délai de 5 secondes, l'autre pour annuler cette notification si elle est active :

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.notifier"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="@string/app_name" android:icon="@drawable/icon">
        <activity android:name="Notifier" 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>
    <uses-permission android:name="android.permission.VIBRATE" />
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="5px"
    >
   <Button
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="Notification dans 5 secondes"
       android:onClick="notifier"
    />
   <Button
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="Supprimer la notification"
       android:onClick="supprimer"
    />    
</LinearLayout>
fr.btsiris.notifier.Notifier.java
package fr.btsiris.notifier;

import android.app.*;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import java.util.*;

public class Notifier extends Activity {
   private static final int NOTIFICATION_ID = 1234;
   private Timer temps = new Timer();
   private int compteur = 0;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
   }
   
   public void notifier(View vue) {
      TimerTask tache = new TimerTask() {
         @Override
         public void run() {
            notifier();
         }
      };
      temps.schedule(tache, 5000);
   }
   
   public void supprimer(View vue) {
      NotificationManager mgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      mgr.cancel(NOTIFICATION_ID);
      compteur = 0;
   }
   
   private void notifier() {
      NotificationManager mgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      Notification notification = new Notification(R.drawable.icon, "Message !!!", System.currentTimeMillis());
      PendingIntent retour = PendingIntent.getActivity(this, 0, new Intent(this, Notifier.class), 0);   
      notification.setLatestEventInfo(this, "Titre de la notification", "Message de notification", retour);
      notification.number = ++compteur;
      alerte(notification);
      mgr.notify(NOTIFICATION_ID, notification);
   }
   
   private void alerte(Notification notification) {
      notification.defaults = Notification.DEFAULT_SOUND;
      notification.vibrate = new long[] {1000, 500, 200, 500};   
      notification.ledARGB = Color.GREEN;
      notification.ledOnMS = 500;
      notification.ledOffMS = 500;
      notification.flags |= Notification.FLAG_SHOW_LIGHTS;
   }
}

Utiliser des alarmes

Les alarmes sont un moyen indépendant d'une application de déclencher des intentions à des heures et intervalles déterminés. Une alarme ne fait donc pas réellement partie intégrante de l'application et celle-ci s'exécutera d'ailleurs en dehors de l'application.

Elles sont réglées hors de votre application et peuvent donc être utilisées pour déclencher des événements et des actions même après l'arrêt de celles-ci. Elles sont particulièrement puissantes lorsqu'elles sont utilisées conjointement aux Broadcast Receivers, vous permettant de déclencher des Broadcast Intents, de démarrer des Services ou même ouvrir des Activités sans qu'une application ne soit ouverte ou en cours d'exécution.

Utilisées ainsi, les alarmes sont un excellent moyen de réduire la consommation de ressources de votre application, particulièrement lorsqu'elle tourne en tâche de fond, en vous permettant d'arrêter les Services et d'éliminer les Timers tout en gardant la possibilité d'effectuer des actions programmées.

Vous pouvez, par exemple utiliser des alames pour implémenter un réveil, effectuer régulièrement des recherches sur le réseau ou programmer des opérations consommatrices de ressources à des heures creuses.

Les alarmes restent actives même si l'appareil est en mode veille et peuvent optionnellement être utilisées pour le réveiller. Elles sont cependant toutes annulées lorsque l'appareil est redémarré.

Créer une alarme

Toutes les opérations liées à une alarme s'effectuent via la classe AlarmManager qui n'est rien d'autre qu'un service du système que vous obtiendrez, comme tous les services du système, au travers de la méthode getSystemService() (comme avec les notifications).

AlarmManager gestionnaireAlarmes = (AlarmManager) getSystemService(ALARM_SERVICE);  // Gestionnaire d'alarmes
  1. Une fois le gestionnaire d'alarmes récupéré, utilisez la méthode set() de ce gestionnaire pour créer une nouvelle alarme. Cette méthode vous offre la possibilité de spécifier le type de la référence de temps pour laquelle vous donnez une valeur : s'agit-il d'une heure ou bien d'un délai depuis le démarrage de l'appareil ? La valeur que vous spécifiez, qui devra être en millisecondes, représente donc soit une durée soit une échéance en fonction de la référence de temps.
  2. La création d'une alarme nécessite également la création d'une intention différée - pour rappel, à l'aide de la classe PendingIntent - qui lui sera spécifiée en paramètre, associée au délai et au type de référence de l'alarme. Une intention différée est logique puisque, par définition, une alarme propose une action au bout d'un certain temps. Par contre, si l'heure de déclenchement que vous spécifiez est passée, l'Alarme sera immédiatement déclenchée.

Quatre types d'alarmes sont disponibles. En fonction de votre choix, la valeur passée à la méthode set() pour l'heure de déclenchement représentera une heure donnée ou une durée écoulée.

  1. RTC : Emet un objet de type PendingIntent à une heure particulière, mais sans réveiller l'appareil. Si l'appareil est en veille au moment de la diffusion de cet objet, alors ce dernier ne sera délivré qu'après le réveil de l'appareil.
  2. RTC_WAKEUP : Sort l'appareil du mode veille pour émettre l'objet PendingIntent à l'heure indiquée.
  3. ELAPSED_REALTIME : Déclenche le PendingIntent en fonction d'une durée écoulée depuis le démarrage de l'appareil mais ne sort pas du mode veille. Le temps écoulé inclut tous les moments où l'appareil était en veille. Notez qu'il est calculé à partir du dernier démarrage.
  4. ELAPSED_REALTIME_WAKEUP : Au bout d'un certain temps suivant le redémarrage de l'appareil, sort du mode veille et déclenche le PendingIntent.
AlarmManager alarmes = (AlarmManager) getSystemService(ALARM_SERVICE);  // Gestionnaire d'alarmes
Intent intention = new Intent(Intent.ACTION_DIAL); 
PendingIntent différé = PendingIntent.getBroadcast(this, 0, intention, 0); // Création de l'intention à émettre
// Création d'une alarme à partir d'un délai ayant pour référence le dernier démarrage de l'appareil
alarmes.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 60L * 1000L * 2L, différé);
// Création d'une alarme utilisant une heure précise pour se lancer dans 2 heures après l'appel de cette méthode
alarmes.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (60 * 1000 * 2), différé);

Quand l'alarme arrivera à son terme, l'intention que vous avez spécifié sera diffusée. Si vous créez une alarme avec le même objet Intent, alors ce dernier remplacera le précédent.

Annuler une alarme

Si finalement l'utilisateur souhaite annuler l'alarme après sa création, cela reste tout à fait possible. Une alarme peut être annulée en utilisant la méthode cancel() du gestionnaire d'alarmes. Notez que les alarmes sont automatiquement supprimées après un redémarrage de l'appareil.

alarmes.cancel(différé);
Régler des alarmes répétitives

Vous pouvez avec le gestionnaire d'alarmes régler des alarmes répétitives pour des situations requérant la programmation d'événements réguliers. Ces alarmes fonctionnent exactement comme les alarmes à usage unique mais continueront à se déclencher aux moments définis jusqu'à ce qu'elles soient annulées.

S'exécutant hors du contexte de votre application, elles sont idéales pour programmer des mises à jour régulières ou des recherches de données et ne requièrent pas qu'un service soit constamment en cours d'exécution en arrière-plan.

Pour régler une alarme répétitive, utilisez une des deux méthodes setRepeating() ou setInexactRepeating() de AlarmManager. Les deux supportent un type d'alarme, une heure de début et une intention différée à déclencher.

  1. Utilisez setRepeating() lorsque vous devez contrôler finement l'interval exact entre deux déclenchements. La valeur passée à cette méthode vous permet de spécifier un intervalle exact à la milliseconde près.
  2. La méthode setInexactRepeating() est une technique puissante qui permet de réduire la consommation électrique nécessaire pour faire sortir l'appareil du mode veille de façon régulière pour effcetuer des mise à jour. Au lieu de spécifier un intervalle exact, cette méthode utilise l'une des constantes de AlarmManager suivantes : INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, INTERVAL_DAY.

A l'exécution, Android synchronisera toutes les alarmes à répétition inexacte et les déclenchera simmultanément. Cela empêche chaque application de sortir l'appareil du mode veille séparément à des périodes similaires mais ne se chevauchant pas pour effectuer une mise à jour ou une recherche sur le réseau. En synchronisant ces alarmes, le système peut ainsi limiter l'impact d'événements répétitifs sur la batterie.

AlarmManager alarmes = (AlarmManager) getSystemService(ALARM_SERVICE);
Intent intention = new Intent(Intent.ACTION_DIAL); 
PendingIntent différé = PendingIntent.getBroadcast(this, 0, intention, 0); 
// Déclenche une intention exactement toutes les heures si déjà réveillé.
alarmes.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 60 * 60 * 1000, 60 * 60 * 1000, différé);
// Réveil et alarme à peu près toutes les heures
alarmes.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 60 * 60 * 1000, AlarmManager.INTERVAL_HOUR, différé);

 

Choix du chapitre Persistance des données

Une application qui ne peut garder trace des interactions avec l'utilisateur et conserver des données d'une session à une autre est une application sans vie, que l'utilisateur s'empressera de désinstaller. Toute application doit pouvoir charger et enregistrer des données.

Quatre techniques sont à la disposition du développeur pour faire persister des données dans le temps, chacune offrant un compromis entre facilité de mise en oeuvre, rapidité et flexibilité, tout cela de façon unifiée quelle que soit l'application :

  1. L'enregistrement du parcours de l'utilisateur dans l'application (écrans rencontrés avec leur contexte) ou persistance des activités (un écran étant lié à une activité). Lorsqu'un utilisateur navigue dans une application, il est important de pouvoir conserver l'état de l'interface utilisateur pour préparer son retour sur les écrans potentiellement déchargés par le système pour gagner des ressources.

    Sauvegarder et charger des données est essentiel pour la plupart des applications. Au minimum, une activité doit sauvegarder l'état de son interface utilisateur à chaque fois qu'elle est placée en arrière-plan. Cela garantit que cette interface sera présentée à l'identique lorsque l'activité reviendra au premier plan, même si le processus a été tué puis redémarré.

  2. Le mécanisme de préférence clé/valeur. Les fichiers de préférences sont avant tout utilisés pour stocker des préférences utilisateurs, la configuration d'une application ou l'état de l'interface d'une activité. Ce mécanisme fournit un stockage simple et efficace par paire clé/valeur de valeurs primitives. il existe également un système d'écran de préférences qui permet de créer des écrans de configuration rapidement et simplement.
  3. L'utilisation d'un système de fichiers. Les fichiers sont le support de stockage élémentaire pour lire et écrire des données brutes dans le système de fichiers d'Android. Vous pouvez stocker des fichiers sur le support de stockage interne ou externe d'Android.
  4. L'utilisation d'une base de données SQLite. Les bases de données SQLite sont réservées pour le stockage et la manipulation de données structurées.

Chaque méthode possède des caractéristiques et des usages propres. Il vous reviendra de choisir qui semblera la plus adaptée au contexte de votre application.

Persistance de l'état des applications

Les activités comprennent des gestionnaires d'événements spécialisés pour enregistrer l'état courant de l'interface utilisateur lorsque votre application est déplacée en arrière-plan.

Le mode de fonctionnement du cycle de vie d'une activité vous empêchera souvent de savoir à quel moment précis une activité d'arrière plan sera déchargée de la mémoire. Dès lors, il était indispensable que la plate-forme Android propose un moyen d'enregistrer l'état des activités afin que l'utilisateur retrouve une interface graphique identique entre chacune des sessions.

Il existe deux mécanismes de sauvegarde de l'état de l'activité : le premier consiste à gérer la persistance grâce aux méthodes du cycle de vie de l'activité et le second permet de gérer de façon manuelle à l'aide des classes de persistance de l'API.

  1. La plupart du temps la persistance de l'interface utilisateur se fera via les méthodes du cycle de vie de l'activité. Ce système a été spécifiquement élaboré pour faciliter l'enregistrement et la gestion de l'état de l'interface tout au long du cycle de vie de l'activité.
  2. La méthode onSaveInstanceState() d'une activité est appelée automatiquement lorsque le système a besoin de libérer des ressources et de détruire l'activité (si vous appuyez sur "Retour" pour quitter l'application, l'activité n'est pas enregistrée puisqu'elle ne sera pas mise dans la pile de l'historique des activités).
  3. L'objet utilisé pour stocker les données est de type Bundle et sera passé comme paramètre aux méthodes onCreate() et onRestoreInstanceState() afin de pouvoir rétablir l'interface utilisateur lors de la création ou retauration de l'activité.
  4. Par défaut, les méthodes onSaveInstanceState(), onRestoreInstanceState() et onCreate() de la classe mère fonctionne selon le principe que les valeurs de toutes les vues possédant un attribut id renseigné sont enregistrées, puis restaurées.
  5. L'implémentation par défaut de la méthode onSaveInstanceState() enregistre l'état des vues identifiées dans un objet de type Bundle. L'objet ainsi sauvegardé est ensuite passé aux méthodes onCreate() et onRestoreInstanceState() pour restaurer l'ensemble.
Afin d'illuster ce mécanisme, créons une application élémentaire possédant une simple activité composée de quelques champs :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/principal"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="5px"
    >
<TextView  
    android:id="@+id/description"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Saisissez votre nom :"
    android:textStyle="bold"
    />
<EditText 
    android:id="@+id/nom"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textSize="18sp"
    /> 
<TextView  
    android:id="@+id/message"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Saisissez un message :"
    android:textStyle="bold"    
    />    
<EditText 
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textSize="18sp"
    /> 
</LinearLayout>
fr.btsiris.persistance.Persistance.java
package fr.btsiris.persistance;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class Persistance extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

   @Override
   protected void onSaveInstanceState(Bundle outState) {
      super.onSaveInstanceState(outState);
      Toast.makeText(this, "État de l'activité sauvegardée", Toast.LENGTH_SHORT).show();
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      Toast.makeText(this, "L'activité est détruite", Toast.LENGTH_SHORT).show();
   } 
}

Exécutez cette application et remplissez les champs. Une fois cette opération effectuée, cliquez sur le bouton "Home" de l'émulateur. Si seul le message "Etat de l'activité sauvegardé" apparaît, cela signifie que l'activité n'a pas été détruite et que l'interface utilisateur possède toujours les valeurs que vous avez précédemment saisies. Si vous lancez ces appplications, ces valeurs réapparaîtrons.

Si vous ne souhaitez pas que les écrans soient conservés après la fermeture de l'application, utilisez l'une des options des outils développeur de l'émulateur. Naviguez dans Dev Tools>Development Settings et cochez la case Immediately destroy activites.

Si vous répétez les étapes précédentes et que vous appuyez sur le bouton "Home" de l'émulateur, vous enregistrerez l'état de l'interface et détruisez du même coup l'activité. Une fois les éléments de l'activité enregistrés, si vous relancez l'application, vous observerez que seul le champ du nom a été restauré et que le champ du message - ne possédant pas d'identifiant - est quant à lui vide.

Configurer le mode de conservation des activités

Le mécanisme par défaut présente de nombreux avantages, le premier étant qu'aucune action ou code spécifique n'est requis pour le développeur afin de faire persister l'interface des activités. Néanmoins, ce comportement trouve vite ses limites, notamment quand vous souhaitez ne pas enregistrer un champ confidentiel qui pour des raisons de logique applicative doit posséder un identifiant.

Afin de personnaliser l'enregistrement de l'état de l'activité ou d'enregistrer des informations supplémentaires, vous devez redéfinir ou modifier la méthode onSaveInstanceState() pour l'enregistrement, et les méthodes onCreate() ou onRestoreInstanceState() pour la restauration. Utilsez ensuite l'objet de type Bundle passé en paramètre de ces méthodes pour lire ou écrire des valeurs.

Reprenons le code utilisé dans l'exemple précédent et modifiez-le pour qu'il ressemble au code suivant :
.

fr.btsiris.persistance.Persistance.java
package fr.btsiris.persistance;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class Persistance extends Activity {
    @Override
    public void onCreate(Bundle sauvegarde)  {
        super.onCreate(sauvegarde);
        setContentView(R.layout.main);
        if (sauvegarde!=null && sauvegarde.containsKey("nom")) {
           String valeur = sauvegarde.getString("nom");
           Toast.makeText(this, "onCreate() : "+valeur, Toast.LENGTH_SHORT).show();
        }
    }

   @Override
   protected void onSaveInstanceState(Bundle sauvegarde) {
      super.onSaveInstanceState(sauvegarde);
      sauvegarde.putString("nom", "Emmanuel");
      Toast.makeText(this, "État de l'activité sauvegardée", Toast.LENGTH_SHORT).show();
   }

   @Override
   protected void onRestoreInstanceState(Bundle sauvegarde) {
      super.onRestoreInstanceState(sauvegarde);    
      if (sauvegarde.containsKey("cle")) {
         String valeur = sauvegarde.getString("cle");
         Toast.makeText(this, "Restauration: "+valeur, Toast.LENGTH_SHORT).show();
      }
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      Toast.makeText(this, "L'activité est détruite", Toast.LENGTH_SHORT).show();
   } 
} 

Exécutez l'application à nouveau, remplissez les champs, puis appuyez sur le bouton "Home" de l'émulateur. Relancez l'application une seconde fois, vous devriez alors voir apparaître le message onCreate() : Emmanuel et la valeur du champ nom restauré. Sachez que les méthodes onCreate() et onRestoreInstanceState() sont uniquement appelées si l'activité a été précédemment détruite.

Le mécanisme que nous venons de voir est utilisé pour préserver une interface utilisateur. Il n'a pas été conçu pour enregistrer des préférences n'ayant pas ou peu de rapport avec l'interface, surtout si vous voulez conserver des valeurs même quand l'utilisateur annule une activité en appuyant sur le bouton "Retour". Pour cela, il vous faudra plutôt utiliser les préférences partagées.

Les préférences partagées

Au-dela de vouloir simplement enregistrer l'état de l'application, vous envisagez sûrement d'enregistrer et de retrouver d'autres valeurs propres à votre application, par exemple les paramètres de l'utilisateur, la configuration de l'application, etc. Android fournit un moyen d'enregistrer d'autres préférences que celles des activité et d'autoriser un accès partagé à ces informations à travers les différents composants de l'application.

Ce mécanisme d'enregistrement, appelé préférences partagées (Shared Preferences), permet la persistance de propriétés sous la forme d'un ensemble de clé/valeur. Que ce soit pour stocker les paramètres d'exécution d'une application ou les préférences de l'utilisateur, ce système de stockage des informations est relativement simple et léger à mettre en oeuvre.

Les références partagées vous permettent de sauvegarder des paires clé/valeur de données simples sous forme de préférences nommées. Ainsi, à l'aide de l'interface SharedPreferences, vous pouvez créer ces ensembles de paires clé/valeur dans votre application et les partager entre les composants exécutés dans le contexte de celle-ci. Les préférences partagées supportent les types primitifs boolean, int, long, float et String et constituent un moyen idéal de stocker rapidement des valeurs par défaut, des attributs d'une instance de classe, l'état de l'interface utilisateur et les préférences de ce dernier.

Récupérer les préférences partagées

Complètement indépendant du cycle de vie de l'activité, ce mécanisme est surtout utilisé pour enregistrer des données qui doivent être partagées entre deux sessions utilisateur et accessibles par l'ensemble des composants de l'application. Vous avez trois solutions possibles pour accéder aux préférences :

  1. Appeler la méthode getPreferences() à partir de votre activité pour accéder à ses propres préférences spécifiques.
  2. Appeler la méthode getSharedPreferences() à partir de votre activité (ou d'un autre Context de l'application) pour accéder aux préférences cette fois-ci de l'application dans son ensemble.
  3. Appeler la méthode getDefaultSharedPreferences() d'un objet de type PreferenceManager pour accéder aux préférences partagées qui fonctionnent de concert avec le système des préférences globales d'Android.

Voici quelques précisions supplémentaires :

  1. Vous pouvez récupérer les préférences en effectuant un appel à la méthode getPreferences() de l'activité courante. Cette méthode vous renverra une instance de l'interface SharedPreferences à partir de laquelle vous pourrez extraire les valeurs.
  2. Vous pouvez créer plusieurs ensembles de préférences partagées, chacun identifié par un nom unique. La méthode getSharedPreferences(String, int) propose de spécifier le nom dans le premier argument.
  3. La méthode getPreferences(int) est un appel à la méthode surchargée getSharedPreferences(String, int) en utilisant le nom de classe de l'activité courante comme paramètre de nom de préférence.
  4. Les SharedPreferences sont partagées par les composants d'une application mais ne sont pas disponibles pour les autres applications.
  5. Une fois l'instance de la classe SharedPreferences récupérée, la lecture des données s'effectue via l'utilisation des différentes méthodes getBoolean(), getString(), getFloat(), getInt() et getLong(), respectivement pour récupérer des valeurs de type boolean, String, float, int et long.
    SharedPreferences sauvegarde = getPreferences(Context.MODE_PRIVATE);
    ou
    SharedPreferences sauvegarde = getSharedPreferences("Sauvegarde", Context.MODE_PRIVATE);
    String nom = sauvegarde.getString("nom", null);
    int age = sauvegarde.getInt("age", 0);
  6. Avec toutes ces méthode getXxx(), il vous est possible de spécifier une valeur par défaut au cas où la clé correspondante n'existe pas.
  7. Si vous souhaitez extraire toutes les paires clé/valeur des préférences en une seule opération, vous disposez de la méthode getAll() qui vous retournera un objet de type Map<String, ?>.
Définir vos préférences

A partir d'un objet SharedPreferences, vous pouvez appeler la méthode edit() pour obtenir un éduteur de préférences. Cet objet dispose d'un ensemble de méthodes modificatrices à l'image des méthodes d'accès de l'objet parent SharedPreferences. Il founit également les méthodes suivantes :

  1. remove() : pour supprimer une préférence par son nom ;
  2. clear() : pour supprimer toutes les préférences ;
  3. commit() : pour valider les modifications effectuées via l'éditeur.

La dernière est importante : si vous modifiez les préférences avec l'éditeur et que vous n'appeliez pas commit() ; les modifications disparaîtront lorsque l'éditeur sera hors de portée.

Inversement, comme les préférences acceptent des modifications en direct, si l'une des parties de votre application (une activité, par exemple) modifie des préférences partagées, les autres parties (tel un service) auront accès à la nouvelle valeur.

Enregistrer ou mettre à jour des préférences partagées

L'enregistrement des préférences est donc rendu possible avec l'utilisation de l'interface Editor intégrée dans l'interface SharedPreferences qui vous offrira toutes les méthodes pour ajouter ou modifier des valeurs.

  1. Pour récupérer une instance de l'interface Editor, vous devez appeler la méthode edit() de votre instance SharedPreferences.
  2. Une fois en possession de ladite instance, vous utiliserez les méthodes putBoolean(), putString(), putInt(), putFloat() et putLong() pour ajouter des valeurs respectivement de type boolean, String, int, float et long.
  3. Chacune de ces méthodes prend en paramètres la clé sous forme de chaîne de caractères ainsi que la valeur associée. Si une valeur est spécifiée avec une clé déjà existante, la valeur sera simplement mise à jour.
  4. Une fois vos ajouts et modifications effectués, une dernière opération est nécessaire afin que vos données soient enrgistrées. En effet, vous devrez expressément spécifier l'enregistrement des données avec l'appel à la méthode commit() de votre objet de type Editor.
    SharedPreferences sauvegarde = getSharedPreferences("Sauvegarde", Context.MODE_PRIVATE);
    SharedPreferences.Editor editeur = sauvegarde.edit();
    editeur.putString("nom", "REMY");
    editeur.putInt("age", 15);
    editeur.commit();
Les permissions des préférences

Vous remarquez que les méthodes getPreferences() et getSharedPreferences() possèdent un paramètre définissant la permission donnée au fichier des préférences. Cette option définit les droits liés aux fichiers. La permission que vous spécifierez n'aura que peu d'influance sur le fonctionnement de l'application, mais elle en aura sur la sécurité d'accès des données enregistrées :

  1. MODE_PRIVATE : est le mode par défaut et signifie que le fichier créé sera accessible uniquement à l'application l'ayant créé.
  2. MODE_WORD_READABLE : permet aux autres applications de lire le fichier mais non de le modifier.
  3. MODE_WORD_WRITABLE : permet aux autres applications de modifier le fichier.

En coulisses : Chemin de stockage des préférences : Les préférences sont stockées dans des fichiers du système interne d'Android. En reprenant l'exemple précédent, le fichier est enregistré à l'emplacement suivant : /data/data/fr.btsiris.preferences/shared_prefs/Sauvegarde.xml.

En utilisant un nom pour votre ensemble de valeurs, vous avez ainsi la possibilité d'utiliser plusieurs portées pour stocker vos données et de mieux organiser ces dernières, notamment si vous avez des paires possédant le même nom de clé. Un autre avantage de proposer un nom, c'est qu'il possible d'échanger des valeurs entre activités.

Réagir aux modifications des préférences avec les événements

L'interface SharedPreferences propose un mécanisme d'événements pour vous permettre de maîtriser les changements de données effectués par l'application. L'interface SharedPreferences possède une méthode registerOnSharedPreferenceChangeListener() permettant de spécifier une méthode de rappel/écouteur qui sera appelée à chaque modification de vos préférences.

  1. Ecouteur : onSharedPreferenceChangeListener.
  2. Méthode à redéfinir : void onSharedPreferenceChanged(SharedPreferences sauvegarde, String clé).
Je vous propose de reprendre un des projets que nous avons abordé lors de cette étude, celui concernant la conversion des angles avec des onglets. J'aimerais que lorsque nous passons d'un onglet à l'autre nous puissions conserver les calculs générés par l'autre onglet.

Dans ce cas de figure, vous devez juste revoir la classe abstraite Conversion. Cette technique, qui consiste à utiliser les préférences partagées, me paraît plus simple à mettre en oeuvre plutôt que de passer par les intentions pour communiquer entre activités.

fr.btsiris.radian.Conversion.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.*;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import java.text.*;

public class Conversion extends Activity {
   EditText degre, radian;
   NumberFormat formatRadian = new DecimalFormat("0.00");
   NumberFormat formatDegre= new DecimalFormat("0 °");  
   SharedPreferences sauvegarde; 
   
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      setContentView(R.layout.main);
      degre = (EditText) findViewById(R.id.degre);
      radian =  (EditText) findViewById(R.id.radian);    
      sauvegarde = getSharedPreferences("conversions", Context.MODE_PRIVATE); 
   }
   
   public abstract void calcul(View vue);

   @Override
   protected void onResume() {
      super.onResume();
      degre.setText(sauvegarde.getString("degre", formatDegre.format(0)));
      radian.setText(sauvegarde.getString("radian", formatRadian.format(0)));
   }

   @Override
   protected void onPause() {
      super.onPause();
      Editor editeur = sauvegarde.edit();
      editeur.putString("degre", degre.getText().toString());
      editeur.putString("radian", radian.getText().toString());
      editeur.commit();
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      Editor editeur = sauvegarde.edit();
      editeur.clear();
      editeur.commit();
      Toast.makeText(this, "L'activité est détruite", Toast.LENGTH_SHORT).show();
   }
}

Comme vous pouvez le remarquer, vous avez juste à prendre en compte les méthodes de rappel onResume() et onPause() pour, respectivement récupérer et soumettre vos valeurs de conversion en degré et en radian pour l'autre activité. Ces méthodes de rappel seront appelées automatiquement lorsque vous réactiverez une activité ou lorsqu'elle se mettra en pause (pour passer à la suivante).

Je me suis servi également de la méthode de rappel onDestroy() afin de pouvoir recommencer à zéro lorsque nous revennons sur cette application afin que les calculs antérieurs ne parasitent pas l'environnement de départ. Il s'agit d'un simple cas d'école. Nous aurions pu tout à fait conserver les anciens calculs.

Les menus de préférences prêts-à-l'emploi

Dans l'exemple précédent je me suis servi des préférences pour communiquer des valeurs entre activités. Ceci dit, normalement, comme leur nom l'indique, le but principal de ces préférences est de permettre la mémorisation d'une configuration choisie par l'utilisateur - le dernier flux consulté par le lecteur RSS, l'ordre de tri par défaut des listes, etc.

Afin de faciliter la vie des développeurs et de ne pas réinventer la roue à chaque application, les développeurs de la plate-forme Android ont créé un type d'activité de présentation et de persistance des paramètres de l'application. Cette activité spécifique, PreferenceActivty qui hérite de ListActivity, permet de créer très rapidement des menus similaires à ceux des paramètres systèmes d'Android.

L'activité PreferenceActivity permet de construire l'interface de votre menu de préférences de plusieurs façons :

  1. Directement dans un fichier XML que vous placerez dans les ressources de votre projet, par exemple à l'emplacement /res/xml/preferences.xml, et dans lequel vous spécifiez la hiérarchie des préférences ;
  2. En utilisant plusieurs activités pour lesquelles vous avez spécifié des métadonnées dans le fichier manifest.xml.
  3. Depuis une hiérarchie d'objets dont la racine est de type PreferenceScreen.

En utilisant ce type d'activité, vous faciliterez l'évolution de votre application et réduisez l'effort que vous allez devoir fournir pour enregistrer et lire les paramètres de l'application.

Chaque paramètre est enregistré automatiquement et vous pourrez retrouver chacune des valeurs en appelant la méthode statique getDefaultSharedPreferences() du gestionnaire de préférences PreferenceManager.

Les étapes nécessaires pour implémenter une activité de préférences (via l'utilisation d'un fichier XML) sont les suivantes :

  1. Créer un fichier XML avec la structure des paramètres.
  2. Créer une classe dérivant de PreferenceActivity.
  3. Charger la structure XML des paramètres.
  4. Récupérer les valeurs de chaque paramètre à l'aide de getDefaultSharedPreferences().

L'élément central du système Android pour les préférences est encore une structure de données XML. Vous pouvez décrire les préférences de votre application dans un fichier XML stocké dans le répertoire /res/xml/ du projet. A partir de ce fichier, Android peut présenter une interface graphique pour manipuler ces préférences, qui seront ensuites stockées dans l'objet SharedPreferences obtenu par getDefaultSharedPreferences().

Définir un layout écran de préférences en XML

La première étape consiste donc à créer un fichier décrivant comment sera présenté votre menu de préférences à l'utilisateur. Bien que comparable aux ressources layout de l'interface utilisateur classique, les layouts des écrans de préférences utilisent un ensemble de contrôles conçus spécifiquement pour créer des écrans semblables à ceux du système.

  1. Chaque élément de préférences est défini hiérarchiquement en commençant par un élément <PreferenceScreen> unique (élément racine).
  2. Il est possible d'inclure des éléments additionnels <PreferenceScreen>, chacun d'eux sera alors représenté comme un élément sélectionnable qui affichera un nouvel écran si nous cliquons dessus.
  3. Au sein de chaque écran, vous pouvez inclure toute combinaison d'éléments représentant des catégories <PreferenceCategory> et des contrôles de préférence prédéfini <xxxPreference>.

Si vous devez proposer un grand nombre de préférences à configurer, les mettre toutes dans une seule longue liste risque d'être peu pratique. Le framework d'Android permet donc de structurer un ensembles de préférences en catégories ou en écrans.

Comme nous venons de le voir, les catégories sont créées à l'aide d'éléments <PreferenceCategory> dans le fichier XML des préférences et permettent de regrouper des préférences apparentées. Au lieu qu'elles soient toutes des filles de la racine <PreferenceScreen>, vous pouvez placer vos préférences dans les catégories appropriées en plaçant des éléments <PreferenceCategory> sous la racine. Visuellement ces groupes seront séparés par une barre contenant le titre de la catégorie.

Exemple de menu de préférences
<?xml version="1.0" encoding="UTF-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Conserver les résultats">
        <CheckBoxPreference 
           android:key="onglets"
           android:title="Onglets"
           android:defaultValue="true"
           android:summaryOn="Transfère les calculs pour l'autre onglet"
           android:summaryOff="Mise à zéro des angles lorsque nous changeons d'onglet" />
        <CheckBoxPreference
           android:key="quitter"
           android:title="Quitter"
           android:defaultValue="true"
           android:dependency="onglets"
           android:summaryOn="Garde les résultats lorsque nous quittons l'application"
           android:summaryOff="Efface tout lorsque nous quittons l'application" />
    </PreferenceCategory>
    <PreferenceCategory android:title="Chiffres après la virgule">
       <EditTextPreference
           android:key="decimalesDegre"
           android:title="Degré"
           android:summary="Précision de la valeur en degré"        
           android:dialogTitle="Décimales pour degré"
           android:defaultValue="0"  />
      <EditTextPreference
           android:key="decimalesRadian"
           android:title="Radian"
           android:summary="Précision de la valeur en radian"        
           android:dialogTitle="Décimales pour radian"
           android:defaultValue="3"  />
    </PreferenceCategory>   
</PreferenceScreen>

Si vous avez un grand nombre de preferences, trop pour qu'il soit envisageable que l'utilisateur les fasse défiler, vous pouvez également les placer dans des écrans distincts à l'aide de l'élément <PreferenceScreen> (le même que l'élément racine).

Tout fils de <PreferenceScreen> est placé dans son propre écran. Si vous imbriquez des <PreferenceScreen>, l'écran père affichera l'écran fils sous la forme d'une entrée : en la touchant, vous ferez apparaître le contenu de l'écran fils correspondant.

Exemple de menu de préférences
<?xml version="1.0" encoding="UTF-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Conserver les résultats">
       <CheckBoxPreference 
           android:key="onglets"
           android:title="Onglets"
           android:defaultValue="true"
           android:summaryOn="Transfère les calculs pour l'autre onglet"
           android:summaryOff="Mise à zéro des angles lorsque nous changeons d'onglet"/>
       <CheckBoxPreference
           android:key="quitter"
           android:title="Quitter"
           android:defaultValue="true"
           android:dependency="onglets"
           android:summaryOn="Garde les résultats lorsque nous quittons l'application"
           android:summaryOff="Efface tout lorsque nous quittons l'application"/>
   </PreferenceCategory>
   <PreferenceCategory android:title="Chiffres après la virgule">
       <PreferenceScreen 
           android:title="Réglages des décimales"
           android:summary="Spécifie les chiffres après la virgule pour chaque unité d'angle">
           <EditTextPreference
                android:key="decimalesDegre"
                android:title="Degré"
                android:summary="Précision de la valeur en degré"        
                android:dialogTitle="Décimales pour degré"
                android:defaultValue="0"/>
          <EditTextPreference
                android:key="decimalesRadian"
                android:title="Radian"
                android:summary="Précision de la valeur en radian"        
                android:dialogTitle="Décimales pour radian"
                android:defaultValue="3"/>
        </PreferenceScreen>      
    </PreferenceCategory>   
</PreferenceScreen>





  

Tout ce qui vous reste à faire est d'ajouter les contrôles qui seront utilisés pour paramétrer les préférences de l'application. Bien que les attributs spécifiques de chaque préférence varient; les quatres suivants sont communs :

  1. android:key : Chaque préférence, quel que soit son type, doit posséder une clé qui sert d'identifiant pour l'enregistrement de chaque valeur.
  2. android:title : Libellé de la préférence.
  3. andriod:summary : Description longue affichée en petits caractères sous le titre.
  4. android:defaultValue : Valeur par défaut affichée (et sélectionnée) si aucune valeur n'a été assignée à la clé.

Android inclut plusieurs contrôles prédéfinis destinés à vos écrans de préférences. Chacun de ces éléments affichera un contrôle du type approprié avec un titre et une description spécifiés dans les attributs de l'élément dans l'écran des préférences.

<CheckBoxPreference>

Case à cocher standard utilisé pour choisir entre vrai et faux. Ce type de préférence possède des propriétés spécifiques permettant de personnaliser l'affichage de façon plus intuitive pour l'utilisateur.

  1. android:summaryOn : Spécifie la description lorsque la case à cocher est activée.
  2. android:summaryOff : Spécifie la description lorsque la case à cocher est désactivée.

Ce type de préférence est opportun pour utliser la propriété de dépendance android:dependency. Cette propriété permet à une préférence de spécifier la clé d'une autre préférence. Si la valeur de la préférence cible est remplie ou vraie, dans le cadre d'une case à cocher par exemple, la préférence dépendante sera alors active, sinon elle sera désactivée.

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Conserver les résultats">
        <CheckBoxPreference 
           android:key="onglets"
           android:title="Onglets"
           android:defaultValue="true"
           android:summaryOn="Transfère les calculs pour l'autre onglet"
           android:summaryOff="Mise à zéro des angles lorsque nous changeons d'onglet" />
        <CheckBoxPreference
           android:key="quitter"
           android:title="Quitter"
           android:defaultValue="true"
           android:dependency="onglets"
           android:summaryOn="Garde les résultats lorsque nous quittons l'application"
           android:summaryOff="Efface tout lorsque nous quittons l'application" />
  
<EditTextPreference>

Permet à l'utilisateur de saisir une valeur de type chaîne de caractères. Lorsque qu'il clique sur cette préférence, une boîte de dialogue apparaît avec une zone de saisie de texte.

La propriété android:dialogTitle permet de spécifier un titre à la boîte de dialogue de la zone de saisie qui apparaît au-dessus de l'activité représentant les préférences.

      <PreferenceCategory android:title="Chiffres après la virgule">
       <PreferenceScreen 
           android:title="Réglages des décimales"
           android:summary="Spécifie les chiffres après la virgule pour chaque unité d'angle">
           <EditTextPreference
                android:key="decimalesDegre"
                android:title="Degré"
                android:summary="Précision de la valeur en degré"        
                android:dialogTitle="Décimales pour degré"
                android:defaultValue="0"/>
          <EditTextPreference
                android:key="decimalesRadian"android:title="Radian"
                android:summary="Précision de la valeur en radian"        
                android:dialogTitle="Décimales pour radian"
                android:defaultValue="3"/>
        </PreferenceScreen>      
    </PreferenceCategory>   
<ListPreference>

Equivalent à un bouton fléché. Le sélectionner affiche une boîte de dialogue contenant une liste de choix. Vous pouvez spécifier différents tableaux contenant le texte à afficher et les valeurs à sélectionner.

L'objectif de ce type de préférence est de proposer une liste de valeur à l'utilisateur parmi lesquelles il ne pourra en sélectionner qu'une seule. Une liste possède des propriétés permettant de définir quels seront les éléments présentés à l'utilisateur.

  1. android:dialogTitle spécifie un titre à la boîte de dialogue affichant la liste à l'utilisateur.
  2. android:entries spécifie les éléments qui seront présentés à l'utilisateur. Vous devez spécifier une référence vers une ressource de type tableau.
  3. android:entryValues spécifie les valeurs qui seront enregistrées dans les préférences.
<?xml version="1.0" encoding="UTF-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Conserver les résultats">
		...
   </PreferenceCategory>
   <PreferenceCategory android:title="Chiffres après la virgule">
       <ListPreference
           android:key="decimalesDegre"
           android:title="Degré"
           android:summary="Précision de la valeur en degré"        
           android:dialogTitle="Décimales pour degré"
           android:entries="@array/liste_degre"
           android:entryValues="@array/liste_valeur_degre"
           android:defaultValue="0"/>
       <ListPreference
           android:key="decimalesRadian"
           android:title="Radian"
           android:summary="Précision de la valeur en radian"        
           android:dialogTitle="Décimales pour radian"
           android:entries="@array/liste_radian"
           android:entryValues="@array/liste_valeur_radian"
           android:defaultValue="3"/>   
    </PreferenceCategory>   
</PreferenceScreen>

Les éléments des listes de l'exemple ci-dessus font références, à l'aide de la syntaxe @array/, à une ressource localisée dans res/values/listes.xml contenant le document XML ci-dessous. Utilisez les balises <array> ou <string-array> pout définir vos différentes listes.

<?xml version="1.0" encoding="UTF-8"?>
<resources>
   <array name="liste_radian">
      <item>1 chiffre</item>
      <item>2 chiffres</item>
      <item>3 chiffres</item>
   </array>
   <string-array name="liste_valeur_radian">
      <item>1</item>
      <item>2</item>
      <item>3</item>      
   </string-array>
   <array name="liste_degre">
      <item>Pas de décimales</item>
      <item>1 chiffre</item>
      <item>2 chiffres</item>
   <array>
   <string-array name="liste_valeur_degre">
      <item>0</item>
      <item>1</item>
      <item>2</item>      
   </string-array>
</resources>

Par défaut, la balise <array> interprète le contenu de chaque <item>. Si vous désirez qu'une valeur numérique soit prise comme une chaîne de caractères, utilisez plutôt la balise <string-array>.

<RingtonePreference>

Liste spécialisée représentant la liste des sonneries. Ceci est particulièrement utile pour des écrans destinés à configurer des notifications.

Quelquefois, vous aurez besoin de paramètres nécessitant la sélection d'une sonnerie, par exemple pour attirer l'attention de l'utilisateur lors d'un événement précis de votre application.

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="Notifier les calculs">
       <RingtonePreference 
           android:key="sonnerie"
           android:title="Sonnerie"
           android:showDefault="true"
           android:showSilent="true"
           android:summary="Choisissez une sonnerie"/>
   </PreferenceCategory>    
   <PreferenceCategory android:title="Conserver les résultats">
       <CheckBoxPreference 
           android:key="onglets"
           android:title="Onglets"
           android:defaultValue="true"
           android:summaryOn="Transfère les calculs pour l'autre onglet"
           android:summaryOff="Mise à zéro des angles lorsque nous changeons d'onglet"/>
       <CheckBoxPreference
           android:key="quitter"
           android:title="Quitter"
           android:defaultValue="true"
           android:dependency="onglets"
           android:summaryOn="Garde les résultats lorsque nous quittons l'application"
           android:summaryOff="Efface tout lorsque nous quittons l'application"/>
   </PreferenceCategory>
...

Les propriétés android:showDefault et android:showSilent sont spécifiques à ce type de préférence, mais ne sont pas obligatoires puisque, la sonnerie par défaut et le mode silencieux sont systématiquement proposée avec la liste des autres sonneries présentes dans votre mobile.

Chacun des contrôles peut être utilisé pour construire une hiérarchie. Vous pouvez également créer vos propres contrôles spécialisés en étendant la classe Preferences (ou l'une de ses sous-classes).

Utiliser vos préférences dans l'application

Lorsque vous avez mis en place le fichier XML décrivant vos préférences, vous pouvez utiliser une activité "quasi intégrée" afin de permettre aux utilisateurs de faire leur choix. Cette activité est "quasi intégrée" car il suffit d'en créer une sous-classe, de la faire pointer vers ce fichier et de la lier au reste de votre application.

Ainsi, afin de pouvoir utiliser la description XML d'un menu de préférences dans votre application, vous devez créer une classe héritant de PreferenceActivity et charger le menu depuis la méthode de création de l'activité. La méthode addPreferencesFromResource() ajoute le menu de préférences à l'activité en s'appuyant sur le fichier XML spécifié.

fr.btsiris.radian.Preferences.java
package fr.btsiris.radian;

import android.os.Bundle;
import android.preference.PreferenceActivity;

public class Preferences extends PreferenceActivity {
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      addPreferencesFromResource(R.xml.preferences);  
   }
}  

Comme toutes les activités, l'activité représentant les préférences doit être incluse dans le manifeste de votre application.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.radian"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Angles" android:icon="@drawable/icon">
        <activity android:name="Choix" android:label="Conversion des angles">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Radian" android:label="Recherche du radian" />
        <activity android:name="Degre" android:label="Recherche du degré" />
        <activity android:name="Preferences" android:label="Réglages" />
    </application>
</manifest>

Pour démarrer l'activité des préférences depuis une activité, il suffit comme nous l'avons déjà vu, de créer une intention spécifique et d'utiliser une des méthodes d'appel startActivity() ou startActivityForResult(). Très souvent, cet appel est réalisé au travers d'un menu d'options. Enfin, pour récupérer les préférences enregistrées précédemment, il suffit d'utiliser la méthode statique getDefaultSharedPreferences(contexte).

fr.btsiris.radian.Conversion.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.*;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.*;
import android.widget.*;
import java.text.*;

public abstract class Conversion extends Activity {
   EditText degre, radian;
   NumberFormat formatRadian = new DecimalFormat("0.000");
   NumberFormat formatDegre = new DecimalFormat("0 °");   
   SharedPreferences prefs;
   
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      degre = (EditText) findViewById(R.id.degre);
      radian =  (EditText) findViewById(R.id.radian);  
      prefs = PreferenceManager.getDefaultSharedPreferences(this);
   }
   
   public abstract void calcul(View vue);

   @Override
   protected void onResume() {
      super.onResume();
      decimalesDegre(prefs.getString("decimalesDegre", "0"));
      decimalesRadian(prefs.getString("decimalesRadian", "3"));  
      if (prefs.getBoolean("onglets", true)) {
         degre.setText(formatDegre.format(prefs.getFloat("degre", 0.0f)));
         radian.setText(formatRadian.format(prefs.getFloat("radian", 0.0f)));
      }
      else {
         degre.setText(formatDegre.format(0));
         radian.setText(formatRadian.format(0));         
      }
   }

   @Override
   protected void onPause() {
      super.onPause();
      if (prefs.getBoolean("onglets", true)) {
         try {        
            Editor editeur = prefs.edit();
            editeur.putFloat("degre", formatDegre.parse(degre.getText().toString()).floatValue());
            editeur.putFloat("radian", formatRadian.parse(radian.getText().toString()).floatValue());
            editeur.commit();
         } 
         catch (ParseException ex) {   }
      }
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      if (!prefs.getBoolean("quitter", true)) {
         Editor editeur = prefs.edit();
         editeur.remove("degre");
         editeur.remove("radian");
         editeur.commit();
      }
   }
   
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {     
      menu.add(Menu.NONE, 1, Menu.NONE, "Réglages des angles");
      return super.onCreateOptionsMenu(menu); 
   }   
   
   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
      if (item.getItemId() == -1) return true;
      startActivity(new Intent(this, Preferences.class));
      return super.onOptionsItemSelected(item);
   }     
   
   private void decimalesDegre(String nombre) {
      String decimales;
      switch(Integer.parseInt(nombre) ) {
         case 0 : decimales = "0"; break;
         case 1 : decimales = "0.0"; break;
         default : decimales = "0.00"; break; 
      }
      formatDegre = new DecimalFormat(decimales+" °"); 
   }   
   
   private void decimalesRadian(String nombre) {
      String decimales;
      switch(Integer.parseInt(nombre)) {
         case 1 : decimales = "0.0"; break;
         case 2 : decimales = "0.00"; break;
         default : decimales = "0.000"; break; 
      }
      formatRadian = new DecimalFormat(decimales);  
   }      
}

Stockage dans des fichiers

Bien qu'Android dispose de moyens de stockage via les préférences et les bases de données, un simple fichier suffit parfois. Le fichier représente l'élément de base du système Android pour stocker n'importe quel type de données : applications, ressources, base de données, etc.

Le paradigme de développement des applications Android prévoit que toutes les ressources soient stockées et regroupées dans le dossier /res. Malgré cela, il y aura toujours des cas où vous ne pourrez pas éviter l'utilisation de fichiers de données bruts (fichier de données sérialisées, texte brut, etc.).

Lire et écrire dans le système de fichier

En plus des classes spécifiques à la gestion des flux classiques de l'API standrad de Java, issu du paquetage java.io, Android propose deux méthodes openFileInput() et openFileOutput() pour simplifier la lecture et l'écriture depuis et dans des fichiers du contexte local.

Ces méthodes openFileInput() et openFileOutput() supportent uniquement l'accès aux fichiers du dossier de l'application courante. Il est impossible de spécifier des séparateurs de chemin, sauf à risquer de voir une exception se lever.

Ouverture d'un fichier en lecture
try {        
       FileInputStream fichier = openFileInput("unFichier.dat");
       ...
       fichier.close();
} 
catch (FileNotFoundException ex) {   }
Ouverture d'un fichier en écriture
try {        
       FileInputStream fichier = openFileOutput("unFichier.dat", MODE_PRIVATE);
       ...
       fichier.close();
} 
catch (FileNotFoundException ex) {   }

La méthode openFileOutput() permet d'ouvrir le fichier en écriture ou de le créer s'il n'existe pas. Si le fichier existe, le comportement par défaut consiste à l'écraser. Si vous souhaitez ajouter du contenu à un fichier déjà existant, vous devez spécifier le mode MODE_APPEND.

try {        
       FileInputStream fichier = openFileOutput("unFichier.dat", MODE_PRIVATE | MODE_APPEND);
       ...
       fichier.close();
} 
catch (FileNotFoundException ex) {   }
Partager un fichier avec d'autres applications

Par défaut, les fichiers générés par la méthode openFileOutput() appartiennent exclusivement à l'application qui les crée et une autre application s'en verra refuser l'accès. La bonne pratique pour partager un fichier entre applications est d'utiliser un fournisseur de contenu (sujet que nous aborderons bientôt).

Néanmoins, vous pouvez spécifier un mode d'ouverture tel que MODE_WORLD_READABLE ou MODE_WORLD_WRITABLE permettant aux autres applications de pouvoir accéder à vos fichiers.

Afin de mieux comprendre le rôle de chaque permission, voici un résumé des modes d'accès possibles (qui ne sera pas sans vous rappeler celle des préférences partagées, également stockées dans un fichier) :

  1. MODE_PRIVATE : est le mode par défaut et signifie que le fichier créé sera accessible uniquement à l'application l'ayant créé.
  2. MODE_WORD_READABLE : permet aux autres applications de pouvoir lire le fichier mais pas de pouvoir le modifier.
  3. MODE_WORD_WRITABLE : permet aux autres applications de pouvoir lire et également modifier le fichier.
  4. MODE_APPEND : permet d'écrire les données en fin de fichier au lieu de l'écraser. Vous pouvez combiner ce mode avec un autre.
Intégrer des ressources dans vos applications

Une application nécessite quelquefois d'embarquer des fichiers de façon à pouvoir charger des données pendant son exécution. L'utilisation de ce mécanisme vous permet de garder l'avantage de la gestion des ressources alternatives : langue, régionalisation et configuration matérielle. Vous pourrez de cette façon proposer des sons ou d'autres ressources qui s'adapteront automatiquement au matériel et à la langue de vos utilisateurs.

Android répond élégamment à cette exigence en vous permettant d'embarquer des données directement dans le fichier APK de votre application. Le moyen le plus simple d'y parvenir consiste à placer ces données dans un fichier (ou plusieurs) situé dans le répertoire res/raw : elles seront ainsi intégrées automatiquement au fichier APK de l'application comme une ressource brute au cours du processus d'assemblage.

Pour accéder à ce fichier, qui est accessible en lecture seule par votre application, vous avez besoin d'un objet de type Resources que vous pouvez obtenir à partir de l'activité en appelant tout simplement la méthode getResources(). Cet objet fournit lui-même la méthode openRawResources() pour récupérer un InputStream sur le fichier spécifier par son identifiant (un entier). Cela fonctionne exactement comme l'accès aux widgets avec la méthode findViewById().

Resources ressources = getResources();
InputString langue = ressources.openRawResource(R.raw.langue);

Attention : Ajouter des fichiers bruts est utile lorsque vous souhaitez embarquer des données déjà traitées (dictionnaires, contenu en texte brut, etc.). Cependant, gardez bien à l'esprit que plus vos données sont volumineuses, plus le fichier APK que vous distribuerez le sera également.

Gérer les fichiers

La plate-forme Android propose plusieurs méthodes utilitaires pour manipuler les fichiers depuis le contexte de l'application :

  1. La méthode deleteFile() permet d'effacer un fichier à partir de son nom.
  2. La méthode fileList() liste tous les fichiers locaux de l'application.
  3. La méthode getFileDir() permet d'obtenir le chemin absolu du répertoire dans le système de fichiers où tous les fichiers créés avec openFileOutput() sont stockés.
  4. La méthode getFileStreamStore() retourne le chemin absolu du répertoire dans le système de fichiers où est stocké le fichier créé avec openFileOutput() et dont le nom est passé en paramètre.
Afin de valider cette utilisation de fichiers, je vous propose de modifier la classe Conversion du projet précédent. Cette classe génèrera un fichier "calculs" qui permettra de sauvegarder la valeur en degré et en radian. Ces données seront ensuite consultées pour l'autre activité ou lorsque nous reviendrons sur cette application. Il s'agit ici juste d'un cas d'école. il est préférable de passe par les préférences partagées :
fr.btsiris.radian.Conversion.java
package fr.btsiris.radian;

import android.app.Activity;
import android.content.*;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.*;
import android.widget.*;
import java.text.*;
import java.io.*;

public abstract class Conversion extends Activity {
   EditText degre, radian;
   NumberFormat formatRadian = new DecimalFormat("0.000");
   NumberFormat formatDegre = new DecimalFormat("0 °");   
   SharedPreferences prefs;
   
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      degre = (EditText) findViewById(R.id.degre);
      radian =  (EditText) findViewById(R.id.radian);  
      prefs = PreferenceManager.getDefaultSharedPreferences(this);
      degre.setText(formatDegre.format(0));
      radian.setText(formatRadian.format(0));      
   }
   
   public abstract void calcul(View vue);

   @Override
   protected void onResume() {
      super.onResume();
      decimalesDegre(prefs.getString("decimalesDegre", "0"));
      decimalesRadian(prefs.getString("decimalesRadian", "3"));  
      if (prefs.getBoolean("onglets", true)) {
         try {
            DataInputStream fichier = new DataInputStream(openFileInput("calculs"));
            degre.setText(formatDegre.format(fichier.readDouble()));
            radian.setText(formatRadian.format(fichier.readDouble()));
         } 
         catch (IOException ex) {   } 
      }
      else {
         degre.setText(formatDegre.format(0));
         radian.setText(formatRadian.format(0));         
      }
   }

   @Override
   protected void onPause() {
      super.onPause();
      if (prefs.getBoolean("onglets", true)) {
         try {       
            DataOutputStream fichier = new DataOutputStream(openFileOutput("calculs", MODE_PRIVATE));
            fichier.writeDouble(formatDegre.parse(degre.getText().toString()).doubleValue());
            fichier.writeDouble(formatRadian.parse(radian.getText().toString()).doubleValue());
         } 
         catch (Exception ex) {   }
      }
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      if (!prefs.getBoolean("quitter", true)) deleteFile("calculs");
   }
   
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {     
      menu.add(Menu.NONE, 1, Menu.NONE, "Réglages des angles");
      return super.onCreateOptionsMenu(menu); 
   }   
   
   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
      if (item.getItemId() == -1) return true;
      startActivity(new Intent(this, Preferences.class));
      return super.onOptionsItemSelected(item);
   }     
   
   private void decimalesDegre(String nombre) {
      String decimales;
      switch(Integer.parseInt(nombre) ) {
         case 0 : decimales = "0"; break;
         case 1 : decimales = "0.0"; break;
         default : decimales = "0.00"; break; 
      }
      formatDegre = new DecimalFormat(decimales+" °"); 
   }   
   
   private void decimalesRadian(String nombre) {
      String decimales;
      switch(Integer.parseInt(nombre)) {
         case 1 : decimales = "0.0"; break;
         case 2 : decimales = "0.00"; break;
         default : decimales = "0.000"; break; 
      }
      formatRadian = new DecimalFormat(decimales);  
   }      
}

Stockage dans une base de données SQLite

L'avantage d'une base de données est qu'elle permet de manipuler et de stocker des données complexes et structurées, ce qui serait impossible, ou du moins difficile à faire, avec les autres moyens de persistance décrits précédemment. Android fournit un support de base de données relationnelles au travers de SQLite, une base de données très utilisée dans le domaine des appareils mobiles (lecteurs mp3, lecteurs de salon, etc.).

A la différence de bon nombre d'autres base de données, SQLite s'exécute sans nécessiter de serveur, ce qui implique que l'exécution des requêtes sur la base de données s'effectue dans le même processus que l'application. Conjugué au fait qu'une base de données est réservée à l'application créatrice, cela rend une base de données SQLite performante au niveau de la gestion des transactions et de la synchronisation.

Vous pouvez créer plusieurs bases de données par application. Néanmoins, chaque base de données est dédiée à l'application, c'est-à-dire que seule l'application qui en est à l'origine pourra y accéder. Si vous souhaitez exposer les données d'une base de données particulière à d'autres applications, vous pourrez utiliser un fournisseur de contenu, comme nous le décrouvrirons dans le chapitre suivant.

Chemin de stockage des bases de données par défaut : Toutes les bases de données sont stockées par défaut dans /data/data/<espace de nom>/databases sur votre appareil. Le fichier de base de données est automatiquement créé en MODE_PRIVATE, d'où le fait que seule l'application l'ayant créé peut y accéder.

Concevoir une base de données SQLite pour une application Android

Nous ne concevons pas et n'utilisons pas une base de données SQLite de la même façon que nous pourrions le faire avec une base de données dédié à un serveur MySQL, PosgreSQL, etc. Gardez bien en tête que vous développez pour les appareils mobiles, avec peu d'espace de stockage, de mémoire vive et de puissance. Evitez donc de mettre des volumes de données importantes dans vos bases de données ou d'effectuer des requêtes fréquentes (attention à la gestion des ressources des curseurs).

Faites en sorte de concevoir une base avec une structure simple et extensible, comportant des données facilement identifiable de façon à ce qu'une requête ne renvoie pas de données inutiles en créant du "bruit" dans vos données. Il en va de la performance de votre application.

SQLite étant une base de données légère, vous devrez éviter d'y enregistrer des données binaires (images, par exemple). A la place, créez des fichiers sur le support de stockage et faite-y référence dans la base. Puisque vous pourriez avoir besoin d'exposer ces données binaires à d'autres applications, une bonne pratique consiste à nommer ces fichiers avec une URI de façon à être directement renvoyé par un fournisseur de contenu et exploité par l'application demandeuse.

Autre élément à prendre en considération, l'ajout systématique d'une colonne d'index auto-incrémentale de façon à présenter chaque ligne de façon unique. C'est un bon conseil, surtout si vous envisagez par la suite de partager vos tables via un fournisseur de contenu.

Présentation rapide de SQLite

SQLite, comme son nom l'indique, utilise un dialecte de SQL, pour effectuer des requêtes (SELECT), des manipulations de données (INSERT, etc.) et des définitions de données (CREATE TABLE, etc.).

Certaines fonctionnalités standard de SQL ne sont pas reconnues par SQLite, notamment les contraintes de clé étrangère, les transactions imbriquées, les jointures externes et certaines variantes de ALTER TABLE.

Ces remarques mises à part, vous disposez d'un SGBDR complet, avec des triggers, des transactions, etc. Les instructions SQL de base comme SELECT fonctionnent exactement comme vous êtes en droit de l'attendre.

Créer et mettre à jour une base de données SQLite

Créer une base de données SQLite est une opération simple, commune à toutes les bases de données. Néanmoins, mettre à jour le schéma de la base de données est une opération plus délicate. Afin de simplifier le code de votre application pour gérer ces opérations, le SDK Android offre une classe d'aide : SQLiteOpenHelper.

Ainsi, pour créer une nouvelle base de données ou ouvrir une déjà existante, la meilleure solution consiste à créer une sous-classe et de cette classe d'aide SQLiteOpenHelper. Elle enveloppe tout ce qui est nécessaire à la création et à la mise à jour d'une base, selon vos spécifications et les besoins de votre application.

L'intérêt de cette dérivation, c'est que vous pouvez créer vos propres méthodes correspondant à votre application et personnaliser celles qui sont nécessaires au fonctionnement. La force de ce modèle de conception est qu'une fois que vous aurez dérivé cette classe, toutes ces opérations seront transparentes pour vous. Vous devez redéfinir trois méthodes dans votre sous-classe :

  1. Un constructeur qui appelle celui de la classe parente et qui prend en paramètre le Context (l'activité), le nom de la base de données, une éventuelle fabrique de curseur (le plus souvent, ce paramètre vaudra null) et un entier représentant la version du schéma de la base.
  2. onCreate() : à laquelle vous passerez l'objet SQLiteDataBase que vous devrez remplir avec les tables et les données initiales que vous souhaitez. Si vous essayez d'ouvrir une base alors qu'elle n'existe pas encore, la classe la créera pour vous en appelant cette méthode onCreate() que vous aurez redéfinie. Si la version de la base de données a changé, alors la méthode onUpgrade() sera aussi appelée.
  3. onUpgrade() : à laquelle vous passerez également un objet SQLiteDataBase ainsi que l'ancien et le nouveau numéro de version afin de pouvoir convertir au mieux une base de données d'un schéma vers un autre. Pour convertir une base d'un ancien schéma à un nouveau, l'approche la plus simple consiste à supprimer les anciennes tables et à en créer de nouvelles.
Voici un exemple simple qui permet d'ouvrir, de créer et mettre à jour une base de données possédant une simple table nommées personnels permettant de recenser l'identité de chaque personne :
fr.btsiris.bd.BD.java
package fr.btsiris.bd;

import android.content.Context;
import android.database.sqlite.*;
import android.database.sqlite.SQLiteDatabase.CursorFactory;

public class BD extends SQLiteOpenHelper {

   public BD(Context context, String name, CursorFactory factory, int version) {
      super(context, name, factory, version);
   }

   @Override
   public void onCreate(SQLiteDatabase db) {
      db.execSQL("CREATE TABLE personnels ("
              + "id INTEGER PRIMARY KEY AUTOINCREMENT,"
              + "nom TEXT NOT NULL,"
              + "prenom TEXT NOT NULL,"
              + "age INTEGER NOT NULL);");
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int ancienneVersion, int nouvelleVersion) {
      db.execSQL("DROP TABLE personnels;");
      onCreate(db);
   }  
}

Ensuite, pour accéder à une base de données à l'aide de la classe SQLiteOpenHelper, vous pouvez appeler les méthodes getReadableDatabase() et getWritableDatabase() et ainsi obtenir une instance de la base de données respectivement en lecture seule et en lecture/écriture.


BD maBase = new BD(context, NOM_BASE_DONNEES, null, VERSION_BASE_DONNEES);
SQLiteDataBase bd = maBase.getWritableDatabase();
Accéder à une base de données

La partie précédente nous a montrée comment le SDK d'Android nous facilite la tâche pour créer une classe d'aide à l'ouverture, à la création et à la mise à jour d'une base de données SQLite. Maintenant, nous allons utiliser cette classe d'aide pour travailler avec la base qui sera créée automatiquement grâce à celle-ci.

La manipulation d'une base de données est toujours quelque peu fastidieuse et la bonne pratique veut que vous proposiez une couche d'abstraction, afin d'apporter une indépendance vis-à-vis de la source de données. Si, par exemple, vous souhaitez par la suite changer de type de source de données et travailler avec des services Internet ou des fichiers, vous ne devrez changer que le code de cette classe d'abstraction.

Ce modèle de conception est appelé adaptateur et propose d'offrir au développeur des méthodes fortement typées pour effectuer les requêtes classiques, comme l'insertion, la suppression et la mise à jour des données de la base mais aussi pour gérer les opérations d'ouverture et de fermeture de la base de données. Nous avons déjà les derniers éléments grâce à notre classe dérivée de SQLiteOpenHelper, nous allons maintenant encapsuler toutes ces actions dans notre adaptateur.

Voici un exemple simple qui permet d'ouvrir, de créer et mettre à jour une base de données possédant une simple table nommées personnels permettant de recenser l'identité de chaque personne :
fr.btsiris.bd.GestionBD.java
package fr.btsiris.bd;

import android.content.*;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;

public class GestionBD {
   private SQLiteDatabase bd;
   
   public void ouverture(Context ctx) { bd = new BD(ctx, "personnels.bd", null, 1).getWritableDatabase();  }
   
   public void fermeture() { bd.close();  }
   
   public long ajouter(Personne personne) { ... }
   
   public int miseAJour(Personne personne) { ... }    
   
   public int supprimer(Personne personne) { ... }   
   
   public Personne getPersonne(int id) { ... }
   
   public ArrayList<Personne> getPersonnes() { ... }
}

De la même façon qu'il est une bonne pratique de proposer un adaptateur pour les accès aux sources de données, il en est aussi de pouvoir proposer des classes typées afin de manipuler les données de façon naturelle dans vos applications. Dans le code précédent, nous faisions référence à une classe Personne représentant un objet métier décrivant les propriétés, non exhaustives, d'une personne.

fr.btsiris.bd.GestionBD.java
package fr.btsiris.bd;

public class Personne {
   private int id;
   private String nom;
   private String prenom;
   private int age;

   public int getId() { return id;  }
   public void setId(int id) { this.id = id;  }

   public int getAge() { return age; }
   public void setAge(int age) { this.age = age; }

   public String getNom() { return nom; }
   public void setNom(String nom) { this.nom = nom.toUpperCase(); }

   public String getPrenom() { return prenom;  }
   public void setPrenom(String original) { 
      StringBuilder prenom = new StringBuilder(original.toLowerCase());
      char premier = Character.toUpperCase(original.charAt(0));
      prenom.setCharAt(0, premier);
      this.prenom = prenom.toString(); 
   }
}  

Ceci dit, il peut être judicieux de factoriser la classe d'aide ainsi que l'adaptateur en une seule et même classe qui, du coup, gère l'ensemble de la base de données en proposant des méthodes simples pour l'utilisateur. C'est cette pratique que je préfère mettre en oeuvre.

Effectuer une requête dans une base SQLite

Il est possible d'utiliser plusieurs approches (méthodes) pour récupérer les données d'une base avec SELECT :

  1. rawQuery() : requête brute : permet d'exécuter directement une instruction SELECT sous forme d'une chaîne complète de caractères, comme toute requête SQL classique.
  2. query() : requête normale : permet de construire une requête à partir de différents composants. La méthode query() prend en paramètre les parties d'une instruction SELECT afin de construire plus facilement la requête.

Dans la majorité des cas, les requêtes de sélection SQLite s'effectueront via la dernière méthode query() d'une instance de SQLiteDatabase. Cette méthode retourne un curseur permettant ensuite de naviguer dans les résultats. Une attention particulière est nécessaire pour la bonne gestion des ressources allouées au travers de ce dernier.

Ainsi, à la différence de nombreuses API pour d'autres bases de données, les requêtes de sélection ne se font plus à partir d'une chaîne SELECT mais par l'utilisation de la méthode query() - et de ses surcharges - proposant directement les critères de sélection au développeur.

La requête de sélection SQL sera construite par la méthode, puis compilée pour interroger la base de données. Les paramètres de la requête sont donc très proches d'une requête SELECT standard. Voici, dans l'ordre, les paramètres possibles dans votre méthode query() :

  1. distinct : Spécifie si le résultat doit contenir ou non des éléments uniques. Optionnel.
  2. table : Spécifie le nom de la table.
  3. columns : Spécifie la projection, c'est-à-dire le nom des colonnes de la table à inclure dans le résultat. Si vous spécifiez une valeur null, toutes les colonnes de la table seront retournées. Cependant, essayez de réduire au maximum votre projection aux seules colonnes qui seront réellement utilisées de façon à réduire les ressources nécessaires. Vous spécifierez un tableau de String pour nommer les colonnes.
  4. selection : Spécifie la clause de filtre WHERE de la requête. Le format doit être identique à une requête WHERE standard. Si vous spécifiez la valeur null, toutes les éléments seront retournés. Vous pouvez utiliser des valeurs '?' dans la sélection, chacune sera remplacée par la valeur spécifiée dans le paramètre selectionArgs.
  5. selectionArgs : Spécifie les valeurs de remplacement des '?' dans le paramètre selection dans l'ordre d'apparition.
  6. groupBy : Un filtre de regroupement des lignes, similaire à la clause GROUP BY. Si vous spécifiez la valeur null, les lignes ne seront pas regroupées.
  7. having : Un filtre de condition d'apparition des lignes en relation avec le paramètre groupBy et similaire à la clause GROUP BY standard. Si vous spécifiez la valeur null, toutes les lignes seront incluses dans le résultat. La valeur null est requise si la valeur du paramètre groupBy est également null.
  8. orderBy : Spécifie l'ordre de tri des lignes selon le format de la clause ORDER BY standard. Si vous spécifiez la valeur null, l'ordre de tri par défaut sera appliqué.
  9. limit : Limite le nombre de lignes retournées par la requête en utilisant le format de la clause LIMIT standard. Si vous spécifiez une valeur null, toutes les lignes seront retournées.
Le code suivant utilise la méthode query() pour retrouver les différentes personnes enregistrées de notre exemple :
fr.btsiris.bd.BD.java
public class GestionBD {
   private SQLiteDatabase bd;
...
   public Personne getPersonne(int id) {
      String[] colonnes = {"nom", "prenom", "age"};
      Cursor curseur = bd.query("personnels", colonnes, "id = "+id, null,  null, null, "nom, prenom") ;
      if (curseur.getCount()==0) return null;
      curseur.moveToFirst();
      Personne personne = new Personne();
      personne.setId(id);
      personne.setNom(curseur.getString(0));
      personne.setPrenom(curseur.getString(1));
      personne.setAge(curseur.getInt(2));
      curseur.close();
      return personne;
   }
   
   public ArrayList<Personne> getPersonnes() {
      ArrayList<Personne> liste = new ArrayList<Personne>();
      Cursor curseur = bd.query("personnels", null, null, null,  null, null, "nom, prenom") ;
      if (curseur.getCount()==0) return liste;
      curseur.moveToFirst();
      do {
         Personne personne = new Personne();
         personne.setId(curseur.getInt(0));
         personne.setNom(curseur.getString(1));
         personne.setPrenom(curseur.getString(2));
         personne.setAge(curseur.getInt(3));
         liste.add(personne);
      }
      while (curseur.moveToNext());
      curseur.close();
      return liste;      
   }
}

La récupération des éléments issus du résultat d'une requête de sélection se fait au travers de l'objet Cursor retourné par la méthode query(). Au lieu de retrouner tous les résultats, le Cursor agit comme un curseur de base de données accessible directement depuis les API. De cette façon, vous vous déplacez à votre guise au sein des résultats en modifiant la position de celui-ci, via les méthodes fournies.

  1. moveToFirst() : Déplace le curseur à la prmière position pour lire les données de la première ligne.
  2. moveToLast() : Déplace le curseur à la dernière position pour lire les données de la dernière ligne de la requête.
  3. moveToNext() : Déplace le curseur d'une position pour lire la ligne suivante.
  4. moveToPrevious() : Déplace le curseur d'une position pour lire la ligne précedente.
  5. moveToPosition(int) : Déplace le curseur à la position indiquée.

D'autres méthodes sont nécessaires pour récupérer des informations des résultats retournés.

  1. getCount() : Retourne le nombre de lignes qui sont renvoyées par la requête.
  2. getColumnName(int) : Retourne le nom de la colonne spécifiée par son index.
  3. getColumnNames() : Retourne un tableau de chaînes de caractères avec le nom de toutes les colonnes retournées.
  4. getColumnCount() : Retourne le nombre de colonnes renvoyées par la requête.

Pour récupérer une valeur depuis un curseur, naviguez au sein de celui-ci, puis utilisez l'une des méthodes getString(), getInt(), getLong(), etc. pour retrouver la valeur souhaitée à partir de l'index de la colonne. En réalité, les valeurs n'étant que faiblement typées dans SQLite, vous pouvez utiliser n'importe quelle méthode getXxx() pour récupérer une valeur.

Afin de gérer au mieux les ressources prises par un Cursor en fonction de l'activité de l'utilisateur et donc du cycle de vie d'une activité, la classe Activity fournit une méthode nommée startManagingCursor(). Lorsque vous n'avez plus besoin de gérer les résultats d'un Cursor, appelez la méthode stopManagingCursor(), puis la méthode close() du Cursor pour libérer les resources du curseur.

Vous pouvez également encapsuler un Cursor dans un SimpleCursorAdapter ou une autre implémentation, puis passer l'adaptateur résultant à une ListView ou à un autre widget de sélction. Après avoir récupérer la liste des constantes triées, par exemple, nous plaçons chacune d'elles dans la ListView en quelques lignes de code.

Comment gérer les nouvelles données dans la table

Lorsque nous créons une base de données et une ou plusieurs tables, c'est généralement pour y placer des données. Pour ce faire, il existe principalement deux approches :

  1. Utiliser exceSQL() : comme vous l'avez déjà fait pour créer les tables. Cette méthode permet en effet d'exécuter n'importe quelle instruction SQL que ne renvoie pas de résultat, ce qui est le cas de INSERT, UPDATE, DELETE, etc.
  2. Utiliser insert(), update() et delete() sur l'objet SQLiteDatabase. Ces méthodes préfabriquées prennent en paramètre une instruction SQL découpée en plusieurs morceaux. Elles utilisent des objets ContentValues qui implémentent une interface ressemblant à Map mais avec des méthodes supplémentaires pour prendre en compte les types SQLite : outre get(), qui permet de récupérer une valeur pas sa clé, vous disposez également de getAsInteger(), getAsString(), etc.
Insérer des données

Insérer ou mettre à jour des données dans une base SQLite repose sur l'utilisation de la méthode insert() de la classe SQLiteDatabase. Pour spécifier les valeurs de la ligne à insérer, la méthode accepte un objet de type ContentValues. Ces objets stockent les valeurs de chaque colonne de la ligne à insérer sous la forme d'une collection d'association entre le nom de la colonne et la valeur :

fr.btsiris.bd.BD.java
public class GestionBD {
   private SQLiteDatabase bd;
...
   public long ajouter(Personne personne) {
      ContentValues valeurs = new ContentValues();
      valeurs.put("nom", personne.getNom());
      valeurs.put("prenom", personne.getPrenom());
      valeurs.put("age", personne.getAge());
      return bd.insert("personnels", null, valeurs);
   }
}
Mettre à jour des données

Pour mettre à jour des données dans SQLite, utilisez la méthode update() de la classe SQLiteDatabase en spécifiant un objet ContentValues contenant les nouvelles valeurs et la valeur de la clause de condition WHERE :

fr.btsiris.bd.BD.java
public class GestionBD {
   private SQLiteDatabase bd;
...
   public int miseAJour(Personne personne) {
      ContentValues valeurs = new ContentValues();
      valeurs.put("nom", personne.getNom());
      valeurs.put("prenom", personne.getPrenom());
      valeurs.put("age", personne.getAge());
      return bd.update("personnels", valeurs, "id = "+personne.getId(), null);      
   }    
}

Le dernier paramètre de la méthode update() est la condition - clause identique au paramètre de la clause standard SQL UPDATE - permettant de spécifier les éléments à metre à jour. Seuls les éléments répondant à cette condition seront modifiés.

Supprimer des données

Pour supprimer des données d'une table, utilisez la méthode delete() de la classe SQLiteDatabase en spécifiant le nom de la table ciblée et le critère permettant à la base d'identifier les éléments à supprimer :

fr.btsiris.bd.BD.java
public class GestionBD {
   private SQLiteDatabase bd;
...
   public int supprimer(Personne personne) {
      return bd.delete("personnels", "id = "+personne.getId(), null);
   } 
} 

Le dernier paramètre de la méthode update() est la condition - clause identique au paramètre de la clause standard SQL UPDATE - permettant de spécifier les éléments à metre à jour. Seuls les éléments répondant à cette condition seront modifiés.

Exemple de mise en oeuvre complète

Je vous propose maintenant de mettre en oeuvre une application qui permet de recenser et d'enregistrer dans la base de données interne les personnels d'une petite entreprise. Vous avez ci-dessous toute l'ossature du projet correspondant.

Cette application possèdent deux activités : la première nous donne la liste du personnel déjà enregistré dans la base de données. Cette première activité possède également un bouton pour insérer un nouvel agent. Le fait d'agir sur ce bouton, nous passons automatiquement sur la deuxième activité qui consiste à compléter l'identité du nouvel agent afin de l'enregistrer par la suite. Cette activité peut également être sollicité lorsque vous choisissez un agent déjà enregistré dans la liste. Dans ce cas de figure, il vous est possible de modifier son identité ou même de la supprimer définitivement.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bd"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Personnels" >
        <activity android:name="Personnels" android:label="Gestion des personnels">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Personnel" android:label="Edition du personnel" />
    </application>
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="5dp">
   <Button
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:text="Nouvelle personne"
      android:onClick="edition"
      android:layout_gravity="right"
      />
   <ListView  
      android:id="@android:id/list"
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" />
</LinearLayout>  









fr.btsiris.bd.Personnels.java
package fr.btsiris.bd;

import android.app.*;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.util.ArrayList;

public class Personnels extends ListActivity {
    private BD bd;
    private EditText nom, prenom, age;
    private ListView liste;
    private ArrayList<Personne> personnes = new ArrayList<Personne>();
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       bd = new BD(this);
    }

    @Override
    protected void onStart() {
       super.onStart();
       personnes = bd.getPersonnes();
       setListAdapter(new ArrayAdapter<Personne>(this, android.R.layout.simple_list_item_1, personnes));
    }

    @Override
    protected void onListItemClick(ListView liste, View vue, int position, long id) {
       Personne personne = personnes.get(position);
       Intent intention = new Intent(this, Personnel.class);
       intention.putExtra("id", personne.getId());
       startActivity(intention);      
    }
    
    public void edition(View vue) {
       startActivity(new Intent(this, Personnel.class));
    }

   @Override
   protected void onDestroy() {
      bd.fermeture();
      super.onDestroy();
   }
} 
res/layout/personnel.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="5dp">
   <EditText
      android:id="@+id/nom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Nom ?" />
   <EditText
      android:id="@+id/prenom"   
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Prénom ?"/>    
   <EditText
      android:id="@+id/age"   
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:inputType="number"
      android:hint="Âge ?" />      
   <LinearLayout 
      android:layout_width="fill_parent"
      android:layout_height="wrap_content">
      <Button
         android:id="@+id/enregistrer"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:hint="Enregistrer"
         android:onClick="enregistrer"/>    
      <Button
         android:id="@+id/modifier"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:hint="Modifier" 
         android:onClick="modifier"/>            
      <Button
         android:id="@+id/supprimer"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:hint="Supprimer" 
         android:onClick="supprimer"/>              
    </LinearLayout>
</LinearLayout>
fr.btsiris.bd.Personnel.java
package fr.btsiris.bd;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class Personnel extends Activity {
    private BD bd;
    private EditText nom, prenom, age;
    private Personne personne;
    private Button enregistrer, modifier, supprimer;
       
    @Override
    public void onCreate(Bundle icicle) {
       super.onCreate(icicle);
       setContentView(R.layout.personnel);
       nom = (EditText) findViewById(R.id.nom);
       prenom = (EditText) findViewById(R.id.prenom);
       age = (EditText) findViewById(R.id.age);
       enregistrer = (Button) findViewById(R.id.enregistrer);
       modifier = (Button) findViewById(R.id.modifier);
       supprimer = (Button) findViewById(R.id.supprimer);
       personne = new Personne();
       bd = new BD(this);
    }

   @Override
   protected void onStart() {
      super.onStart();
      Bundle données = getIntent().getExtras();     
      if (données!=null) {
         enregistrer.setEnabled(false);
         modifier.setEnabled(true);
         supprimer.setEnabled(true);
         personne = bd.getPersonne(données.getInt("id"));
         nom.setText(personne.getNom());
         prenom.setText(personne.getPrenom());
         age.setText(""+personne.getAge());  
      }
      else {
         enregistrer.setEnabled(true);
         modifier.setEnabled(false);
         supprimer.setEnabled(false);         
      }
    }
    
    public void enregistrer(View vue) {
       personne.setNom(nom.getText().toString());
       personne.setPrenom(prenom.getText().toString());
       personne.setAge(Integer.parseInt(age.getText().toString()));
       bd.ajouter(personne);
       finish();
    }
    
    public void modifier(View vue) {
       personne.setNom(nom.getText().toString());
       personne.setPrenom(prenom.getText().toString());
       personne.setAge(Integer.parseInt(age.getText().toString()));
       bd.miseAJour(personne);
       finish();      
    }
    
    public void supprimer(View vue) {
       bd.supprimer(personne.getId());
       finish();       
    }
    
   @Override
   protected void onDestroy() {
      bd.fermeture();
      super.onDestroy();
   }
}  
fr.btsiris.bd.BD.java
package fr.btsiris.bd;

import android.content.*;
import android.database.Cursor;
import android.database.sqlite.*;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import java.util.ArrayList;

public class BD extends SQLiteOpenHelper {
   private SQLiteDatabase bd;

   public BD(Context context, String name, CursorFactory factory, int version) {
      super(context, name, factory, version);
   }

   @Override
   public void onCreate(SQLiteDatabase db) {
      db.execSQL("CREATE TABLE personnels ("
              + "id INTEGER PRIMARY KEY AUTOINCREMENT,"
              + "nom TEXT NOT NULL,"
              + "prenom TEXT NOT NULL,"
              + "age INTEGER NOT NULL);");
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int ancienneVersion, int nouvelleVersion) {
      db.execSQL("DROP TABLE personnels;");
      onCreate(db);
   }  
   
   public BD(Context ctx) {
      super(ctx, "personnels.bd", null, 1);
      bd = getWritableDatabase();
   }
   
   public void fermeture() { bd.close();  }
   
   public long ajouter(Personne personne) { 
      ContentValues valeurs = new ContentValues();
      valeurs.put("nom", personne.getNom());
      valeurs.put("prenom", personne.getPrenom());
      valeurs.put("age", personne.getAge());
      return bd.insert("personnels", null, valeurs);   
   }
   
   public int miseAJour(Personne personne) { 
      ContentValues valeurs = new ContentValues();
      valeurs.put("nom", personne.getNom());
      valeurs.put("prenom", personne.getPrenom());
      valeurs.put("age", personne.getAge());
      return bd.update("personnels", valeurs, "id = "+personne.getId(), null);     
   }    
   
   public int supprimer(int id) { 
      return bd.delete("personnels", "id = "+id, null);
   }   
   
   public Personne getPersonne(int id) {
      Cursor curseur = bd.query("personnels", null, "id = "+id, null, null, null, null) ;
      if (curseur.getCount()==0) return null;
      curseur.moveToFirst();
      return curseurToPersonne(curseur);   
   }
   
   public ArrayList<Personne> getPersonnes() { 
      ArrayList<Personne> liste = new ArrayList<Personne>();
      Cursor curseur = bd.query("personnels", null, null, null,  null, null, "nom, prenom") ;
      if (curseur.getCount()==0) return liste;
      curseur.moveToFirst();
      do {
         liste.add(curseurToPersonne(curseur));
      }
      while (curseur.moveToNext());
      curseur.close();
      return liste;    
   }
   
   private Personne curseurToPersonne(Cursor curseur) {
      Personne personne = new Personne();
      personne.setId(curseur.getInt(0));
      personne.setNom(curseur.getString(1));
      personne.setPrenom(curseur.getString(2));
      personne.setAge(curseur.getInt(3)); 
      return personne;
   }   
}
fr.btsiris.bd.Personne.java
package fr.btsiris.bd;

public class Personne {
   private int id;
   private String nom;
   private String prenom;
   private int age;

   public int getId() { return id;  }
   public void setId(int id) { this.id = id;  }

   public int getAge() { return age; }
   public void setAge(int age) { this.age = age; }

   public String getNom() { return nom; }
   public void setNom(String nom) { this.nom = nom.toUpperCase(); }

   public String getPrenom() { return prenom;  }
   public void setPrenom(String original) { 
      StringBuilder prenom = new StringBuilder(original.toLowerCase());
      char premier = Character.toUpperCase(original.charAt(0));
      prenom.setCharAt(0, premier);
      this.prenom = prenom.toString(); 
   }

   @Override
   public String toString() {
      return  nom + ", " + prenom + " (" + age + ')';
   }
}