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