Lire un extrait ( PDF 524 Ko)

Transcription

Lire un extrait ( PDF 524 Ko)
8
Bases de données
et fournisseurs de contenu
Au sommaire de ce chapitre :
■■
Créer des bases de données et utiliser SQLite
■■
Utiliser les fournisseurs de contenu, les curseurs et les content values pour
stocker, partager et consommer des données d’application
■■
Interroger des fournisseurs de contenu de façon asynchrone grâce aux chargeurs
de curseurs
■■
Ajouter des fonctionnalités de recherche à vos applications
■■
Utiliser les fournisseurs de contenu natifs MediaStore, Contacts et Agenda
Ce chapitre présente les mécanismes de stockage persistant d’Android, en commençant
par la base de données SQLite. Cette API offre une bibliothèque de bases de données
SQL puissante, qui fournit une couche de persistance robuste et entièrement contrôlable.
Vous apprendrez également à construire et à utiliser des fournisseurs de contenu pour
stocker, partager et consommer des données structurées dans et entre vos applications.
Les fournisseurs de contenu offrent une interface standard vers n’importe quelle
source de données en découplant la couche de stockage des données de la couche
applicative. Vous verrez comment interroger les fournisseurs de contenu de façon
asynchrone afin de garantir la réactivité de votre application.
Bien que l’accès à une base de données soit réservé à l’application qui l’a créée, les
fournisseurs de contenu offrent à vos applications un mécanisme standard pour
partager leurs données et consommer les données d’autres applications – notamment
celles de nombreux dépôts de données natifs.
Vous apprendrez également à ajouter une fonction de recherche à vos applications
et à construire des fournisseurs de contenu capables de fournir des suggestions de
recherche en temps réel.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 263
03/08/12 07:26
264

Android 4
Les fournisseurs de contenu pouvant être utilisés entre les applications, vous avez
la possibilité d’intégrer plusieurs fournisseurs natifs dans vos propres applications,
comme les contacts, l’agenda et le MediaStore. Vous apprendrez à stocker et à
récupérer des données de ces applications essentielles d’Android afin de fournir
à vos utilisateurs un plus grand confort et une intégration totale avec le système.
Introduction aux bases de données Android
Android assure la persistance des données structurées à l’aide d’une combinaison de
bases de données SQLite et de fournisseurs de contenu. Les bases de données SQLite
peuvent servir à stocker les données des applications au moyen d’une approche structurée et gérée. Android offre une bibliothèque SQLite complète. Chaque application
peut créer ses propres bases sur lesquelles elle dispose d’un contrôle absolu.
Si vous avez créé votre dépôt de données sous-jacent, les fournisseurs de contenu
offrent une interface générique et bien définie pour l’utilisation et le partage de
données.Bases de données SQLite
Vous pouvez créer à l’aide de SQLite des bases de données relationnelles indépendantes
pour vos applications. Utilisez-les pour stocker et gérer des données d’application
complexes et structurées.
Les bases de données Android sont stockées sur votre terminal (ou votre émulateur)
dans le dossier /data/data/<nom_package>/databases. Toutes les bases de données
sont privées et ne sont accessibles que par l’application qui les a créées.
La conception de bases de données est un vaste sujet qui mériterait beaucoup plus de
temps que celui que nous pouvons lui accorder dans ce livre. Il faut souligner que les
bonnes pratiques de conception des bases de données s’appliquent sous Android. En
particulier, lorsque vous créez des bases de données pour des appareils aux ressources
limitées (comme des téléphones mobiles), il est important de normaliser vos données
pour réduire les redondances.
Fournisseurs de contenu
Les fournisseurs de contenu mettent à disposition une interface pour la publication et
la consommation des données, reposant sur un modèle simple d’adressage par URI
utilisant le schéma content://. Ils vous permettent de découpler la couche applicative
de la couche de données, rendant ainsi vos applications indépendantes des sources
des données en masquant les sources de données sous-jacentes.
Les fournisseurs de contenu peuvent être partagés entre les applications et interrogés ;
leurs enregistrements peuvent être mis à jour ou supprimés et de nouvelles données
peuvent y être ajoutées. Toute application possédant les permissions appropriées peut
ajouter, supprimer ou mettre à jour des données d’une autre application, y compris
celles des fournisseurs de contenu natifs d’Android.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 264
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
265
Plusieurs fournisseurs de contenu natifs sont accessibles par les applications tierces,
notamment le gestionnaire des contacts, la base de médias et l’agenda, comme nous
le verrons plus loin dans ce chapitre.
En publiant vos propres fournisseurs de contenu, vous vous donnez la possibilité
(ainsi qu’à d’autres développeurs) d’incorporer et d’étendre vos données dans de
nouvelles applications.
Introduction à SQLite
SQLite est un système de gestion de bases de données relationnelles (SGBDR) bien
connu. Il est :
■■
open-source ;
■■
conforme aux standards ;
■■
léger ;
■■
mono tiers.
Il a été implémenté sous la forme d’une bibliothèque C compacte incluse dans Android.
Étant implémenté sous forme de bibliothèque et non exécuté dans un processus
distinct, chaque base de données SQLite fait partie intégrante de l’application qui
l’a créée. Cela réduit les dépendances externes, minimise la latence et simplifie le
verrouillage des transactions et la synchronisation.
SQLite a une réputation de grande fiabilité et il est le SGBDR choisi par de nombreux
appareils électroniques, notamment beaucoup de lecteurs MP3 et de smartphones.
Léger et puissant, il diffère des moteurs de bases de données conventionnels par son
typage faible des colonnes, ce qui signifie que les valeurs d’une colonne ne doivent
pas forcément être d’un seul type. Chaque valeur est typée individuellement par
ligne. La conséquence en est que la vérification de type n’est pas obligatoire lors de
l’affectation ou de l’extraction des valeurs des colonnes d’une ligne.
Info
Vous trouverez des informations complètes sur SQLite sur le site officiel http://www.
sqlite.org/, en particulier sur ses forces et limites.
Curseurs et Content Values
Les Content Values servent à insérer de nouvelles lignes dans des tables. Chaque
objet ContentValues représente une ligne de la table sous la forme d’une association
des noms de colonnes vers les valeurs.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 265
03/08/12 07:26
266

Android 4
Sous Android, le résultat des requêtes est renvoyé sous la forme d’objets Cursor.
Ceux-ci sont des pointeurs vers les résultats et non des extractions des valeurs. Ils
fournissent un moyen de contrôler votre position (ligne) dans le résultat d’une requête,
d’où leur nom..
La classe Cursor inclut de nombreuses fonctions de déplacement dont quelques
exemples suivent :
■■ moveToFirst.
■■ moveToNext.
Déplace le curseur sur la première ligne du résultat.
Déplace le curseur sur la ligne suivante.
■■ moveToPrevious.
■■ getCount.
Déplace le curseur sur la ligne précédente.
Renvoie le nombre de lignes du résultat.
■■ getColumnIndexOrThrow. Renvoie l’indice de la colonne indiquée et renvoie une
exception si aucune colonne de ce nom n’existe.
■■ getColumnName.
Renvoie le nom de la colonne à l’indice indiqué.
■■ getColumnNames.
Renvoie un tableau de chaînes contenant les noms de toutes
les colonnes du curseur courant.
■■ getPosition.
Renvoie la position courante du curseur.
Android fournit un mécanisme pratique pour garantir que les requêtes s’effectuent
de façon asynchrone : la classe CursorLoader et son gestionnaire associé ont été
introduits par Android 3.0 (API level 11) et font désormais partie de la bibliothèque
support, ce qui vous permet d’en tirer profit tout en supportant les versions plus
anciennes d’Android.
Plus loin dans ce chapitre, vous apprendrez comment interroger une base de données
et extraire des valeurs spécifiques de ligne et de colonne des curseurs résultants.
Utiliser des bases de données SQLite
Cette section explique comment créer et utiliser les bases de données SQLite dans
vos applications.
Lorsque l’on utilise des bases de données, il est conseillé d’encapsuler la base de
données sous-jacente et de n’exposer que les méthodes et les constantes publiques
nécessaires aux interactions avec la base – en se servant d’une classe utilitaire. Cette
classe doit exposer les constantes de la base de données – notamment les noms des
colonnes – nécessaires à son remplissage et à son interrogation. Plus loin dans ce
chapitre, nous présenterons les fournisseurs de contenu, qui peuvent également servir
à exposer ces constantes d’interaction.
Le Listing 8.1 montre un exemple de constantes de la base de données qui devraient
être rendues publiques dans une classe utilitaire.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 266
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
267
Listing 8.1 : Squelette de code pour les constantes de la classe utiliitaire
// Colonne index (clé) à utiliser dans les clauses where.
public static final String KEY_ID="_id";
// Nom et indice de chaque colonne dans la base de données.
// Ces noms doivent être évocateurs.
public static final String KEY_GOLD_HOARD_NAME_COLUMN =
“GOLD_HOARD_NAME_COLUMN”;
public static final String KEY_GOLD_HOARD_ACCESSIBLE_COLUMN =
“OLD_HOARD_ACCESSIBLE_COLUMN”;
public static final String KEY_GOLD_HOARDED_COLUMN =
“GOLD_HOARDED_COLUMN”;
// À faire : créer des variables publiques pour chaque colonne.
Introduction à SQLiteOpenHelper
est une classe abstraite utilisée pour implémenter un modèle de
bonnes pratiques pour la création, l’ouverture et la mise à jour des bases de données.
SQLiteOpenHelper
En l’implémentant, vous masquez la logique utilisée pour décider si une base de
données doit être créée ou mise à jour avant d’être ouverte et vous garantissez que
chaque opération s’exécute de façon efficace.
Il est conseillé de reporter la création et l’ouverture des bases de données tant qu’elles
ne sont pas nécessaires. L’objet SQLiteOpenHelper met en cache les instances de
la base une fois qu’elles ont été ouvertes : vous pouvez donc demander à ouvrir la
base juste avant d’effectuer une requête ou une transaction. Pour la même raison, il
n’est pas nécessaire de fermer la base de données manuellement, sauf si vous n’en
avez plus besoin.
Info
Les opérations sur les bases de données, notamment leur ouverture ou leur création,
peuvent durer un certain temps. Pour être sûr que cela ne pénalisera pas le confort
d’utilisation de votre application, faites en sorte que toutes les transactions sur la base
soient asynchrones.
Le Listing 8.2 montre comment étendre SQLiteOpenHelper en redéfinissant le
constructeur et les méthodes onCreate et onUpgrade pour prendre en charge, respectivement, la création d’une base de données et la mise à jour vers une nouvelle version.
Listing 8.2 : Implémentation de SQLiteOpenHelper
private static class HoardDBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = “myDatabase.db”;
private static final String DATABASE_TABLE = “GoldHoards”;
private static final int DATABASE_VERSION = 1;
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 267
03/08/12 07:26
268

Android 4
// Instruction SQL pour créer une base de données.
private static final String DATABASE_CREATE = “create table “ +
DATABASE_TABLE + “ (“ + KEY_ID +
“ integer primary key autoincrement, “ +
KEY_GOLD_HOARD_NAME_COLUMN + “ text not null, “ +
KEY_GOLD_HOARDED_COLUMN + “ float, “ +
KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + “ integer);”;
public HoardDBOpenHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version);
}
// Appelée lorsque aucune base n’existe sur le disque et que la classe
// utilitaire doit en créer une nouvelle.
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
// Appelée si une version de la base ne correspond pas, ce qui signifie
// que la version de la base sur le disque doit être mise à jour vers
// la version courante.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
// Inscrit la mise à jour de version dans le journal.
Log.w(“TaskDBAdapter”, “Mise à jour de la version “ +
oldVersion + “ vers la version “ +
newVersion + “, ce qui détruira toutes les anciennes données”);
// Mise à jour de la base existante pour se conformer à la nouvelle
// version. Plusieurs versions antérieures peuvent être gérées en
// comparant les valeurs de oldVersion et newVersion.
// Le cas le plus simple consiste à supprimer l’ancienne table et à
// en créer une nouvelle.
db.execSQL(“DROP TABLE IF IT EXISTS “ + DATABASE_TABLE);
// Création d’une nouvelle.
onCreate(db);
}
}
Info
Dans cet exemple, onUpgrade supprime simplement la table existante et la remplace
par sa nouvelle définition. C’est souvent la solution la plus simple et la plus pratique.
Cependant, pour les données importantes qui ne sont pas synchronisées avec des services
en ligne ou qui sont difficiles à reproduire, une meilleure approche consiste à migrer
les données existantes dans la nouvelle table.
Pour accéder à une base de données via cette classe utilitaire, appelez
getWritableDatabase ou getReadableDatabase pour ouvrir et renvoyer, respectivement, une instance en écriture ou en lecture de la base sous-jacente.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 268
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
269
En coulisses, si la base n’existe pas, l’objet SQLiteOpenHelper exécute son gestionnaire onCreate. Si la version de la base a changé, le gestionnaire onUpgrade sera
lancé. Dans les deux cas, l’appel à getWritableDatabase ou getReadableDatabase
renverra la base de données en cache, nouvellement ouverte, nouvellement créée
ou mise à jour.
Lorsqu’une base de données a été ouverte, le SQLiteOpenHelper met en cache la
base de données qui vient d’être ouverte : vous pouvez (et devriez) donc utiliser ces
méthodes pour chaque interrogation ou transaction sur la base, au lieu de mettre en
cache la base de données dans votre application.
Un appel à getWritableDatabase peut échouer en raison d’un problème d’espace
disque ou de permissions. Il est donc conseillé de se replier sur la méthode
getReadableDatabase pour les requêtes. Dans la plupart des cas, cette méthode
renverra la même instance de base de données en cache et ouverte en écriture que
getWritableDatabase, sauf si la base n’existe pas encore ou que les mêmes problèmes
de permission ou d’espace disque interviennent, auquel cas elle renverra une copie
en lecture seule.
Info
Pour créer ou mettre à jour une base de données, celle-ci doit être ouverte en écriture.
Il est donc généralement conseillé de tenter d’abord de l’ouvrir en écriture et de ne
se replier vers une ouverture en lecture seule que si cette première tentative échoue.
Ouvrir et créer des bases de données sans SQLiteOpenHelper
Si vous préférez gérer directement la création, l’ouverture et le contrôle de version
de vos bases de données, vous pouvez utiliser la méthode openOrCreateDatabase
dans le contexte de l’application :
SQLiteDatabase db = context.openOrCreateDatabase(DATABASE_NAME,
Context.MODE_PRIVATE,
null);
Après avoir créé la base de données, vous devez gérer la création et les mises à jour
de versions qui sont normalement prises en charge dans les gestionnaires onCreate
et onUpdate de SQLiteOpenHelper – généralement en utilisant la méthode execSQL
de la base de données pour créer et supprimer les tables.
Il est conseillé de reporter la création et l’ouverture des bases de données tant qu’elles
ne sont pas nécessaires et de mettre en cache les instances de la base après leur
ouverture afin de limiter les coûts que ces opérations induisent en termes d’efficacité.
Au minimum, ces opérations doivent être traitées de façon asynchrone pour éviter
de perturber le thread principal de l’application.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 269
03/08/12 07:26
270

Android 4
Considérations sur la conception d’une base de données Android
Il existe plusieurs considérations spécifiques à Android que vous devrez garder à
l’esprit lorsque vous concevrez votre base de données.
■■ Les fichiers (comme les bitmaps ou les fichiers audio) ne sont en général pas
stockés dans une base. Utilisez une chaîne pour stocker un chemin vers le fichier,
de préférence une URI qualifiée.
■■ Il est recommandé, bien que non obligatoire, que toutes les tables contiennent
une colonne auto-incrémentée qui servira d’index unique à chaque ligne. Si vous
prévoyez de partager votre table en utilisant un fournisseur de contenu, cette
colonne devient obligatoire.
Interroger une base de données
Chaque requête sur une base de données est renvoyée sous la forme d’un objet Cursor.
Ceci permet à Android de gérer plus efficacement les ressources en retrouvant et en
libérant les valeurs de lignes et de colonnes à la demande.
Pour exécuter une requête sur une base de données, utilisez la méthode query en lui
passant les éléments suivants :
■■
■■
■■
■■
■■
■■
■■
■■
■■
Un booléen facultatif indiquant si le résultat ne doit contenir que des valeurs
uniques.
Le nom de la table à interroger.
Une projection sous forme de tableau de chaînes énumérant les colonnes à
inclure dans le résultat.
Une clause where définissant les lignes à ramener. Vous pouvez inclure des
jokers ? qui seront remplacés par les valeurs passées par le paramètre des
arguments de sélection.
Un tableau d’arguments de sélection qui remplaceront les ? de la clause where.
Une clause group by qui définira comment les lignes de résultat devront être
groupées.
Une clause having définissant les groupes de lignes à inclure lorsque l’on a
indiqué une clause group by.
Une chaîne décrivant l’ordre des lignes ramenées.
Une chaîne facultative définissant la limite du nombre de lignes ramenées.
Le Listing 8.3 montre comment récupérer une sélection de lignes d’une table SQLite.
Listing 8.3 : Interrogation d’une base de données
// Précise la projection des colonnes du résultat. On renvoie l’ensemble
// de colonnes minimum correspondant à nos besoins.
String[] result_columns = new String[] {
KEY_ID, KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, KEY_GOLD_HOARDED_COLUMN };
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 270
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
271
// Clause where pour limiter le nombre de lignes du résultat.
String where = KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + “=1”;
// À remplacer par des instructions SQL valides en fonction des besoins.
String whereArgs[] = null;
String groupBy = null;
String having = null;
String order = null;
SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
Cursor cursor = db.query(HoardDBOpenHelper.DATABASE_TABLE,
result_columns, where,
whereArgs, groupBy, having, order);
Info
Dans le Listing 8.3, on ouvre une instance de base de données SQLite à l’aide de l’implémentation de SQLiteOpenHelper, qui reporte la création et l’ouverture des instances de
base tant qu’elles ne sont pas nécessaires et les met en cache après leur ouverture.
En conséquence, il est conseillé de demander une instance de base de données chaque
fois que l’on effectue une requête ou une transaction sur la base. Pour des raisons
d’efficacité, vous ne devriez fermer votre instance de base de données que si vous
pensez que vous n’en aurez plus besoin – typiquement, lorsque l’activité ou le service
qui l’utilise est stoppé.
Extraire les résultats d’un curseur
Pour extraire les valeurs d’un curseur, utilisez d’abord les méthodes moveTo<location>
décrites plus haut pour positionner le curseur sur la bonne ligne puis utilisez les
méthodes get<type> (en passant l’indice de colonne) pour récupérer la valeur d’une
colonne de la ligne courante. Pour trouver l’indice d’une colonne particulière dans
un curseur, servez-vous de ses méthodes getColumnIndexOrThrow et getColumnIndex.
Lorsque la colonne est censée exister dans tous les cas, il est conseillé d’utiliser
getColumnIndexOrThrow. En revanche, appeler getColumnIndex et tester qu’elle renvoie
–1, comme on le fait dans l’extrait ci-dessous, est une technique plus efficace que
la capture des exceptions lorsque la colonne peut ne pas exister dans certains cas.
int columnIndex = cursor.getColumnIndex(KEY_COLUMN_1_NAME);
if (columnIndex > -1) {
String columnValue = cursor.getString(columnIndex);
// Traiter la valeur de la colonne.
} else {
// Faire quelque chose d’autre si la colonne n’existe pas.
}
Info
Les implémentations de bases de données doivent publier des constantes statiques qui
donnent les noms des colonnes. Ces constantes statiques sont généralement exposées
par la classe utilitaire ou le fournisseur de contenu.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 271
03/08/12 07:26
272

Android 4
Le Listing 8.4 montre comment parcourir un curseur et extraire une colonne de
valeurs flottantes pour en faire la moyenne.
Listing 8.4 : Extraction des valeurs d’un Cursor
float totalHoard = 0f;
float averageHoard = 0f;
// Trouve l’indice de la (les) colonne(s) utilisée(s).
int GOLD_HOARDED_COLUMN_INDEX =
cursor.getColumnIndexOrThrow(KEY_GOLD_HOARDED_COLUMN);
// Parcourt les lignes du curseur.
// Le curseur est positionné avant la première ligne lorsqu’il est
// initialisé : on peut donc simplement vérifier si une colonne "suivante"
// existe. Si le curseur est vide, l’appel renverra false.
while (cursor.moveToNext()) {
float hoard = cursor.getFloat(GOLD_HOARDED_COLUMN_INDEX);
totalHoard += hoard;
}
// Calcule une moyenne – en vérifiant les divisions par zéro.
float cursorCount = cursor.getCount();
averageHoard = cursorCount > 0 ? (totalHoard / cursorCount) : Float.NaN;
// Fermeture du curseur lorsque l’on n’en a plus besoin.
cursor.close();
Les colonnes des bases SQLite étant faiblement typées, vous pouvez les transtyper en
types valides selon vos besoins. Les valeurs stockées en virgules flottantes peuvent,
par exemple, être lues comme des chaînes.
Lorsque vous avez fini d’utiliser le curseur, il est important de le fermer pour éviter
les fuites mémoire et pour réduire les ressources utilisées par l’application :
cursor.close();
Ajouter, mettre à jour et supprimer des lignes
La classe SQLiteDatabase expose les méthodes insert, delete et update qui encapsulent les instructions SQL requises pour ces actions. De plus, la méthode execSQL
vous permet d’exécuter n’importe quelle instruction SQL valide sur vos tables si vous
souhaitez exécuter ces opérations (ou d’autres) manuellement.
Chaque fois que vous modifiez des valeurs de la base sous-jacente, vous devez mettre
à jour vos curseurs en exécutant une nouvelle requête.
Insérer de nouvelles lignes
Pour créer une nouvelle ligne, construisez un objet ContentValues et utilisez sa
méthode put pour ajouter des paires nom/valeur représentant chaque nom de colonne
et sa valeur associée.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 272
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
273
Insérez la nouvelle ligne en passant cet objet à la méthode insert de la base cible,
ainsi que le nom de la table (voir le Listing 8.5).
Listing 8.5 : Insertion de nouvelles lignes dans une base
// Crée une nouvelle ligne à insérer.
ContentValues newValues = new ContentValues();
// Affecte des valeurs à chaque ligne.
newValues.put(KEY_GOLD_HOARD_NAME_COLUMN, hoardName);
newValues.put(KEY_GOLD_HOARDED_COLUMN, hoardValue);
newValues.put(KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, hoardAccessible);
// [ ... Répéter pour chaque paire nom/valeur de colonne ... ]
// Insère la ligne.
SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
db.insert(HoardDBOpenHelper.DATABASE_TABLE, null, newValues);
Info
Le second paramètre passé à insert dans le Listing 8.5 est appelé "astuce de la colonne
null".
Si vous voulez ajouter une ligne vide dans une base de données SQLite en passant un
objet ContentValues vide, vous devez également passer le nom d’une colonne dont la
valeur est explicitement fixée à null.
Lorsque vous insérez une nouvelle ligne dans une base de données SQLite, vous devez
toujours préciser au moins une colonne et sa valeur correspondante – qui peut être
null. Si le second paramètre de insert est null, comme avec l’astuce de la colonne null,
l’insertion d’un objet ContentValues vide lèvera une exception.
Il est généralement préférable de s’assurer que votre code ne tente pas d’insérer des
ContentValues vides dans une base de données SQLite.
Mettre à jour des lignes
La mise à jour se fait également avec des valeurs de contenus.
Créez un nouvel objet ContentValues et utilisez la méthode put pour affecter de
nouvelles valeurs à chaque colonne que vous voulez mettre à jour. Appelez update
sur la base de données en lui passant le nom de la table, l’objet ContentValues modifié
et une clause where précisant la ou les lignes à mettre à jour (voir le Listing 8.6).
Listing 8.6 : Mise à jour d’une ligne
// Définit le contenu de la ligne mise à jour.
ContentValues updatedValues = new ContentValues();
// Affecte une valeur pour chaque ligne.
updatedValues.put(KEY_GOLD_HOARDED_COLUMN, newHoardValue);
[ ... Répéter pour chaque colonne à modifier... ]
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 273
03/08/12 07:26
274

Android 4
// Création d’une clause where définissant les lignes qui devront
// être mises à jour. Définit les paramètres éventuels de where.
String where = KEY_ID + "=" + hoardId; String whereArgs[] = null;
// Met à jour la ligne indiquée par where avec les nouvelles valeurs.
SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
db.update(HoardDBOpenHelper.DATABASE_TABLE, updatedValues,
where, whereArgs);
Supprimer des lignes
Pour supprimer une ligne, appelez simplement delete sur la base en indiquant le
nom de la table et une clause where ramenant les lignes que vous voulez supprimer
(voir le Listing 8.7).
Listing 8.7 : Suppression d’une ligne
// Créer une clause where précisant la ou les lignes à supprimer.
// Définit les paramètres éventuels de where.
String where = KEY_GOLD_HOARDED_COLUMN + “=0”;
String whereArgs[] = null;
// Supprime les lignes correspondant à la clause where.
SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase();
db.delete(HoardDBOpenHelper.DATABASE_TABLE, where, whereArgs);
Créer des fournisseurs de contenu
Les fournisseurs de contenu offrent une interface pour la publication de données qui
seront consommées à l’aide d’un résolveur de contenus. Ils permettent de découpler
les sources de données sous-jacentes et les composants applicatifs qui consomment
les données, offrant ainsi un mécanisme générique grâce auquel les applications
peuvent partager leurs données ou consommer les données fournies par d’autres.
Pour créer un nouveau fournisseur de contenu, étendez la classe abstraite
ContentProvider :
public class MyContentProvider extends ContentProvider
Comme pour la classe utilitaire décrite dans la section précédente, il est conseillé de
définir des constantes statiques dans cette classe – notamment les noms des colonnes
et l’autorité du fournisseur de contenu – qui seront requises par la suite pour effectuer
des transactions sur la base de données ou pour l’interroger.
Vous devez également redéfinir la méthode onCreate pour créer (et initialiser) la
source de données sous-jacente, ainsi que les méthodes query, update, delete, insert
et getType pour implémenter l’interface utilisée par le résolveur de contenus pour
interagir avec les données, comme nous l’expliquons dans les sections qui suivent.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 274
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
275
Enregistrer les fournisseurs de contenu
Comme les activités et les services, les fournisseurs de contenu doivent être enregistrés
dans le manifeste de votre application pour que le résolveur puisse les découvrir.
Pour cela, on utilise la balise provider qui dispose d’un attribut name fournissant le
nom de la classe du fournisseur et d’un attribut authorities qui définit l’URI de
base de l’autorité du fournisseur.
Cette autorité du fournisseur de contenu est utilisée par le résolveur de contenus
comme une adresse et permet de retrouver la base de données avec laquelle on
souhaite interagir.
Chaque autorité de fournisseur devant être unique, il est conseillé d’utiliser le chemin du
nom de votre paquetage pour construire cette URI. La forme générale de l’autorité d’un
fournisseur de contenu est donc : com.<NomSociété>.provider.<NomApplication>.
Une balise provider complète doit ressembler à cet exemple :
<provider android:name=“.MyContentProvider”
android:authorities=”com.paad.skeletondatabaseprovider”/>
Publier l’URI de votre fournisseur de contenu
Chaque fournisseur de contenu devrait exposer son autorité en utilisant une propriété
statique publique CONTENT_URI afin qu’elle soit plus facilement découverte. Cette
propriété devrait contenir un chemin d’accès vers le contenu primaire :
public static final Uri CONTENT_URI =
Uri.parse(“content://com.paad.skeletondatabaseprovider/elements”);
Ces URI de contenu seront utilisées par un résolveur de contenus pour accéder à
votre fournisseur. Une requête utilisant l’URI ci-dessus représente une requête de
toutes les lignes, alors qu’une URI se terminant par /<numéro de ligne>, comme
dans l’exemple ci-dessous, permet de créer une requête d’une seule ligne :
content://com.paad.skeletondatabaseprovider/elements/5
Il est conseillé de supporter ces deux formes d’accès à votre fournisseur. Le moyen
le plus simple d’y parvenir consiste à utiliser un UriMatcher pour analyser les URI
et déterminer leurs formes.
Le Listing 8.8 montre un squelette d’implémentation pour définir un analyseur d’URI
qui détermine si une URI est une requête de toutes les données ou d’une simple ligne.
Listing 8.8 : Définition d’un UriMatcher pour déterminer si une requête porte sur
tous les éléments ou sur une seule ligne
// Crée les constantes utilisées pour différencier les requêtes
private static final int ALLROWS = 1;
private static final int SINGLE_ROW = 2;
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 275
03/08/12 07:26
276

Android 4
private static final UriMatcher uriMatcher;
// Remplit l’objet UriMatcher, où une URI se terminant par
// ’elements’ correspondra à une requête de tous les éléments et
// ’elements/[rowID]’ correspondra à une requête d’une seule ligne.
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements”, ALLROWS);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements/#”, SINGLE_ROW);
Vous pouvez utiliser la même technique pour exposer des URI alternatives pour différents sous-ensembles de données ou différentes tables dans votre base, en utilisant
le même fournisseur de contenu.
Maintenant que vous savez différencier les requêtes sur toute une table et sur une seule
ligne, vous pouvez vous servir de la classe SQLiteQueryBuilder pour appliquer une
condition de sélection supplémentaire à votre requête, comme dans l’exemple suivant :
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
// Si c’est une requête d’une seule ligne, on limite l’ensemble résultat
// à la ligne transmise en paramètre.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
Plus loin dans ce chapitre, nous verrons comment effectuer une requête à l’aide de
SQLiteQueryBuilder .
Créer la base de données du fournisseur de contenu
Pour initialiser la source de données à laquelle vous comptez accéder au moyen
du fournisseur de contenu, redéfinissez la méthode onCreate comme dans le Listing 8.9. On utilise généralement une implémentation de SQLiteOpenHelper, du type
décrit dans la section précédente, afin de reporter la création et l’ouverture de la
base de données tant qu’elle n’est pas nécessaire.
Listing 8.9 : Création de la base de données du fournisseur de contenu
private MySQLiteOpenHelper myOpenHelper;
@Override
public boolean onCreate() {
// Construction de la base de données sous-jacente.
// Reporte l’ouverture de la base tant que l’on n’en a pas besoin
// pour une requête ou une transaction.
myOpenHelper = new MySQLiteOpenHelper(getContext(),
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 276
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
277
MySQLiteOpenHelper.DATABASE_NAME, null,
MySQLiteOpenHelper.DATABASE_VERSION);
return true;
}
Info
Lorsque votre application est lancée, le gestionnaire onCreate de chacun de ses fournisseurs de contenu est exécuté dans le thread principal de l’application.
Comme pour les exemples précédents de la section précédente, la meilleure approche
consiste à utiliser un SQLiteOpenHelper pour reporter l’ouverture (et, si nécessaire, la
création) de la base de données sous-jacente tant qu’elle n’est pas nécessaire dans les
méthodes query et transaction du fournisseur de contenu.
Pour des raisons d’efficacité, il est préférable de laisser le fournisseur de contenu ouvert
tant que l’application s’exécute ; il n’est pas utile de fermer manuellement la base de
données. Si le système a besoin de ressources, votre application sera tuée et les bases
de données associées seront fermées.
Implémenter les requêtes au fournisseur de contenu
Pour supporter les requêtes à votre fournisseur de contenu, vous devez implémenter
les méthodes query et getType qui seront utilisées par les résolveurs de contenu
pour accéder aux données sous-jacentes sans connaître leur structure ou leur implémentation. Ces méthodes permettent aux applications de partager leurs données
sans devoir publier une interface spécifique à chaque source de données.
Le scénario le plus courant consiste à utiliser un fournisseur de contenu pour offrir
un accès à une base de données SQLite, mais ces méthodes permettent d’accéder
à n’importe quelle source de données (notamment à des fichiers ou à des variables
d’instance de l’application).
Notez que l’objet UriMatcher sert à affiner les requêtes et les transactions, et que
l’objet SQLiteQueryBuilder est un outil pratique pour réaliser des requêtes de lignes.
Le Listing 8.10 est un squelette d’implémentation de requêtes sur un fournisseur de
contenu utilisant une base de données SQLite sous-jacente.
Listing 8.10 : Implémentation de requêtes et de transactions sur un fournisseur de
contenu
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Ouverture de la base de données.
SQLiteDatabase db;
try {
db = myOpenHelper.getWritableDatabase();
} catch (SQLiteException ex) {
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 277
03/08/12 07:26
278

Android 4
db = myOpenHelper.getReadableDatabase();
}
// À remplacer par des instructions SQL valides si nécessaire.
String groupBy = null;
String having = null;
// Utilisation d’un objet SQLiteQueryBuilder pour simplifier la
// construction de la requête.
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
// Si c’est une requête de ligne, on limite l’ensemble résultat à
// la ligne passée en paramètre.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
// Précise la table sur laquelle effectuer la requête.
// Il peut s’agir d’une table spécifique ou d’une jointure.
queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
// Exécute la requête.
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, groupBy, having,
sortOrder);
// Renvoie le curseur résultat.
return cursor;
}
Si vous implémentez les requêtes, vous devez également préciser un type MIME
pour identifier les données renvoyées. Pour cela, redéfinissez la méthode getType
pour qu’elle renvoie une chaîne décrivant de façon unique le type de vos données.
Le type renvoyé pourra être de deux formes – une pour les entrées simples, l’autre
pour toutes les entrées :
■■
Élément simple :
vnd.android.cursor.item/vnd.<nomSociété>.<typeContenu>
■■
Tous les éléments :
vnd.android.cursor.dir/vnd.<nomSociété>.<typeContenu>
Le Listing 8.11 montre comment redéfinir la méthode getType pour qu’elle renvoie
le type MIME correct en fonction de l’URI.
Listing 8.11 : Renvoi du type MIME d’un fournisseur de contenu
@Override
public String getType(Uri uri) {
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 278
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
279
// Renvoie une chaîne qui identifie le type MIME
// d’une URI de fournisseur de contenu.
switch (uriMatcher.match(uri)) {
case ALLROWS:
return “vnd.android.cursor.dir/vnd.paad.elemental”;
case SINGLE_ROW:
return “vnd.android.cursor.item/vnd.paad.elemental”;
default:
throw new IllegalArgumentException(“URI non reconnue : “ + uri); }
➥ Transactions sur un fournisseur de contenu
Pour exposer les transactions de suppression, insertion et mise à jour sur votre
fournisseur de contenu, implémentez les méthodes delete, insert, et update
correspondantes.
Comme la méthode query, ces méthodes seront utilisées par le résolveur de contenus
pour effectuer les transactions sur les données sous-jacentes sans connaître leur
implémentation – ce qui permet aux applications de modifier des données entre
elles. Il est préférable d’utiliser la méthode notifyChange du résolveur lorsque vous
effectuez des transactions qui modifient les données des tables. Celle-ci préviendra
les observateurs de contenu enregistrés pour un curseur donné (à l’aide de la méthode
Cursor.registerContentObserver) que la table sous-jacente (ou l’une de ses lignes)
a été supprimée, ajoutée ou modifiée. Comme pour les requêtes, le cas d’utilisation
le plus fréquent est l’exécution d’une transaction sur une base de données SQLite,
bien que ce ne soit pas obligatoire. Le Listing 8.12 est un squelette de code qui implémente des transactions sur un fournisseur de contenu utilisant une base de données
SQLite sous-jacente.
Listing 8.12 : Implémentation typique des transactions sur un fournisseur de contenu
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la suppression à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
//
//
//
if
Il faut indiquer une clause where pour renvoyer le nombre d’éléments
supprimés. Pour supprimer toutes les lignes et renvoyer une valeur,
passez le paramètre "1".
(selection == null)
selection = “1”;
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 279
03/08/12 07:26
280

Android 4
// Effectue la suppression.
int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE,
selection, selectionArgs);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(uri, null);
// Renvoie le nombre d’éléments supprimés.
return deleteCount;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Pour ajouter des lignes vides la base en passant un objet
// ContentValues vide, vous devez utiliser l’astuce du paramètre de
// colonne null pour indiquer le nom de la colonne qui peut être mise
// à null.
String nullColumnHack = null;
// Insère les valeurs dans la table.
long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
nullColumnHack, values);
// Construit et renvoie l’URI de la ligne insérée.
if (id > -1) {
// Construit et renvoie l’URI de la ligne insérée.
Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(insertedId, null);
return insertedId;
} else
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la modification à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
// Effectue la modification.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 280
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
281
int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,
values, selection, selectionArgs);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
Info
La classe ContentUris fournit la méthode withAppendedId qui permet d’ajouter facilement
un identifiant de ligne à la valeur CONTENT_URI d’un fournisseur de contenu. Nous l’utilisons dans le Listing 8.12 pour construire l’URI des lignes que l’on vient d’insérer et
nous nous en servirons également dans les sections suivantes pour désigner une ligne
particulière au cours des requêtes et des transactions sur la base de données.
Stocker des fichiers dans un fournisseur de contenu
Au lieu de stocker directement les gros fichiers dans votre fournisseur de contenu,
vous devriez les représenter sous la forme d’URI pleinement qualifiées pointant
vers leur emplacement sur le système de fichiers.
Pour qu’une table puisse supporter les fichiers, vous devez inclure une colonne nommée _data qui contiendra le chemin vers le fichier représenté par cette ligne. Cette
colonne ne devrait pas être utilisée par les applications clientes. Redéfinissez le
gestionnaire openFile pour qu’il renvoie un objet ParcelFileDescriptor lorsque
le résolveur de contenus demande le fichier associé à cette ligne.
Généralement, un fournisseur de contenu comprend deux tables, une qui ne sert
qu’à stocker les fichiers externes, l’autre qui contient une colonne destinée à l’utilisateur, contenant une référence vers les lignes de la première.
Le Listing 8.13 est un squelette de redéfinition du gestionnaire openFile d’un fournisseur de contenu. Ici, le nom du fichier sera représenté par l’identifiant de la ligne
à laquelle il appartient.
Listing 8.13 : Stockage de fichiers dans un fournisseur de contenu
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
// Trouve l’identifiant de ligne et l’utilise comme nom de fichier.
String rowID = uri.getPathSegments().get(1);
// Crée un objet fichier dans le répertoire des fichiers externes de
// l’application.
String picsDir = Environment.DIRECTORY_PICTURES;
File file = new File(getContext().getExternalFilesDir(picsDir), rowID);
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 281
03/08/12 07:26
282

Android 4
// Si le fichier n’existe pas, on le crée.
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, “Échec de création du fichier : “ + e.getMessage());
}
}
// Traduit le mode d’ouvertur dans le mode correspondant pour le
// ParcelFileDescriptor
int fileMode = 0;
if (mode.contains(“w”))
fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
if (mode.contains(“r”))
fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;
if (mode.contains(“+”))
fileMode |= ParcelFileDescriptor.MODE_APPEND;
// Renvoie un ParcelFileDescriptor qui représente le fichier.
return ParcelFileDescriptor.open(file, fileMode);
}
Info
Les fichiers associés à des lignes dans la base de données étant stockés à l’extérieur
de celle-ci, il est important de réfléchir à l’effet que devrait avoir la suppression d’une
ligne sur le fichier sous-jacent.
Squelette d’implémentation d’un fournisseur de contenu
Le Listing 8.14 est un squelette d’implémentation d’un fournisseur de contenu. Il utilise un objet SQLiteOpenHelper pour gérer la base et transmet simplement chaque
requête ou transaction directement à la base de données SQLite sous-jacente.
Listing 8.14 : Squelette d’implémentation d’un fournisseur de contenu
import
import
import
import
import
import
import
import
import
import
import
import
import
android.content.ContentProvider;
android.content.ContentUris;
android.content.ContentValues;
android.content.Context;
android.content.UriMatcher;
android.database.Cursor;
android.database.sqlite.SQLiteDatabase;
android.database.sqlite.SQLiteDatabase.CursorFactory;
android.database.sqlite.SQLiteOpenHelper;
android.database.sqlite.SQLiteQueryBuilder;
android.net.Uri;
android.text.TextUtils;
android.util.Log;
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 282
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
283
public class MyContentProvider extends ContentProvider {
public static final Uri CONTENT_URI =
Uri.parse(“content://com.paad.skeletondatabaseprovider/elements”) ;
// Crée les constantes utilisées pour différencier les requêtes
private static final int ALLROWS = 1;
private static final int SINGLE_ROW = 2;
private static final UriMatcher uriMatcher;
// Remplit l’objet UriMatcher, où une URI se terminant par
// ’elements’ correspondra à une requête de tous les éléments et
// ’elements/[rowID]’ correspondra à une requête d’une seule ligne.
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements”, ALLROWS);
uriMatcher.addURI(“com.paad.skeletondatabaseprovider”,
“elements/#”, SINGLE_ROW);
}
// Le nom de la colonne index (clé) utilisée par les clauses where.
public static final String KEY_ID = “_id”;
// Le nom et l’indice de chaque colonne de la base.
// Ils devraient être évocateurs.
public static final String KEY_COLUMN_1_NAME = “KEY_COLUMN_1_NAME”;
// À faire : créer des champs publics pour chaque colonne de la table.
// Variable SQLiteOpenHelper
private MySQLiteOpenHelper myOpenHelper;
@Override
public boolean onCreate() {
// Construit la base de données sous-jacente.
// Reporte l’ouverture de la base tant que l’on n’en a pas
// besoin pour une requête ou une transaction.
myOpenHelper = new MySQLiteOpenHelper(getContext(),
MySQLiteOpenHelper.DATABASE_NAME, null,
MySQLiteOpenHelper.DATABASE_VERSION);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Ouverture de la base de données.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();;
// À remplacer par des instructions SQL valides si nécessaire.
String groupBy = null;
String having = null;
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 283
03/08/12 07:26
284

Android 4
// Si c’est une requête de ligne, on limite l’ensemble résultat à
// la ligne passée en paramètre.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
// Exécute la requête.
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, groupBy, having,
sortOrder);
// Renvoie le curseur résultat.
return cursor;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la suppression à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
//
//
//
if
Il faut indiquer une clause where pour renvoyer le nombre d’éléments
supprimés. Pour supprimer toutes les lignes et renvoyer une valeur,
passez le paramètre "1".
(selection == null)
selection = “1”;
// Effectue la suppression.
int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE,
selection, selectionArgs);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(uri, null);
// Renvoie le nombre d’éléments supprimés.
return deleteCount;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Pour ajouter des lignes vides la base en passant un objet
// ContentValues vide, vous devez utiliser l’astuce du paramètre de
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 284
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
285
// colonne null pour indiquer le nom de la colonne qui peut être mise
// à null.
String nullColumnHack = null;
// Insère les valeurs dans la table.
long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
nullColumnHack, values);
// Construit et renvoie l’URI de la ligne insérée.
if (id > -1) {
// Construit et renvoie l’URI de la ligne insérée.
Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(insertedId, null);
return insertedId;
} else
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Ouvre la base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la modification à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
// Effectue la modification.
int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,
values, selection, selectionArgs);
// Prévient les observateurs que l’ensemble des données a été modifié.
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
@Override
public String getType(Uri uri) {
// Renvoie une chaîne qui identifie le type MIME
// d’une URI de fournisseur de contenu.
switch (uriMatcher.match(uri)) {
case ALLROWS:
return “vnd.android.cursor.dir/vnd.paad.elemental”;
case SINGLE_ROW:
return “vnd.android.cursor.item/vnd.paad.elemental”;
default:
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 285
03/08/12 07:26
286

Android 4
throw new IllegalArgumentException(“URI non reconnue : “ + uri);
}
}
private static class MySQLiteOpenHelper extends SQLiteOpenHelper {
// [ ... Implémentation de SQLiteOpenHelper ... ]
}
}
Utiliser les fournisseurs de contenu
La section suivante présente la classe ContentResolver et son utilisation pour effectuer
des requêtes et des transactions sur un fournisseur de contenu.
Introduction aux résolveurs de contenu
Chaque application contient une instance de ContentResolver accessible par la
méthode getContentResolver.
ContentResolver cr = getContentResolver();
Lorsque les fournisseurs de contenu sont utilisés pour exposer des données, les
résolveurs de contenu sont les classes correspondantes permettant d’interroger et
d’effectuer des transactions sur ces fournisseurs. Tandis que les fournisseurs de
contenu offrent une abstraction par rapport aux données sous-jacentes, les résolveurs
de contenu fournissent une abstraction par rapport au fournisseur qui est interrogé
ou manipulé.
Le résolveur de contenu inclut les méthodes pour les requêtes et les transactions
correspondant à celles qui ont été définies dans vos fournisseurs. Il n’a pas besoin
de connaître l’implémentation des fournisseurs de contenu avec lesquels il interagit
– chaque méthode de requête ou transaction prend simplement en paramètre une
URI qui indique le fournisseur de contenu concerné.
Une URI de fournisseur de contenu est son autorité définie dans son manifeste et
généralement publiée sous la forme d’une constante statique de l’implémentation du
fournisseur.
Les fournisseurs de contenu acceptent en général deux formes d’URI, l’une pour les
requêtes sur toutes les données et l’autre pour les requêtes sur une seule ligne. Dans
cette dernière, un /<rowID> est ajouté à l’URI de base.
Effectuer des requêtes
Les requêtes sur un fournisseur de contenu sont très semblables à celles effectuées
sur une base de données. Les résultats sont renvoyés sous forme de curseurs de la
façon décrite plus haut dans ce chapitre.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 286
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
287
Vous pouvez extraire les valeurs d’un curseur en utilisant les mêmes techniques que
celles décrites dans la section "Extraire les résultats d’un curseur".
Utilisez la méthode query sur l’objet ContentResolver en lui passant les éléments
suivants :
■■
L’URI des données du fournisseur de contenu que vous voulez interroger.
■■
Une projection énumérant les colonnes que vous voulez inclure dans le résultat.
■■
Une clause where définissant les lignes à ramener. Vous pouvez inclure des
jokers ? qui seront remplacés par les valeurs passées par le paramètre des
arguments de sélection.
■■
Un tableau d’arguments de sélection qui remplaceront les ? de la clause where.
■■
Une chaîne décrivant l’ordre des lignes ramenées.
Le Listing 8.15 montre l’utilisation d’un résolveur de contenu pour interroger un
fournisseur de contenu.
Listing 8.15 : Interrogation d’un fournisseur de contenu à l’aide d’un résolveur de
contenu
// Récupérer le résolveur de contenu
ContentResolver cr = getContentResolver();
// Indique la projection des colonnes du résultat. Renvoie
// l’ensemble minimal de colonnes nécessaires aux besoins.
String[] result_columns = new String[] {
MyHoardContentProvider.KEY_ID,
MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN };
// Définition de la clause where qui limitera le nombre de lignes
// du résultat.
String where = MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN
+ “=” + 1;
// À remplacer par les instructions SQL valides en fonction des besoins.
String whereArgs[] = null;
String order = null;
// Renvoie les lignes indiquées.
Cursor resultCursor = cr.query(MyHoardContentProvider.CONTENT_URI,
result_columns, where, whereArgs, order);
Dans cet exemple, la requête utilise les constantes statiques fournies par la classe
MyHoardContentProvider, mais une application tierce aurait très bien pu exécuter la
même requête pourvu qu’elle connaisse l’URI du contenu et les noms des colonnes,
et qu’elle dispose des permissions appropriées.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 287
03/08/12 07:26
288

Android 4
La plupart des fournisseurs de contenu incluent également un raccourci permettant
d’accéder à une ligne précise en ajoutant son identifiant à l’URI. Vous pouvez utiliser
la méthode withAppendedId de la classe ContentUris pour simplifier la création de
ce raccourci, comme le montre le Listing 8.16.
Listing 8.16 : Accès à une ligne particulière dans un fournisseur de contenu
// Récupération du résolveur de contenu.
ContentResolver cr = getContentResolver();
// Indique la projection des colonnes du résultat. Renvoie
// l’ensemble minimal de colonnes nécessaires aux besoins.
String[] result_columns = new String[] {
MyHoardContentProvider.KEY_ID,
MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN };
// Ajoute un identifiant de ligne à l’URI pour accéder à une ligne
// particulière.
Uri rowAddress =
ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
rowId);
// Ces
String
String
String
variables sont null car on ne demande qu’une seule ligne.
where = null;
whereArgs[] = null;
order = null;
// Renvoie la ligne indiquée.
Cursor resultCursor = cr.query(rowAddress,
result_columns, where, whereArgs, order);
Pour extraire les valeurs d’un curseur, utilisez les mêmes techniques que celles que
nous avons décrites plus haut, en vous servant des méthodes moveTo<endroit> et
get<type> pour extraire les valeurs de la ligne et de la colonne.
Le Listing 8.17 étend le code du Listing 8.16 en parcourant un curseur pour afficher
le nom du plus gros magot.
Listing 8.17 : Extraction les valeurs du curseur d’un fournisseur de contenu
loat largestHoard = 0f;
String hoardName = “Pas de magot”;
// Trouve les indices des colonnes utilisées.
int GOLD_HOARDED_COLUMN_INDEX = resultCursor.getColumnIndexOrThrow(
MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN);
int HOARD_NAME_COLUMN_INDEX = resultCursor.getColumnIndexOrThrow(
MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN);
// Parcourt les lignes du curseur.
// Le curseur est placé avant la première ligne lorsqu’il est initialisé.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 288
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
289
// On peut donc simplement vérifier qu’il existe une ligne "suivante".
// Si le curseur est vide, ce test renverra false.
while (resultCursor.moveToNext()) {
float hoard = resultCursor.getFloat(GOLD_HOARDED_COLUMN_INDEX);
if (hoard > largestHoard) {
largestHoard = hoard;
hoardName = resultCursor.getString(HOARD_NAME_COLUMN_INDEX);
}
}
// Ferme le curseur lorsque l’on n’en a plus besoin.
resultCursor.close();
Lorsque l’on a terminé d’utiliser le curseur, il est important de le fermer pour éviter
les fuites mémoire et pour réduire les ressources consommées par l’application.
Vous verrez d’autres exemples plus loin dans ce chapitre lorsque nous présenterons
les fournisseurs de contenu natifs Android.
Attention
Les requêtes sur les bases de données peuvent durer un certain temps. Par défaut, le
résolveur de contenu exécutera les requêtes – et les autres transactions – dans le thread
principal de l’application.
Pour garantir que votre application restera réactive, vous devez exécuter toutes les
requêtes de façon asynchrone, comme nous l’expliquons dans la section qui suit.
Faire des requêtes asynchrones avec un chargeur de curseur
Les opérations sur les bases de données pouvant durer un certain temps, il est particulièrement important que les requêtes sur les bases de données et les fournisseurs
de contenu ne s’exécutent pas dans le thread principal de l’application.
Gérer les curseurs pour qu’ils se synchronisent correctement avec le thread de
l’interface utilisateur tout en s’assurant que les requêtes aient lieu en arrière-plan
peut être une tâche assez compliquée. Pour la simplifier, Android 3.0 (API level
11) a introduit la classe Loader, qui permet de définir des chargeurs. Ceux-ci sont
désormais également disponibles dans la bibliothèque support, ce qui autorise donc
leur utilisation sur toutes les anciennes plateformes Android jusqu’à la version 1.6.
Introduction aux chargeurs
Les chargeurs sont disponibles pour chaque activité et fragment via la classe
LoaderManager. Ils sont conçus pour charger les données de façon asynchrone et
pour surveiller les modifications de la source de données sous-jacente.
Bien que les chargeurs puissent être implémentés pour charger n’importe quelle sorte
de données à partir de n’importe quelle source, la classe CursorLoader mérite une
attention spéciale. Un chargeur de curseur permet en effet d’effectuer des requêtes
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 289
03/08/12 07:26
290

Android 4
asynchrones sur les fournisseurs de contenu et renvoie un curseur et les notifications
de chaque mise à jour du fournisseur sous-jacent.
Info
Pour que le code reste concis, tous les exemples de ce chapitre n’utilisent pas un chargeur
de curseur pour effectuer des requêtes à un fournisseur de contenu. Pour vos applications, nous vous recommandons toutefois de toujours utiliser un chargeur de curseur
pour gérer les curseurs dans vos activités et vos fragments.
Utiliser un chargeur de curseur
Un chargeur de curseur gère toutes les tâches nécessaires à l’utilisation d’un curseur
dans une activité ou un fragment, ce qui rend obsolètes les méthodes managedQuery
et startManagingCursor d’Activity.
Ces tâches incluent notamment la gestion du cycle de vie des curseurs pour garantir
qu’ils seront fermés lorsque l’activité sera terminée.
Les chargeurs de curseurs surveillent également les modifications du contenu sousjacent : vous n’avez donc plus besoin d’implémenter vos propres observateurs de
contenu.
Implémenter les fonctions de rappel d’un chargeur de curseur
Pour utiliser un chargeur de curseur, créez une nouvelle implémentation de
LoaderManager.LoaderCallbacks. Les fonctions de rappel d’un chargeur étant implémentées au moyen de méthodes génériques, vous devez préciser le type explicite qui
sera chargé – ici, le type Cursor – lorsque vous implémentez votre propre fonction
de rappel :
LoaderManager.LoaderCallbacks<Cursor> loaderCallback =
new LoaderManager.LoaderCallbacks<Cursor>() {
Si vous n’avez besoin que d’une seule implémentation de chargeur dans votre activité
ou votre fragment, la démarche classique consiste à implémenter cette interface dans
ce composant.
Les fonctions de rappel d’un chargeur consistent en trois gestionnaires :
■■ OnCreateLoader.
Appelé lorsque le chargeur est initialisé, ce gestionnaire crée
et renvoie un nouveau chargeur de curseur. Les paramètres du constructeur
de CursorLoader sont les répliques de ceux dont on a besoin pour exécuter
une requête en utilisant le résolveur de contenu car, lorsque ce gestionnaire est
exécuté, ces paramètres servent à exécuter une requête.
■■ OnLoadFinished.
Lorsque le LoaderManager a terminé la requête asynchrone,
ce gestionnaire est appelé avec le curseur résultat en paramètre. On utilise ce
curseur pour mettre à jour les adaptateurs et les autres éléments de l’interface
utilisateur.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 290
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
291
■■ OnLoaderReset.
Appelé lorsque le LoaderManager réinitialise le chargeur de
curseur. Dans ce gestionnaire, vous devriez libérer toutes les références aux
données renvoyées par la requête et réinitialiser l’interface utilisateur en conséquence. Le curseur sera fermé par le LoaderManager : il ne faut donc pas essayer
de le fermer manuellement.
Attention
onLoadFinished et onLoaderReset ne sont pas synchronisées avec le thread de l’interface
utilisateur. Si vous voulez modifier directement les éléments de l’interface, vous devrez
d’abord vous synchroniser avec le thread de l’interface utilisateur à l’aide d’un gestionnaire ou d’un mécanisme similaire. La synchronisation avec le thread de l’interface
utilisateur sera étudiée en détail au Chapitre 9.
Le Listing 8.18 est un squelette d’implémentation des fonctions de rappel d’un
chargeur de curseur.
Listing 8.18 : Implémentation des fonctions de rappel d’un chargeur
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// Construit la nouvelle requête sous le forme d’un chargeur.
// Le paramètre id permet de construire et renvoyer des chargeurs
// différents..
String[] projection = null;
String where = null;
String[] whereArgs = null;
String sortOrder = null;
// URI de la requête.
Uri queryUri = MyContentProvider.CONTENT_URI;
// Crée le chargeur de curseur.
return new CursorLoader(DatabaseSkeletonActivity.this, queryUri,
projection, where, whereArgs, sortOrder);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Remplace le curseur résultat affiché par le CursorAdapter par
// le nouvel ensemble résultat.
adapter.swapCursor(cursor);
// Ce gestionnaire n’étant pas synchronisé avec le thread de
// l’interface utilisateur, vous devez le synchroniser avant de
// modifier directement les éléments de l’interface.
}
public void onLoaderReset(Loader<Cursor> loader) {
// Supprime le curseur résultat existant du ListAdapter.
adapter.swapCursor(null);
// Ce gestionnaire n’étant pas synchronisé avec le thread de
// l’interface utilisateur, vous devez le synchroniser avant de
// modifier directement les éléments de l’interface.
}
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 291
03/08/12 07:26
292

Android 4
Initialiser et relancer le chargeur de curseur
Chaque activité ou fragment donne l’accès à son LoadManager via un appel à
getLoaderManager :
LoaderManager loaderManager = getLoaderManager();
Pour initialiser un nouveau chargeur, appelez la méthode initLoader du LoaderManager,
en lui passant en paramètre une référence à votre implémentation de LoaderCallbacks,
un Bundle de paramètres facultatif et un identifiant de chargeur :
Bundle args = null;
loaderManager.initLoader(LOADER_ID, args, myLoaderCallbacks);
Cette initialisation a généralement lieu dans la méthode onCreate de l’activité hôte
(ou dans la méthode onActivityCreated si vous utilisez un fragment).
Dans la plupart des cas, ce sera suffisant : le LoaderManager gérera le cycle de vie de
tous les chargeurs que vous initialisez, ainsi que les requêtes et les curseurs sous-jacents.
Il prendra également en charge les modifications dans les résultats des requêtes.
Lorsqu’un chargeur a été créé, des appels répétés à initLoader renverront simplement
le chargeur existant. Si vous voulez le supprimer et en recréer un autre, utilisez la
méthode restartLoader :
loaderManager.restartLoader(LOADER_ID, args, myLoaderCallbacks);
Ceci est généralement nécessaire lorsque les paramètres de la requête changent – dans
le cas de recherches ou de modifications de l’ordre du tri, notamment.
Ajouter, mettre à jour et supprimer du contenu
Pour effectuer des transactions sur des fournisseurs de contenu, utilisez les méthodes
delete, update et insert sur l’objet ContentResolver. Comme les requêtes, sauf si
elles sont déplacées dans un thread de travail, les transactions sur un fournisseur de
contenu s’exécuteront dans le thread principal de l’application.
Info
Les opérations sur les bases de données pouvant durer un certain temps, il est important
d’exécuter chaque transaction de façon asynchrone.
Insertions
Le résolveur de contenu propose deux méthodes pour insérer de nouveaux enregistrements dans un fournisseur de contenu, insert et bulkInsert. Toutes les deux prennent
en paramètre l’URI du fournisseur de contenu dans lequel vous insérez l’élément. La
première prend un objet ContentValues en entrée et la seconde, un tableau.
La méthode insert renvoie une URI vers l’enregistrement inséré alors que bulkInsert
renvoie le nombre de lignes ajoutées.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 292
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
293
Le Listing 8.19 montre l’usage des méthodes insert et bulkInsert.
Listing 8.19 : Insertion de nouvelles lignes dans un fournisseur de contenu
// Récupère le résolveur de contenu.
ContentResolver cr = getContentResolver();
// Crée une nouvelle ligne.
ContentValues newValues = new ContentValues();
// Affecte des valeurs à chaque ligne.
newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
hoardName);
newValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN,
hoardValue);
newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
hoardAccessible);
[ ... Répéter pour chaque paire colonne/valeur ... ]
// Inseère la ligne dans la table
Uri myRowUri = cr.insert(MyHoardContentProvider.CONTENT_URI,
newValues);
Suppressions
Pour supprimer un enregistrement, appelez delete sur le résolveur de contenu en
passant l’URI de la ligne à supprimer. Vous pouvez également indiquer une clause
where pour supprimer plusieurs lignes (voir le Listing 8.20).
Listing 8.20 : Suppression des enregistrements d’un fournisseur de contenu
// Précise la clause where qui détermine la ou les lignes à supprimer.
// Indique les paramètres where en fonction des besoins.
String where = MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN +
“=0”;
String whereArgs[] = null;
// Récupére le résolveur de contenu.
ContentResolver cr = getContentResolver();
// Supprime les lignes correspondantes
int deletedRowCount =
cr.delete(MyHoardContentProvider.CONTENT_URI, where, whereArgs);
Mises à jour
Les mises à jour des lignes d’un fournisseur de contenu sont effectuées par la méthode
update du résolveur de contenu. Cette méthode reçoit l’URI du fournisseur de contenu
cible, un objet ContentValues contenant les valeurs des colonnes à mettre à jour et
une clause where qui indique quelles lignes mettre à jour.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 293
03/08/12 07:26
294

Android 4
Lorsque la mise à jour est effectuée, chaque ligne concernée par la clause where est
mise à jour par les ContentValues, et le nombre de mises à jour réussies est renvoyé.
Vous pouvez aussi choisir de modifier une ligne spécifique en indiquant son URI
unique, comme dans le Listing 8.21.
Listing 8.21 : Mise à jour d’un enregistrement dans un fournisseur de contenu
// Crée le contenu modifié de la ligne, en affectant des valeurs à
// chaque ligne.
ContentValues updatedValues = new ContentValues();
updatedValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN,
newHoardValue);
// [ ... Répéter pour chaque colonne à modifier ... ]
// Crée une URI pour désigner une ligne précise.
Uri rowURI =
ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
hoardId);
// On indique une ligne spécifique : il n’y a donc pas besoin de clause
// de sélection.
String where = null;
String whereArgs[] = null;
// Récupère le résolveur de contenu.
ContentResolver cr = getContentResolver();
// Modifie la ligne indiquée.
int updatedRowCount =
cr.update(rowURI, updatedValues, where, whereArgs);
Accéder à des fichiers stockés dans des fournisseurs de contenu
Les fournisseurs de contenu représentent les gros fichiers sous forme d’URI qualifiées
et non comme des fichiers binaires bruts (blobs). Cependant, cette représentation est
cachée lorsque l’on utilise le résolveur de contenu.
Pour insérer un fichier dans un fournisseur de contenu ou accéder à un fichier existant,
utilisez respectivement les méthodes openOutputStream et openInputStream du
résolveur de contenu en leur passant l’URI de la ligne du fournisseur de contenu qui
contient le fichier concerné. Le fournisseur interprétera votre requête et renverra un
flux en écriture ou en lecture vers le fichier demandé, comme le montre le Listing 8.22.
Listing 8.22 : Lecture et écriture des fichiers à partir ou dans un fournisseur de
contenu
public void addNewHoardWithImage(String hoardName, float hoardValue,
boolean hoardAccessible, Bitmap bitmap) {
// Crée une ligne de valeurs à insérer.
ContentValues newValues = new ContentValues();
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 294
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
295
// Affecte des valeurs à chaque ligne.
newValues.put(MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
hoardName);
newValues.put(MyHoardContentProvider.KEY_GOLD_HOARDED_COLUMN,
hoardValue);
newValues.put( MyHoardContentProvider.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN,
hoardAccessible);
// Récupère le résolveur de contenu.
ContentResolver cr = getContentResolver();
// Insère la ligne dans la table.
Uri myRowUri =
cr.insert(MyHoardContentProvider.CONTENT_URI, newValues);
try {
// Ouvre un flux en écriture en utilisant l’URI de la nouvelle ligne.
OutputStream outStream = cr.openOutputStream(myRowUri);
// Compresse le bitmap et le sauvegarde dans le fournisseur.
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
}
catch (FileNotFoundException e) {
Log.d(TAG, “Aucun fichier trouvé pour cet enregistrement.”);
}
}
public Bitmap getHoardImage(long rowId) {
Uri myRowUri =
ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,
rowId);
try {
// Ouvre un flux en lecture en utilisant l’URI de la nouvelle ligne.
InputStream inStream = getContentResolver().openInputStream(myRowUri);
// Copie du bitmap.
Bitmap bitmap = BitmapFactory.decodeStream(inStream);
return bitmap;
}
catch (FileNotFoundException e) {
Log.d(TAG, “Aucun fichier trouvé pour cet enregistrement.”);
}
return null;
}
Création d’une base de données et d’un fournisseur de contenu pour
la liste de tâches
Au Chapitre 4, vous avez créé une application pour gérer une liste de tâches. Nous
allons ici créer une base de données et un fournisseur de contenu pour sauvegarder
chaque élément de la liste.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 295
03/08/12 07:26
296

Android 4
1. Commencez par créer une classe ToDoContentProvider qui étend
ContentProvider. Elle servira à héberger la base de données en utilisant un
SQLiteOpenHelper et à gérer les interactions de la base. Ajoutez-lui des ébauches
pour les méthodes onCreate, query, update, insert, delete et getType, ainsi
qu’un squelette d’implémentation d’un SQLiteOpenHelper :
package com.paad.todolist;
import
import
import
import
import
import
import
import
import
import
import
import
import
android.content.ContentProvider;
android.content.ContentUris;
android.content.ContentValues;
android.content.Context;
android.content.UriMatcher;
android.database.Cursor;
android.database.sqlite.SQLiteDatabase;
android.database.sqlite.SQLiteQueryBuilder;
android.database.sqlite.SQLiteDatabase.CursorFactory;
android.database.sqlite.SQLiteOpenHelper;
android.net.Uri;
android.text.TextUtils;
android.util.Log;
public class ToDoContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public String getType(Uri url) {
return null;
}
@Override
public Cursor query(Uri url, String[] projection, String selection,
String[] selectionArgs, String sort) {
return null;
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
return null;
}
@Override
public int delete(Uri url, String where, String[] whereArgs) {
return 0;
}
@Override
public int update(Uri url, ContentValues values,
String where, String[]wArgs) {
return 0;
}
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 296
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
297
private static class MySQLiteOpenHelper extends SQLiteOpenHelper {
public MySQLiteOpenHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version);
}
// Appelée lorsque aucune base n’existe sur le disque et que la classe
// helper a besoin d’en créer une nouvelle.
@Override
public void onCreate(SQLiteDatabase db) {
// À faire : créer les tables de la base.
}
// Appelée s’il y a un conflit de version de la base, ce qui signifie
// que la version de la base de données sur le disque doit être mise à
// jour avec la version courante.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// À faire : mettre à jour la version de la base.
}
}
}
2. Publiez l’URI de ce fournisseur. Cette URI servira à accéder à ce fournisseur
de contenu depuis d’autres composants d’application via le résolveur de contenu.
public static final Uri CONTENT_URI =
Uri.parse(“content://com.paad.todoprovider/todoitems”);
3. Créez des variables statiques publiques qui définissent les noms des colonnes.
Elles serviront à l’objet MySQLiteOpenHelper pour créer la base et aux autres
composants d’application pour extraire des valeurs de vos requêtes.
public static final String KEY_ID = “_id”;
public static final String KEY_TASK = “task”;
public static final String KEY_CREATION_DATE = “creation_date”;
4. Dans MysSQLiteOpenHelper, créez des variables pour stocker le nom et la version
de la base de données ainsi que le nom de la table de la liste de tâches.
private static final String DATABASE_NAME = “todoDatabase.db”;
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_TABLE = “todoItemTable”;
5. Toujours dans MysSQLiteOpenHelper , récrivez les méthodes onCreate et
onUpgrade pour traiter la création de la base de données en utilisant les colonnes
de l’étape 3 et des instructions de mise à jour standard.
// Instruction SQL pour créer une base de données. .
private static final String DATABASE_CREATE = “create table “ +
DATABASE_TABLE + “ (“ + KEY_ID +
“ integer primary key autoincrement, “ +
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 297
03/08/12 07:26
298

Android 4
KEY_TASK + “ text not null, “ +
KEY_CREATION_DATE + “long);”;
// Appelée lorsque aucune base n’existe sur le disque et que la classe
// helper a besoin d’en créer une nouvelle.
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
// Appelée s’il y a un conflit de version de la base, ce qui signifie
// que la version de la base de données sur le disque doit être mise à
// jour avec la version courante.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Enregistre dans le journal le changement de version.
Log.w(“TaskDBAdapter”, “Le passage de la version “ +
oldVersion + “ à la version “ +
newVersion + “, détruira les anciennes données.”);
// Mise à jour de la base existante pour se conformer à la nouvelle version.
// On peut gérer plusieurs anciennes versions en comparant les valeurs de
// oldVersion et newVersion
// Le cas le plus simple consiste à supprimer l’ancienne table et à en créer
// une nouvelle.
db.execSQL(“DROP TABLE IF IT EXISTS “ + DATABASE_TABLE);
// Création d’une nouvelle table.
onCreate(db);
}
6. Revenez à ToDoContentProvider et ajoutez-lui une variable privée pour stocker
une instance de la classe MySQLiteOpenHelper que vous créerez dans le gestionnaire onCreate.
private MySQLiteOpenHelper myOpenHelper;
@Override
public boolean onCreate() {
// Construit la base de données sous-jacente.
// Reporte l’ouverture de la base tant que l’on n’en a pas besoin pour
// une requête ou une transaction.
myOpenHelper = new MySQLiteOpenHelper(getContext(),
MySQLiteOpenHelper.DATABASE_NAME, null,
MySQLiteOpenHelper.DATABASE_VERSION);
return true;
}
7. Toujours dans le fournisseur de contenu, créez un nouvel objet UriMatcher pour
permettre à votre fournisseur de contenu de faire la différence entre une requête
sur la table entière et une requête sur une ligne particulière. Utilisez-le dans le
gestionnaire getType pour renvoyer le type MIME correct en fonction du type
de la requête.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 298
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
299
private static final int ALLROWS = 1;
private static final int SINGLE_ROW = 2;
private static final UriMatcher uriMatcher;
// Remplit l’objet UriMatcher, où une URI se terminant par ’todoitems’
// correspondra à une requête de toutes les tâches et où ’todoitems/[rowID]’
// représente une seule ligne .
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(“com.paad.todoprovider”, “todoitems”, ALLROWS);
uriMatcher.addURI(“com.paad.todoprovider”, “todoitems/#”, SINGLE_ROW);
}
@Override
public String getType(Uri uri) {
// Renvoie une chaîne identifiant le type MIME de l’URI
// d’un fournisseur de contenu.
switch (uriMatcher.match(uri)) {
case ALLROWS: return “vnd.android.cursor.dir/vnd.paad.todos”;
case SINGLE_ROW: return “vnd.android.cursor.item/vnd.paad.todos”;
default: throw new IllegalArgumentException(“URI non supportée : “ + uri);
}
}
8. Implémentez l’ébauche de la méthode query. Commencez par demander une
instance de la base de données avant de construire une requête utilisant les
paramètres passés. Dans cette instance, vous ne devez appliquer les mêmes
paramètres de requête qu’à la base de données sous-jacente – ne modifiez la
requête que pour prendre en compte la possibilité qu’une URI désigne une seule
ligne.
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Ouvre une base de données en lecture seule.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Remplacer par des instructions SQL valides si nécessaire.
String groupBy = null;
String having = null;
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
// Si c’est une requête de ligne, limite l’ensemble résultat à la ligne
// passée en paramètre.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
queryBuilder.appendWhere(KEY_ID + “=” + rowID);
default: break;
}
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 299
03/08/12 07:26
300

Android 4
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, groupBy, having, sortOrder);
return cursor;
}
9. Implémentez les méthodes delete, insert et update selon la même approche –
passez les paramètres reçus tout en traitant le cas particulier des URI de ligne
simple.
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Ouvre une base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la suppression à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
//
//
//
if
Pour renvoyer le nombre d’éléments supprimés, il faut indiquer une
clause where. Pour supprimer toutes les lignes et renvoyer une valeur,
on passe "1".
(selection == null)
selection = “1”;
// Exécute la suppression.
int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE, selection,
selectionArgs);
// Prévient les observateurs de la modification de l’ensemble des données.
getContext().getContentResolver().notifyChange(uri, null);
return deleteCount;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Ouvre une base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Pour ajouter des lignes vides à la base de données en passant un
// ContentValues vide, il faut utiliser le paramètre de colonne null pour
// indiquer le nom de la colonne qui peut être initialisée à null.
String nullColumnHack = null;
// Insère les valeurs dans la table.
long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,
nullColumnHack, values);
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 300
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
301
if (id > -1) {
// Construit et renvoie l’URI vers la ligne venant d’être insérée.
Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id);
// Prévient les observateurs de la modification de l’ensemble des données.
getContext().getContentResolver().notifyChange(insertedId, null);
return insertedId;
} else
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Ouvre une base en lecture/écriture pour la transaction.
SQLiteDatabase db = myOpenHelper.getWritableDatabase();
// Si c’est une URI de ligne, limite la mise à jour à la ligne indiquée.
switch (uriMatcher.match(uri)) {
case SINGLE_ROW :
String rowID = uri.getPathSegments().get(1);
selection = KEY_ID + “=” + rowID
+ (!TextUtils.isEmpty(selection) ?
“ AND (“ + selection + ’)’ : “”);
default: break;
}
// Effectue la mise à jour.
int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,
values, selection, selectionArgs);
// Prévient les observateurs de la modification de l’ensemble des données.
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
10. Ceci termine la classe du fournisseur de contenu. Ajoutez-la au manifeste de
votre application, en indiquant l’URI qui servira d’autorité.
<provider android:name=”.ToDoContentProvider”
android:authorities=”com.paad.todoprovider”/>
11. Revenez à l’activité ToDoList et modifiez-la pour rendre persistant le tableau de la
liste de tâches. Commencez par modifier l’activité pour qu’elle implémente Loade
rManager.LoaderCallbacks<Cursor>, puis ajoutez les ébauches de méthodes
associées.
public class ToDoList extends Activity implements
NewItemFragment.OnNewItemAddedListener, LoaderManager.
LoaderCallbacks<Cursor> {
// [... Code existant de l’activité ToDoList ...]
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 301
03/08/12 07:26
302

Android 4
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return null;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
}
public void onLoaderReset(Loader<Cursor> loader) {
}
}
12. Complétez le gestionnaire onCreateLoader pour qu’il construise et renvoie un
objet CursorLoader qui demande tous les éléments de ToDoListContentProvider.
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader loader = new CursorLoader(this,
ToDoContentProvider.CONTENT_URI, null, null, null, null);
return loader;
}
13. Lorsque la requête de loader se termine, le curseur résultat est renvoyé au
gestionnaire onLoadFinished. Modifiez ce dernier pour qu’il parcoure le curseur
et remplisse l’ArrayAdapter de la liste de tâches en conséquence.
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int keyTaskIndex = cursor.getColumnIndexOrThrow(ToDoContentProvider.
➥ KEY_TASK);
todoItems.clear();
while (cursor.moveToNext()) {
ToDoItem newItem = new ToDoItem(cursor.getString(keyTaskIndex));
todoItems.add(newItem);
}
aa.notifyDataSetChanged();
}
14. Modifiez le gestionnaire onCreate pour lancer le chargeur à la création de
l’activité et le gestionnaire onResume pour relancer le chargeur lorsque l’activité
est redémarrée.
public void onCreate(Bundle savedInstanceState) {
// [... Code existant de onCreate ...]
getLoaderManager().initLoader(0, null, this);
}
@Override
protected void onResume() {
super.onResume();
getLoaderManager().restartLoader(0, null, this);
}
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 302
03/08/12 07:26
Chapitre 8
Bases de données et fournisseurs de contenu
303
15. La dernière étape consiste à modifier le comportement du gestionnaire
onNewItemAdded. Au lieu d’ajouter directement l’élément à la liste de tâches,
utilisez le résolveur de contenu pour l’ajouter au fournisseur de contenu.
public void onNewItemAdded(String newItem) {
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(ToDoContentProvider.KEY_TASK, newItem);
cr.insert(ToDoContentProvider.CONTENT_URI, values);
getLoaderManager().restartLoader(0, null, this);
}
Info
Tous les extraits de code de cet exemple font partie du projet Todo List Chapitre 8,
disponible sur le site consacré à cet ouvrage.
Vous avez créé une base de données dans laquelle sauvegarder vos tâches. Une
meilleure approche que la copie des lignes du curseur dans une ArrayList consiste
à utiliser un SimpleCursorAdapter. Nous le ferons plus loin dans ce chapitre, dans
la section "Créer un fournisseur de tremblements de terre avec la fonctionnalité de
recherche".
Pour rendre cette application de liste de tâches plus utile, vous pourriez ajouter la
possibilité de supprimer, modifier les tâches de la liste, modifier l’ordre du tri et vous
pourriez stocker des informations supplémentaires.
Ajouter une fonctionnalité de recherche à vos applications
Permettre d’effectuer des recherches dans le contenu de vos applications est un
moyen simple et puissant d’améliorer l’implication de l’utilisateur et la visibilité d’une
application. Sur les terminaux mobiles où la rapidité est le critère prédominant, la
recherche offre un mécanisme permettant aux utilisateurs de trouver rapidement les
informations dont ils ont besoin.
Android fournit un framework qui simplifie la recherche d’informations dans les
fournisseurs de contenu en ajoutant à vos activités une fonction de recherche et en
intégrant les résultats de cette recherche à l’écran d’accueil.
Jusqu’à Android 3.0 (API level 11), la plupart des terminaux Android disposaient d’une
touche recherche physique. Sur les versions plus récentes, celle-ci a été remplacée
par des widgets qui sont généralement placés dans la barre d’action de l’application.
En implémentant la recherche dans votre application, vous pouvez présenter une
fonctionnalité de recherche spécifique à votre application à chaque appui sur le
bouton ou le widget de recherche.
© 2012 Pearson France – Android 4 – Reto Meier
Livre ANDROID4.indb 303
03/08/12 07:26

Documents pareils