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.
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.
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 :
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 :
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é.
<?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.
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 = { , , , , , , };
@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]);
}
}
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 :
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).
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+ );
}
}
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.
<?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>
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( );
}
}
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 :
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.
<?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>
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( );
}
}
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.
<?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.
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.
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 :
<?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>
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.
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.
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 :
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é.
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.
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( +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));
}
}
<?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>.
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.
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 :
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 :
<?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.
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 = { , , , , , , };
@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]);
}
}
Le constructeur utilisé pour instancier un nouvel objet de type ArrayAdapter prend trois paramètres :
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.
<?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.
<?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.
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.
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 :
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.
<?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>
<?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.
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( ); break;
case Mercredi : commentaire.setText( ); break;
case Samedi :
case Dimanche : commentaire.setText( ); break;
}
return ligne;
}
}
}
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.
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.
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( ); break;
case Mercredi : commentaire.setText( ); break;
case Samedi :
case Dimanche : commentaire.setText( ); 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.
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.
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.
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 :
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 :
<?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 :
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 = ;
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 = ; break;
case 2 : nombreDecimales = ; break;
case 3 : nombreDecimales = ; break;
}
formatMonnaie = new DecimalFormat( +nombreDecimales+symbole);
getValeur();
}
}
Pour l'activité principale, nous devons maintenant mettre en oeuvre notre menu au moyen des deux méthodes importantes, savoir onCreateOptionsMenu() et onOptionsItemSelected().
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, );
menu.add(Menu.NONE, 2, Menu.NONE, );
menu.add(Menu.NONE, 3, Menu.NONE, );
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; } }
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.
Voici ci-dessous les modifications à apporter dans le code de Conversion.java afin d'obtenir le lancement de ce sous-menu :
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, );
SubMenu sousMenu = menu.addSubMenu(Menu.NONE, -1, Menu.NONE, );
sousMenu.add(Menu.NONE, 2, Menu.NONE, );
sousMenu.add(Menu.NONE, 3, Menu.NONE, );
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.
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.
Voici ci-dessous les modifications à apporter dans le code de Conversion.java afin d'obtenir le lancement de ce menu contextuel :
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( );
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, );
menu.add(Menu.NONE, 2, Menu.NONE, );
menu.add(Menu.NONE, 3, Menu.NONE, );
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);
}
}
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.
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.
toast.setGravity(Gravity.TOP, 0, 40);
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( );
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, );
menu.add(Menu.NONE, 2, Menu.NONE, );
menu.add(Menu.NONE, 3, Menu.NONE, );
monnaie = vue;
}
@Override public boolean onContextItemSelected(MenuItem item) {
String nom = ;
if (monnaie.equals(euro)) {
euro.setNombreDecimales(item.getItemId());
nom = ;
}
if (monnaie.equals(franc)) {
franc.setNombreDecimales(item.getItemId());
nom = ;
}
if (nom.length()!=0) {
String decimales = item.getItemId() == 0 ? : + item.getItemId() + ;
Toast.makeText(this, nom + decimales, Toast.LENGTH_SHORT).show();
}
return super.onOptionsItemSelected(item);
}
}
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 :
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.
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( );
menu.setHeaderIcon(R.drawable.icon);
menu.add(Menu.NONE, 0, Menu.NONE, );
menu.add(Menu.NONE, 2, Menu.NONE, );
menu.add(Menu.NONE, 3, Menu.NONE, );
monnaie = vue;
}
@Override public boolean onContextItemSelected(MenuItem item) {
String nom = ;
if (monnaie.equals(euro)) {
euro.setNombreDecimales(item.getItemId());
nom = ;
}
if (monnaie.equals(franc)) {
franc.setNombreDecimales(item.getItemId());
nom = ;
}
if (nom.length()!=0) {
String decimales = item.getItemId() == 0 ? : item.getItemId() + ;
new AlertDialog.Builder(this)
.setTitle(nom)
.setIcon(R.drawable.icon)
.setMessage(decimales)
.setNeutralButton( , 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.
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.
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.
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() :
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é.
<?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 :
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) {}
}
}
}
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 :
... 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) {}
}
}
}
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.
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 :
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.
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.
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.
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) :
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é).
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.
Intent intention = new Intent(this, MonActivité.class);
Ici, le constructeur de la classe Intent prend deux paramètres. Le premier correspondant au contexte à partir duquel l'intention est créée et sera envoyée. Ce premier paramètre fait référence la plupart du temps à l'activité en cours. Le second paramètre fait référence à un type de classe Java héritant de la classe Activity.
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 :
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().
<?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.
<?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>
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.
<?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>
<?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().
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( ); NumberFormat formatDegre= new DecimalFormat( ); @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); }
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) { } } }
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) { } } }
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.
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( ) .setIndicator( ) .setContent(new Intent(this, Radian.class))); onglets.addTab(onglets.newTabSpec( ) .setIndicator( ) .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.
<?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" />
<?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>
<?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>
<?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>
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.
La méthode onActivityResult() utilise trois paramètres pour identifier l'activité et ses valeurs de retour :
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.
<?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>
<?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>
<?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>
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( ); NumberFormat formatDegre= new DecimalFormat( ); @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 = ; for (int i=1; i<resultCode; i++) motif += ; formatRadian = new DecimalFormat( +motif); radian.setText(formatRadian.format(valeur)); } catch (ParseException ex) { } } } }
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.
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.
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 :
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.
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.
Uri téléphone = Uri.parse("tel:0612345678"); Intent composerNuméro = new Intent(Intent.ACTION_DIAL, téléphone); startActivity(composerNuméro);
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.
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.
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. |
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.
<?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.
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( +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.
Certaines actions requièrent des privilèges spéciaux qui vous amènerons à ajouter des permissions dans le fichier de configuration de votre application.
Uri téléphone = Uri.parse("tel:0612345678"); Intent composerNuméro = new Intent(Intent.ACTION_CALL, téléphone); startActivity(composerNuméro);
<?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>
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.
<?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>
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>
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.
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 :
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é.
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( ); Intent radian = new Intent(Intent.ACTION_EDIT, commande); startActivity(radian); } }
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.
<?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.
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.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) { } } }
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);
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().
Intent intention = new Intent(this, ServiceDemarrer.class); intention.putExtra("maclé", valeur); startService(intention);
Bundle données = getIntent().getExtras(); if (données!=null) { int valeur = données.getInt( ); ... }
int valeur = getIntent().getIntExtra( , 18);
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.
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.getIntExtra( , 0)); startActivityForResult(radian, CALCUL_RADIAN); } public void calculDegre(View vue) { degre.putExtra( , radian.getDoubleExtra( , 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; } } }
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( , 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( , resultat); setResult(RESULT_OK, getIntent()); } catch (ParseException ex) { setResult(RESULT_CANCELED); } } }
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( , 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( , resultat); setResult(RESULT_OK, getIntent()); } catch (ParseException ex) { setResult(RESULT_CANCELED); } } }
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( ); ... }
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.
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);
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().
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.
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).
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. |
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.
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é.
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.
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.
NotificationManager gestionnaire = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // Accès à l'instance du gestionnaire de notifications final int NOTIFICATION_ID = 1234; ... gestionnaire.notify(NOTIFICATION_ID, 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);
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.
.
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.
.
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;
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};
<uses-permission android:name="android.permission.VIBRATE" />
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.
notification.ledARGB = Color.RED; notification.ledOffMS = 0; notification.ledOnMS = 1; notification.flags |= Notification.FLAG_SHOW_LIGHTS;
notification.ledARGB = Color.RED; notification.ledOffMS = 500; notification.ledOnMS = 500; notification.flags |= Notification.FLAG_SHOW_LIGHTS;
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 :
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;
<?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>
<?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>
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, , System.currentTimeMillis()); PendingIntent retour = PendingIntent.getActivity(this, 0, new Intent(this, Notifier.class), 0); notification.setLatestEventInfo(this, , , 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; } }
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é.
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
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.
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.
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é);
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.
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é);
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 :
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é.
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.
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.
<?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>
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, , Toast.LENGTH_SHORT).show(); } @Override protected void onDestroy() { super.onDestroy(); Toast.makeText(this, , 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.
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 :
.
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( )) { String valeur = sauvegarde.getString( ); Toast.makeText(this, +valeur, Toast.LENGTH_SHORT).show(); } } @Override protected void onSaveInstanceState(Bundle sauvegarde) { super.onSaveInstanceState(sauvegarde); sauvegarde.putString( , ); Toast.makeText(this, , Toast.LENGTH_SHORT).show(); } @Override protected void onRestoreInstanceState(Bundle sauvegarde) { super.onRestoreInstanceState(sauvegarde); if (sauvegarde.containsKey( )) { String valeur = sauvegarde.getString( ); Toast.makeText(this, +valeur, Toast.LENGTH_SHORT).show(); } } @Override protected void onDestroy() { super.onDestroy(); Toast.makeText(this, , 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.
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.
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 :
Voici quelques précisions supplémentaires :
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);
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 :
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.
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.
SharedPreferences sauvegarde = getSharedPreferences("Sauvegarde", Context.MODE_PRIVATE); SharedPreferences.Editor editeur = sauvegarde.edit(); editeur.putString("nom", "REMY"); editeur.putInt("age", 15); editeur.commit();
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 :
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.
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.
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.
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( ); NumberFormat formatDegre= new DecimalFormat( ); 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( , Context.MODE_PRIVATE); } public abstract void calcul(View vue); @Override protected void onResume() { super.onResume(); degre.setText(sauvegarde.getString( , formatDegre.format(0))); radian.setText(sauvegarde.getString( , formatRadian.format(0))); } @Override protected void onPause() { super.onPause(); Editor editeur = sauvegarde.edit(); editeur.putString( , degre.getText().toString()); editeur.putString( , radian.getText().toString()); editeur.commit(); } @Override protected void onDestroy() { super.onDestroy(); Editor editeur = sauvegarde.edit(); editeur.clear(); editeur.commit(); Toast.makeText(this, , 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.
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.
L'activité PreferenceActivity permet de construire l'interface de votre menu de préférences de plusieurs façons :
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 :
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().
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.
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.
<?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.
<?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 :
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.
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.
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" />
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>
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.
<?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>.
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).
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é.
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.
<?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).
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( ); NumberFormat formatDegre = new DecimalFormat( ); 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( , )); decimalesRadian(prefs.getString( , )); if (prefs.getBoolean( , true)) { degre.setText(formatDegre.format(prefs.getFloat( , 0.0f))); radian.setText(formatRadian.format(prefs.getFloat( , 0.0f))); } else { degre.setText(formatDegre.format(0)); radian.setText(formatRadian.format(0)); } } @Override protected void onPause() { super.onPause(); if (prefs.getBoolean( , true)) { try { Editor editeur = prefs.edit(); editeur.putFloat( , formatDegre.parse(degre.getText().toString()).floatValue()); editeur.putFloat( , formatRadian.parse(radian.getText().toString()).floatValue()); editeur.commit(); } catch (ParseException ex) { } } } @Override protected void onDestroy() { super.onDestroy(); if (!prefs.getBoolean( , true)) { Editor editeur = prefs.edit(); editeur.remove( ); editeur.remove( ); editeur.commit(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, 1, Menu.NONE, ); 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 = ; break; case 1 : decimales = ; break; default : decimales = ; break; } formatDegre = new DecimalFormat(decimales+ ); } private void decimalesRadian(String nombre) { String decimales; switch(Integer.parseInt(nombre)) { case 1 : decimales = ; break; case 2 : decimales = ; break; default : decimales = ; break; } formatRadian = new DecimalFormat(decimales); } }
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.).
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.
try { FileInputStream fichier = openFileInput("unFichier.dat"); ... fichier.close(); } catch (FileNotFoundException ex) { }
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) { }
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) :
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.
La plate-forme Android propose plusieurs méthodes utilitaires pour manipuler les fichiers depuis le contexte de l'application :
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( ); NumberFormat formatDegre = new DecimalFormat( ); 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( , )); decimalesRadian(prefs.getString( , )); if (prefs.getBoolean( , true)) { try { DataInputStream fichier = new DataInputStream(openFileInput( )); 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( , true)) { try { DataOutputStream fichier = new DataOutputStream(openFileOutput( , 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( , true)) deleteFile("calculs"); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, 1, Menu.NONE, ); 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 = ; break; case 1 : decimales = ; break; default : decimales = ; break; } formatDegre = new DecimalFormat(decimales+ ); } private void decimalesRadian(String nombre) { String decimales; switch(Integer.parseInt(nombre)) { case 1 : decimales = ; break; case 2 : decimales = ; break; default : decimales = ; break; } formatRadian = new DecimalFormat(decimales); } }
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.
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.
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 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 :
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( + + + + ); } @Override public void onUpgrade(SQLiteDatabase db, int ancienneVersion, int nouvelleVersion) { db.execSQL( ); 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();
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.
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, , 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.
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.
Il est possible d'utiliser plusieurs approches (méthodes) pour récupérer les données d'une base avec SELECT :
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() :
public class GestionBD { private SQLiteDatabase bd; ... public Personne getPersonne(int id) { String[] colonnes = { , , }; Cursor curseur = bd.query( , colonnes, +id, null, null, null, ) ; 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( , null, null, null, null, null, ) ; 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.
D'autres méthodes sont nécessaires pour récupérer des informations des résultats retournés.
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.
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 :
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 :
public class GestionBD { private SQLiteDatabase bd; ... public long ajouter(Personne personne) { ContentValues valeurs = new ContentValues(); valeurs.put( , personne.getNom()); valeurs.put( , personne.getPrenom()); valeurs.put( , personne.getAge()); return bd.insert( , null, valeurs); } }
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 :
public class GestionBD { private SQLiteDatabase bd; ... public int miseAJour(Personne personne) { ContentValues valeurs = new ContentValues(); valeurs.put( , personne.getNom()); valeurs.put( , personne.getPrenom()); valeurs.put( , personne.getAge()); return bd.update( , valeurs, +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.
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 :
public class GestionBD { private SQLiteDatabase bd; ... public int supprimer(Personne personne) { return bd.delete( , +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.
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.
<?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>
<?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>
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( , personne.getId()); startActivity(intention); } public void edition(View vue) { startActivity(new Intent(this, Personnel.class)); } @Override protected void onDestroy() { bd.fermeture(); super.onDestroy(); } }
<?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>
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( )); 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(); } }
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( + + + + ); } @Override public void onUpgrade(SQLiteDatabase db, int ancienneVersion, int nouvelleVersion) { db.execSQL( ); onCreate(db); } public BD(Context ctx) { super(ctx, , null, 1); bd = getWritableDatabase(); } public void fermeture() { bd.close(); } public long ajouter(Personne personne) { ContentValues valeurs = new ContentValues(); valeurs.put( , personne.getNom()); valeurs.put( , personne.getPrenom()); valeurs.put( , personne.getAge()); return bd.insert( , null, valeurs); } public int miseAJour(Personne personne) { ContentValues valeurs = new ContentValues(); valeurs.put( , personne.getNom()); valeurs.put( , personne.getPrenom()); valeurs.put( , personne.getAge()); return bd.update( , valeurs, +personne.getId(), null); } public int supprimer(int id) { return bd.delete( , +id, null); } public Personne getPersonne(int id) { Cursor curseur = bd.query( , null, +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( , null, null, null, null, null, ) ; 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; } }
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 + ')'; } }