• Aucun résultat trouvé

Bases de données et fournisseurs de contenu

N/A
N/A
Protected

Academic year: 2022

Partager "Bases de données et fournisseurs de contenu"

Copied!
41
0
0

Texte intégral

(1)

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.

(2)

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 struc- turé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.

(3)

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 ContentValues

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.

(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

. Déplace le curseur sur la première ligne du résultat.

■ moveToNext

. Déplace le curseur sur la ligne suivante.

■ moveToPrevious

. Déplace le curseur sur la ligne précédente.

■ getCount

. 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.

(5)

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

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.

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, respecti- vement, 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;

(6)

// 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, respecti-

vement, une instance en écriture ou en lecture de la base sous-jacente.

(7)

En coulisses, si la base n’existe pas, l’objet

SQLiteOpenHelper

exécute son gestion- naire 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.

(8)

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 };

(9)

// 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.

(10)

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 encap- sulent 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érerdenouvelleslignes

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.

(11)

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àjourdeslignes

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... ]

(12)

// 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);

Supprimerdeslignes

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.

(13)

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;

(14)

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 Lis- ting 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(),

(15)

MySQLiteOpenHelper.DATABASE_NAME, null, MySQLiteOpenHelper.DATABASE_VERSION);

return true;

}

Info

Lorsque votre application est lancée, le gestionnaire onCreate de chacun de ses fournis- seurs 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) {

(16)

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) {

(17)

// 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;

}

// 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".

if (selection == null) selection = “1”;

(18)

// 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.

(19)

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’uti- lisons 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 nom- mé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’utili- sateur, 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 four- nisseur 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);

(20)

// 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 uti- lise 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 android.content.ContentProvider;

import android.content.ContentUris;

import android.content.ContentValues;

import android.content.Context;

import android.content.UriMatcher;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteDatabase.CursorFactory;

import android.database.sqlite.SQLiteOpenHelper;

import android.database.sqlite.SQLiteQueryBuilder;

import android.net.Uri;

import android.text.TextUtils;

import android.util.Log;

(21)

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);

(22)

// 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;

}

// 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".

if (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

(23)

// 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:

(24)

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 b

ase

.

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.

(25)

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.

(26)

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 variables sont null car on ne demande qu’une seule ligne.

String where = null;

String whereArgs[] = null;

String 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é.

(27)

// 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 parti- culiè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.

Introductionauxchargeurs

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

(28)

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 applica- tions, nous vous recommandons toutefois de toujours utiliser un chargeur de curseur pour gérer les curseurs dans vos activités et vos fragments.

Utiliserunchargeurdecurseur

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 sous- jacent : 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.

(29)

■ 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 gestion- naire 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.

}

Références

Documents relatifs

Vous pouvez maintenant créer une base de données personnelle, dans le domaine de votre choix, mais qui satisfait aux conditions suivantes : au moins trois tables distinctes, avec

Vous trouverez l''''intégralité des informations et des données dans la documentation pour l''utilisateur

‚ Par exemple : le nom d’un livre et de ses auteurs ñ Inutile de faire plusieurs requêtes. ñ Sélection sur

‚ Par exemple : le nom d’un livre et de ses auteurs ñ Inutile de faire plusieurs requêtes. ñ Sélection sur

Pour chaque projet, on mémorise le numéro de projet, le nom du projet, la date de début et la date de fin ainsi que le chef de projet (qui est toujours un employé de l’entreprise).

h) Nom et prime des joueurs sponsorisés par Peugeot entre 1985 et 1990. i) Nom et année de naissance des joueurs ayant participé au tournoi de Rolland Garros de 1989. j) Nom

Une base de données est un ensemble structuré de données enregistrées avec le minimum de redondance pour satisfaire simultanément plusieurs utilisateurs de façon sélective en un

Cette requête retourne exactement les mêmes colonnes qu’il y a dans la base de données. Pour en savoir plus sur le sujet il est recommandé de lire l’article avantage et