Stockage de donnée Android - M-A-Akar/android_stockage_donnee GitHub Wiki
Une application qui ne peut garder trace des interactions avec l’utilisateur et conserver des données d’une session à une autre est une application sans vie, que l’utilisateur s’empressera souvent de désinstaller. Toute application doit pouvoir charger et enregistrer des données. Android offre plusieurs méthodes pour stocker les données d’une application. La solution choisie va dépendre des besoins : données privées ou publiques, un petit ensemble de données à préserver ou un large ensemble à préserver localement ou via le réseau. Ces méthodes utilisent soit des éléments propres à l’API Java ou ceux associés à l’API d’Android. Ces méthodes sont :
-
Persistance dans l'état d'une application On utilise pour cela la notion de « Bundle » et les différents cycles de l’activité pour sauvegarder l’information utile à l’aide du « bundle » et récupérer cette information dans un autre état de l’activité. On ne peut utiliser qu’un seul bundle, par ailleurs la donnée n’est pas persistante et n’est disponible que tant que l’application est utilisée.
-
Préférences partagées Un ensemble de paires : clé et valeur. Clé est un « String » et Valeur est un type primitif (« boolean », « String », etc.). Ces préférences sont gérées à travers un code Java ou bien via une activité. Les préférences sont adaptées pour des paires simples, mais dès qu’il est question de données plus complexes, il est préférable d’utiliser des fichiers.
-
Fichiers (création et sauvegarde) Android permet la création, la sauvegarde et, la lecture de fichiers à travers un média persistant (mémorisation et disponibilité).Les fichiers peuvent être de n’importe quel type (image, xml, etc.). Les fichiers peuvent être considérés pour une utilisation interne, donc locale à l’application, ou bien externe, donc partagée avec plusieurs applications.
-
Base de données relationnelle, SQLite Android offre aussi la possibilité d’utiliser toutes les propriétés d’une base de données relationnelle Android utilise pour cela une base de données basée sur « SQLite » (www.sqlite.org). Android stocke la base de données localement à l’application. Si l’on veut partager cette structure de données avec d’autres applications, il faudra utiliser dans ce cas, un gestionnaire de contenu (content provider) configuré à cet effet.
-
Stockage réseau Android permet de stocker des fichiers sur un serveur distant. Les données ne sont pas cryptées.
Android peut arrêter l’activité et la redémarrer quand il y a :
- Rotation de l’écran.
- Changement de langue.
- L’application est en arrière-plan et le système a besoin de ressources.
- Et quand vous cliquez le bouton « retour » (« back »).
Ce redémarrage peut provoquer la perte des changements apportés à votre activité.
Une solution consiste à préserver les données dans un bundle à travers la méthode onSaveInstanceState puis récupérer cette information par la suite à l’aide de la méthode onRestoreInstanceState (ou bien dans la méthode onCreate).
Cette solution n’est pas appropriée dans le cas d’un clic sur le bouton « retour ».Dans ce cas, l’application démarre à partir de zéro et les données préservées sont détruites Généralement, l’orientation est gérée par la création d’une vue appropriée. Mais supposons que vous n’ayez pas eu le temps de le faire! Nous pouvons fixer dans le fichier « AndroidManifest.xml » l’orientation supportée, comme suit :
La classe SharedPreferences fournit un ensemble de méthode permettant de persister et de récupérer des couples clé/valeur de type simple. Les données sont sauvegarder de manière permanente. C'est à dire que vous ne perdez pas les informations quand vous quittez l'application. Pour récupérer un objet SharedPreferences, vous disposez de deux méthodes:
- Context.getSharedPreferences(String name,int mode): est à utiliser si vous voulez disposer de plusieurs fichiers de préférences. Dans ce cas, vous devez spécifier en premier paramètre le nom du fichier dans lequel sont stockées les préférences de votre application.
- Context.getPreferences(int mode): s'utilise dans le cas où vous souhaitez n'avoir qu'un seul fichier de préférence pour votre application.
- Mode spécifie les droits de lecture/écriture, les valeurs possibles sont:
- MODE_PRIVATE
- MODE_WORLD_READABLE
- MODE_WORLD_WRITEABLE
Les valeurs enregistrées avec un SharedPreferences en MODE_PRIVATE ne pourront être manipulée que par les applications tournants avec le même « user ID » alors que si le SharedPreferences utilisait le MODE_WORLD_READABLE ou le MODE_WORLD_WRITEABLE, ses données pourraient être lues ou modifiées pour tout les applications.
Les lectures des préférences se font assez simplement: une fois récupérée l’instance de SharedPrefernces souhaitée,il suffit d’invoquer la méthode « get » correspondant au type de la valeur à chercher avec en paramètre sa clé et la valeur à renvoyer si elle n’est pas retrouvée ( valeur par défaut ).
L’exemple ci-dessous montre comment au démarrage de l’application on pourrait charger des éléments de paramétrage qui aurrait été précédemment sauvegardés:
/*
SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
boolean tvaReduite = settings.getBoolean("TVAReduite", false);
float prixBanane = settings.getFloat("prixBanane",6.5F);
int stockDisponible = settings.getInt("stockDisponible", 500);
String etiquette = settings.getString("etiquette", null);
//affectation des valeurs */
Dans l’exemple, le SharedPreferences chargé de stocker les données est privé, c’est celui dédié à l’activité. Si la méthode getPreferences était invoquée sur une autre activité, l’instance retournée serait différente et les valeurs ne seraient pas trouvées. C’est bien là la philosophie de cet objet ; son but est de mémoriser la configuration,les préférences, de l’activité.
L’écriture des préférences a la particularité de se faire de manière atomique grâce à l’objet SharedPreferences.Editor. Une instance de cet objet est retournée par la méthode edit() de la classe SharedPreferences. C’est sur cette instance que seront appelées les méthodes putBoolean, putFloat, putInt, putLong et putString accompagnées de la clé et de la valeur de la préférence. Ensuite, au moment de l’appel au commit(), les données positionnées par les appels aux méthodes « put » seront persistées toutes d’un seul bloc. Voici un exemple d’utilisation:
/*
SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.clear();
editor.putBoolean("TVAReduite", tvaReduite);
editor.putFloat("prixBanane",prixBanane);
editor.putInt("stockDisponible", stockDisponible);
editor.putString("etiquette", etiquette);
editor.commit();
*/
La méthode clear() a pour effet d’effacer tous les enregistrements présents à l’instant où le SharedPreferences.Editor a été obtenu. Elle aurait pu être aussi appelée juste avant le commit(), le résultat produit aurait été identique, l’instance de SharedPreferences ne contiendrait in fine que les quatre valeurs mémorisées par l’objet editor.
Les modifications apportées à un SharedPreferences peuvent être suivies par le biais de l’enregistrement d’un callback, grâce à la méthode registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener), la méthode unregister... sert à déréférencer ce callback.
Même si le système des préférences peut être employé pour sauvegarder tout type de données primitives, il a avant tout été pensé pour enregistrer la configuration des applications. Le package « android.preference » contient un ensemble de classes qui aident à la création d’interfaces graphiques permettant d’afficher et d’éditer des préférences.
Grâce à ce framework, la création des écrans de configuration peut se faire au travers d’un fichier xml. Jusqu’à présent, rien de nouveau car c’est le cas pour tous les écrans avec les fichiers de layout, mais l’avantage d’utiliser le framework de préférence réside dans la possibilité de définir le « data binding » également dans le fichier xml. Ainsi la synchronisation entre l’interface visuelle de configuration et le fichier de stockage des préférences est automatique.
Le fichier de préférence doit être positionné dans le répertoire « res/xml ». Par exemple, voici le contenu du fichier preferences.xml :
/*
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="tvaReduite"
android:title="TVA à taux réduit ?"
android:summary="5,5% ou 19,6 %" />
<EditTextPreference
android:key="etiquette"
android:title="Libellé de l’étiquette"
android:summary="Sera affiché sur l’étiquette" />
<RingtonePreference
android:key="sonnerie"
android:title="Sonnerie"
android:showDefault="true"
android:showSilent="true"
android:summary="Choisissez une sonnerie" />
</PreferenceScreen> */
Ce fichier déclare trois préférences dont les clés sont « tvaReduite », « étiquette » et « sonnerie ». Ensuite, il faut charger ce fichier, un peu comme un layout classique. Pour cela, il existe une activité spéciale, PreferenceActivity, qu’il s’agit d’étendre :
/* package org.florentgarin.android.persistence;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class PreferencesUIFrameworkActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
Dans, l’exemple, on a utilisé un composant particulier qui est le RingtonePreference grâce auquel on peut choisir une sonnerie parmi celles disponibles. Le framework permet aussi de regrouper, pour une meilleure ergonomie, certaines préférences au sein de PreferenceCategory et même aussi de les étaler sur plusieurs écrans. Une fois ces préférences enregistrées, pour y accéder, il faut recourir à l’objet PreferenceManager:
/* SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
//lecture des paramètres... */
Vous pourrez stocker des fichiers sur le support de stockage interne ou externe d’Android .
4.1.1 Les fichiers standards
Le stockage interne permet de stocker directement des fichiers dans la mémoire interne du téléphone. Par défaut, aucune autre application ne peut y accéder. Pas même l'utilisateur pour être exact. Il faut noter que lors ce que vous supprimez votre application, vous perdez par la même occasion toutes les données stockées dans la mémoire interne.
Pour créer et écrire dans un fichier, il suffit d'utiliser les méthodes suivantes:
- Context.openFileOutput(file, mode): ouverture d'un fichier avec son mode d'accès; retourne un FileOutputStream
- write() pour remplir le buffer que la méthode openFileOutput() nous a fourni.
- close() qui permet de fermer le fichier en s'assurant que tout ce qui a été écrit est bien sauvegardé.
Voici un exemple d'écriture sur un fichier interne.
/*
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close(); */
Les différents modes d'ouverture d'un fichier sont:
- MODE_PRIVATE: permet de créer un fichier ou de le remplacer s'il existe déjà et surtout rend le fichier privé à votre application.
- MODE_APPEND: possède le même principe de fonctionnement que MODE_PRIVATE mais offre la possibilité d'écrire à la fin du fichier plutôt que de l'écraser.
- MODE_WORLD_READABLE: Offre la possibilité à toutes les autres applications de lire ce fichier.
- MODE_WORLD_WRITEABLE: Offre la possibilité à toutes les autres applications d'éditer ce fichier.
4.1.2 Les fichiers statiques
Les fichiers statiques permettent de stocker des données dans un fichier à la compilation. Les fichiers statiques sont uniquement accessibles en lecture. C'est à dire que vous ne pourrez pas modifier le fichier. Pour les utiliser :
- Stockez votre fichier dans le repertoire res/raw/
- Ouvrez le fichier grâce à la méthode openRawResource() en passant l'identifiant de la ressource qui doit être de la forme R.raw.File_name
- Vous pouvez lire le fichier de la même manière que les fichiers standards via la méthode read(), puis vous devez utiliser la méthode close() quand la lecture est finie.
4.1.3 Les fichiers de cache
Si vous désirez mettre des données en cache plutôt que de les stocker de manière permanente, vous pouvez utiliser la méthode getCacheDir() pour ouvrir un fichier qui représente le répertoire de cache. Une fois cela effectué, vous pouvez créer un fichier dans ce répertoire qui sera donc en cache. Il faut noter que ces fichiers peuvent être supprimé par le système si celui-ci à besoin de place. Cependant, il ne faut pas compter sur le système pour nettoyer son cache. Vous devez le faire par vous même lorsque vous avez fini de l'utiliser. Il faut aussi noter que ces fichiers sont supprimés lorsque l'application est désinstallé.
4.1.4 Méthodes utiles pour le traitement de fichier
Les différentes méthodes utiles pour le traitement de fichier sont:
- getFileDir(), retourne le chemin absolue vers le repertoire de stockage interne.
- getDir(), récupère ou créer un répertoire dans votre espace de stockage interne.
- deleteFile(), permet de supprimer un fichier de l'espace interne.
- fileList(), retourne la liste des fichiers stockés en interne pour votre application.
Pour conclure sur le stockage interne, il faut bien garder à l'esprit que ce stockage est supprimé lorsque l'application est désinstallée. De plus, un grand nombre de téléphone n'ont pas un grand espace de stockage interne, il faut donc limiter la taille des fichiers stockés en interne. Nous allons maintenant étudier le stockage externe.
Tous les téléphones Android disposent d'un support de stockage externe que vous pouvez utiliser pour stocker des fichiers. Ceci peut être une carte SD amovible ou un stockage non-amovible au téléphone. Les fichiers sur un support externe sont accessible par tout le monde, notamment par l'utilisateur lorsqu'il branche son téléphone à son ordinateur via un cable USB. Il faut donc faire très attention avec ce type de fichier, puisque l'utilisateur peut les supprimer à n'importe quel moment.
4.2.1 Contrôle d'accessibilité du stockage externe
Pour contrôler l'accessibilité du stockage externe, vous devez utiliser la méthode getExternalStorageStage(). Vous trouverez un exemple ci-dessous qui vérifie si la carte SD est accessible.
/* boolean mExternalStorageAvailable = false;
boolean mExternalStorageWriteable = false;
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// We can read and write the media
mExternalStorageAvailable = mExternalStorageWriteable = true;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// We can only read the media
mExternalStorageAvailable = true;
mExternalStorageWriteable = false;
} else {
// Something else is wrong. It may be one of many other states, but all we need
// to know is we can neither read nor write
mExternalStorageAvailable = mExternalStorageWriteable = false;
}*/
4.2.2 Accéder à un fichier
Si vous utilisez l'API de niveau 8 qui est utilisable pour les téléphones sous Android 2.2, vous pouvez utiliser la méthode getExternalFilesDir() pour ouvrir un fichier qui représente le répertoire dans lequel vous devriez sauvegarder vos fichiers. Cet méthode prend un paramètre appelé type qui permet de stocker votre fichier dans le répertoire approprié. Des types possibles sont : DIRECTORY_MUSIC, DIRECTORY_PICTURES, ... Si vous désirez accéder à la racine du répertoire prévu pour votre application, il suffit de passer null comme paramètre. Si le répertoire n'existe pas, cette méthode le créera. En spécifiant le type de média que vous voulez stocker, vous permettez au système de partager vos médias avec les autres applications. Par exemple si vous stocker une image dans le dossier DIRECTORY_PICTURES, ces images seront accessibles dans la galerie de photo.
Si vous utilisez cette méthode, les données stockées sur la carte SD seront supprimées lorsque l'application est désinstallée.
Si vous utilisez l'API de niveau 7 ou inférieur, vous devez utiliser la méthode getExternalStorageDirectory() pour ouvrir un fichier qui représente le répertoire racine de la carte SD. Il est recommandé de stocker les données provenant de votre application dans le dossier suivant: : /Android/data/<package_name>/files avec package_name représentant le package de votre application écrit dans le style Java. Par exemple: "com.example.android.app« .
Si l'utilisateur utilise l'API 8, les données stockées dans ce dossier seront automatiquement supprimées à la désinstallation de l'application. Il faut cependant noter que sur les versions précédentes, les fichiers restent dans tous les cas sur la carte SD après la désinstallation.
4.2.3 Sauvegarder des fichiers partagés
Si vous désirez sauvegarder des fichiers qui ne soient pas supprimés à la désinstallation de l'application et ce quelque soit l'API utilisé, vous devez les stocker dans les répertoires publiques. Ces répertoires se trouvent à la racine de la carte SD. Dans les API de niveau 8 et supérieur, vous devez utiliser la méthode getExternalStoragePublicDorectory() en passant en paramètre le type de média stocké. Les types de média sont par exemple: DIRECTORY_MUSIC, DIRECTORY_PICTURES. Pour les API 7 et inférieur, vous devez utiliser les méthodes getExternalStorageDirectory() pour ouvrir un fichier représentant la racine de la carte SD. Puis stocker les données dans un des répertoires suivants:
- Music/
- Podcasts/
- Ringtones
- Alarms/
- Notifications/
- Pictures/
- Movies/
- Download/
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.
Pour créer et ouvrir une base de données, la meilleure solution consiste à créer une sous classe de SQLiteOpenHelper. Cette classe enveloppe tout ce qui est nécessaire à la création et à la mise à jour d’une base, selon vos spécifications et les besoins de votre application. Cette sous-classe aura besoin de trois méthodes :
- Un constructeur qui appelle celui de sa classe parente et qui prend en paramètre le Context (une Activity), le nom de la base de données, une éventuelle fabrique de curseur (le plus souvent, ce paramètre vaudra null) et un entier représentant la version du schéma de la base.
- onCreate(), à laquelle vous passerez l’objet SQLiteDatabase que vous devrez remplir avec les tables et les données initiales que vous souhaitez.
- onUpgrade(), à laquelle vous passerez un objet SQLiteDatabase ainsi que l’ancien et le nouveau numéro de version. Pour convertir une base d’un ancien schéma à un nouveau, l’approche la plus simple consiste à supprimer les anciennes tables et à en créer de nouvelles.
Pour accéder à une base de données à l’aide de la classe SQLiteOpenHelper, vous pouvez appeler les méthodes getReadableDatabase et getWritableDatabase et ainsi obtenir une instance de la base de données respectivement en lecture seule et en lecture/écriture.
Voici un cas d’exemple d’implémentation de SQLiteOpenHelper :
package com.test.SQLITE;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class HotOrNot {
public static final String KEY_ROWID="_id";
public static final String KEY_NAME="persons_name";
public static final String KEY_HOTNESS="persons_hotness";
private static final String DATABASE_NAME="HotOrNot";
private static final String DATABASE_TABLE="peopleTable";
private static final int DATABASE_VERSION=1;
private DBHelper ourHelper;
private final Context ourContext;
private SQLiteDatabase ourDatabase;
private static class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
// TODO Auto-generated constructor stub
}
@Override
public void onCreate(SQLiteDatabase db) {
// TODO Auto-generated method stub
db.execSQL("CREATE TABLE " + DATABASE_TABLE + " (" +
KEY_ROWID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
KEY_NAME + " TEXT NOT NULL, " +
KEY_HOTNESS + " TEXT NOT NULL);"
); }
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE);
onCreate(db); }
}
public HotOrNot(Context c){
ourContext=c;}
public HotOrNot open() throws SQLException {
ourHelper =new DBHelper(ourContext);
ourDatabase=ourHelper.getWritableDatabase();
return this;}
public void close(){
ourHelper.close();}
Lorsque l’on crée une base de données et une ou plusieurs tables, c’est généralement pour y placer des données. Pour ce faire, il existe principalement deux approches. Vous pouvez encore utiliser execSQL(), comme vous l’avez fait pour créer les tables. Cette méthode permet en effet d’exécuter n’importe quelle instruction SQL qui ne renvoie pas de résultat, ce qui est le cas d’INSERT, UPDATE, DELETE, etc. Vous pourriez donc utiliser ce code :
db.execSQL("INSERT INTO widgets (name, inventory)"+
"VALUES (’Sprocket’, 5)");
Une autre solution consiste à utiliser insert(), update() et delete() sur l’objet SQLite-Database. Ces méthodes utilisent des objets ContentValues qui implémentent une interface ressemblant à Map mais avec des méthodes supplémentaires pour prendre en compte les types de SQLite : outre get(), qui permet de récupérer une valeur par sa clé, vous disposez également de getAsInteger(), getAsString(), etc. La méthode insert() prend en paramètre le nom de la table, celui d’une colonne pour l’astuce de la colonne nulle et un objet ContentValues contenant les valeurs que vous voulez placer dans cette ligne. L’astuce de la colonne nulle est utilisée dans le cas où l’instance de ContentValues est vide – la colonne indiquée pour cette astuce recevra alors explicitement la valeur NULL dans l’instruction INSERT produite par insert().
ContentValues cv=new ContentValues();
cv.put(Constantes.TITRE, "Gravity, Death Star I");
cv.put(Constantes.VALEUR, SensorManager.GRAVITY_DEATH_STAR_I);
db.insert("constantes", getNullColumnHack(), cv);
La méthode update() prend en paramètre le nom de la table, un objet ContentValues contenant les colonnes et leurs nouvelles valeurs et, éventuellement, une clause WHERE et une liste de paramètres qui remplaceront les marqueurs présents dans celle-ci. update() n’autorisant que des valeurs fixes pour mettre à jour les colonnes, vous devrez utiliser execSQL() si vous souhaitez affecter des résultats calculés. La clause WHERE et la liste de paramètres fonctionnent comme les paramètres positionnels qui existent également dans d’autres API de SQL :
// remplacements est une instance de ContentValues
String[] params=new String[] {"snicklefritz"};
db.update("widgets", remplacements, "name=?", params);
La méthode delete() fonctionne comme update() et prend en paramètre le nom de la table et, éventuellement, une clause WHERE et une liste des paramètres positionnels pour cette clause.
Comme pour INSERT, UPDATE et DELETE, vous pouvez utiliser plusieurs approches pour récupérer les données d’une base SQLite avec SELECT :
- rawQuery() permet d’exécuter directement une instruction SELECT.
- query() permet de construire une requête à partir de ses différentes composantes. Un sujet de confusion classique est la classe SQLiteQueryBuilder et le problème des curseurs et de leurs fabriques.
5.3.1 Requêtes brutes
La solution la plus simple, au moins du point de vue de l’API, consiste à utiliser rawQuery() en lui passant simplement la requête SELECT. Cette dernière peut contenir des paramètres positionnels qui seront remplacés par les éléments du tableau passé en second paramètre. Voici un exemple :
Cursor c=db.rawQuery("SELECT name FROM sqlite_master
WHERE type=’table’
AND name=’constantes’", null);
Ici, nous interrogeons une table système de SQLite (sqlite_master) pour savoir si la table constantes existe déjà. La valeur renvoyée est un Cursor qui dispose de méthodes permettant de parcourir le résultat.
Si vos requêtes sont bien intégrées à votre application, c’est une approche très simple. En revanche, elle se complique lorsqu’une requête comprend des parties dynamiques que les paramètres positionnels ne peuvent plus gérer. Si l’ensemble de colonnes que vous voulez récupérer n’est pas connu au moment de la compilation, par exemple, concaténer les noms des colonnes pour former une liste délimitée par des virgules peut être ennuyeux – c’est là que query() entre en jeu.
5.3.2 Requêtes normales
La méthode query() prend en paramètre les parties d’une instruction SELECT afin de construire la requête. Ces différentes composantes apparaissent dans l’ordre suivant dans la liste des paramètres :
- Le nom de la table interrogée.
- La liste des colonnes à récupérer.
- La clause WHERE, qui peut contenir des paramètres positionnels.
- La liste des valeurs à substituer à ces paramètres positionnels.
- Une éventuelle clause GROUP BY.
- Une éventuelle clause ORDER BY.
- Une éventuelle clause HAVING.
À part le nom de la table, ces paramètres peuvent valoir null lorsqu’ils ne sont pas nécessaires :
Android offre un mécanisme permettant à une application à accéder aux données d’une autre application.
Ce mécanisme porte le nom de « fournisseur de contenu » ou « Content Provider ».
C’est l’interface qui connecte les données associées à un processus avec le code exécuté par un autre processus.
Android contient un ensemble de fournisseurs de contenu natifs, destinés à gérer des données du type audio, vidéo, image etc.
- Browser
- CallLog
- Contacts
- People
- Phones
- Photos
- Groups
- MediaStore
- Audio
- Albums
- Artists
- Genres
- Playlists
- Images
- Thumbnails
- Video
- Settings
Peu importe comment les données sont stockées, un fournisseur de contenu fournit une interface uniforme pour accéder à ces données.
Les données sont exposées sous forme tabulaire.
Les lignes représentent les enregistrements, les colonnes représentent les attributs.
Chaque enregistrement est identifié par un identificateur unique et représente la clé d’entrée vers l’enregistrement.
Utiliser un fournisseur de contenu, consiste à se servir d’une boîte noire. Le plus important est de savoir comment récupérer l’information de la boîte et non pas comment la boîte a été fabriquée.
Je veux par exemple récupérer la liste des contacts, peu importe comment le système a fait pour regrouper et sauvegarder ces contacts.
Chaque fournisseur de contenu est identifié par une URI (Uniform Resource Identifier) unique.
URI
La forme de l’URI est comme suit :
content://nompaquetage.provider/ comments /2
« content » pour signifier qu’il s’agit d’un fournisseur de contenu, et non pas le protocole ftp par exemple.
« nompaquetage.provider » représente l’autorité. Elle permet d’identifier le fournisseur de contenu.
« comments », le nom de la table. Il n’y a pas de limite sur le nombre de tables utilisées.
« 2 », le 2e enregistrement dans la table.
Si je veux accéder aux fournisseurs de contenu natifs :
android.provider.Browser.BOOKMARKS_URI
ContactsContract.contacts.CONTENT_URI
Méthodes
Pour accéder à un fournisseur de contenu, nous utilisons la classe abstraite « android.content.Contentresolver ».
Une instance de cette classe peut-être obtenue par l’appel : ContentResolver contentResolver=getContentResolver();
Les principales méthodes de la classe « ContentResolver » sont : « query » pour faire une requête à la base, « insert » pour insérer un élément, « update » pour mettre à jour un élément, « delete » pour effacer un élément et « getType » pour récupérer le type MIME de l’élément. On constate la similarité avec les méthodes de la base de données SQLite.
Nous allons étudier un exemple complet de création et l’utilisation d’un ContentProvider. Pour commencer, nous allons créer un projet :
-
Nom du projet : Content Provider Exemple
-
SDK version : 2.3.3
-
Nom de l’application : ContentProviderExemple
-
Nom du package : com.tutos.android.content.provider
-
Activité : ContentProviderExempleActivity Notre ContentProvider servira à gérer une liste de cours, chaque cours possédant un id, un nom et une description. Pour commencer, nous allons créer la classe qui contiendra le nom des colonnes de notre table SQLite.
public class SharedInformation { public SharedInformation() { } public static final class Cours implements BaseColumns { private Cours() {} public static final String COURS_ID = "COURS"; public static final String COURS_NAME = "COURS_NAME"; public static final String COURS_DESC = "COURS_DESC"; } }
Cette classe est très simple, elle contient simplement l’identifiant des 3 colonnes. Elle doit absolument implémenter BaseColumns.
Maintenant, nous allons créer une classe qui gérera notre base de données. Elle doit hériter de la classe SQLiteOpenHelper, pour nous faciliter la création de notre base de données.
Cette classe se nommera : AndroidProvider
// URI de notre content provider, elle sera utilisé pour accéder au ContentProvider
public static final Uri CONTENT_URI = Uri.parse("content://com.tutos.android.content.provider.tutosandroidprovider");
// Nom de notre base de données
public static final String CONTENT_PROVIDER_DB_NAME = "tutosandroid.db";
// Version de notre base de données
public static final int CONTENT_PROVIDER_DB_VERSION = 1;
// Nom de la table de notre base
public static final String CONTENT_PROVIDER_TABLE_NAME = "cours";
// Le Mime de notre content provider, la premiére partie est toujours identique
public static final String CONTENT_PROVIDER_MIME = "vnd.android.cursor.item/vnd.tutos.android.content.provider.cours";
// Notre DatabaseHelper
private static class DatabaseHelper extends SQLiteOpenHelper {
// Création à partir du Context, du Nom de la table et du numéro de version
DatabaseHelper(Context context) {
super(context,TutosAndroidProvider.CONTENT_PROVIDER_DB_NAME, null, TutosAndroidProvider.CONTENT_PROVIDER_DB_VERSION);
}
// Création des tables
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME + " (" + Cours.COURS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Cours.COURS_NAME + " VARCHAR(255)," + Cours.COURS_DESC + " VARCHAR(255)" + ");");
}
// Cette méthode sert à gérer la montée de version de notre base
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME);
onCreate(db);
}
}
Cette classe comprend :
-
La déclaration de l’uri de notre ContentProvider, vous remarquez bien le format « content:// »
-
Le nom de notre base de données
-
La version de la base
-
Le nom de notre table
-
Le Mime correspondant à notre ContentProvider
-
Une méthode onCreate, qui sert à créer nos tables. Il s’agit d’une Requête SQL classique.
-
Une méthode onUpgrade qui permet de monter de version dans une application. Maintenant nous allons surcharger les méthodes : onCreate (Celle du ContentProvider) qui permet d’initialiser notre DatabaseHelper dans la classe représentant notre ContentProvider.
@Override public boolean onCreate() { dbHelper = new DatabaseHelper(getContext()); return true; }
geType : qui retourne le type de notre ContentProvider,ce qui correspond tout simplement à notre MIME @Override public String getType(Uri uri) { return TutosAndroidProvider.CONTENT_PROVIDER_MIME; }
getId : Cette méthode nous permet de récupérer l’id de notre Uri
private long getId(Uri uri) {
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment != null) {
try {
return Long.parseLong(lastPathSegment);
} catch (NumberFormatException e) {
Log.e("TutosAndroidProvider", "Number Format Exception : " + e);
}
}
return -1;
}
insert : Cette méthode sert à rajouter une valeur à notre ContentProvider
/**
* Insert a value
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
try {
long id = db.insertOrThrow( TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME, null, values);
if (id == -1) {
throw new RuntimeException(String.format(
"%s : Failed to insert [%s] for unknown reasons.","TutosAndroidProvider", values, uri));
} else {
return ContentUris.withAppendedId(uri, id);
}
} finally {
db.close();
}
}
- On commence par récupérer une instance de la base de données en mode ecriture.
- Puis on insère nos données à l’aide de la méthode insertOrThrow qui retourne l’id de l’insertion dans la base et -1 en cas d’échec de l’insertion.
- Sans oublier de fermer la connexion à la base de données quelques soit le resultat.
update : Cette méthode permet de mettre à jour une valeur déja existante dans la base.
/**
* update a value
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
long id = getId(uri);
SQLiteDatabase db = dbHelper.getWritableDatabase();
try {
if (id < 0)
return db.update( TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME,values, selection, selectionArgs);
else
return db.update( TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME,values, Cours.COURS_ID + "=" + id, null);
} finally {
db.close();
}
}
- On récupérer l’id de l’élément
- Si l’id est supérieur à 0, on met à jour l’élément
- Sinon on essaye à mettre à jour l’élement par sa valeur.
- Sans oublier de fermer la base à la fin
delete : Cette méthode sert à suprimer un élément de notre Content provider.
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
long id = getId(uri);
SQLiteDatabase db = dbHelper.getWritableDatabase();
try {
if (id < 0)
return db.delete(
TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME, selection, selectionArgs);
else
return db.delete(
TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME,
Cours.COURS_ID + "=" + id, selectionArgs);
} finally {
db.close();
}
}
Même fonctionnement que pour le “Update” query : Cette méthode sert à récupérer une donnée présente dans notre ContentProvider.
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
long id = getId(uri);
SQLiteDatabase db = dbHelper.getReadableDatabase();
if (id < 0) {
return db.query(TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME,projection, selection, selectionArgs, null, null,
sortOrder);
} else {
return db.query(TutosAndroidProvider.CONTENT_PROVIDER_TABLE_NAME,
projection, Cours.COURS_ID + "=" + id, null, null, null,
null);
}
}
Création d’une classe de test Nous allons modifier ContentProviderExempleActivity pour tester notre contentProvider
public class ContentProviderExempleActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
insertRecords();
displayContentProvider();
}
private void displayContentProvider() {
String columns[] = new String[] { Cours.COURS_ID, Cours.COURS_NAME, Cours.COURS_DESC };
Uri mContacts = TutosAndroidProvider.CONTENT_URI;
Cursor cur = managedQuery(mContacts, columns, null, null, null);
Toast.makeText(ContentProviderExempleActivity.this, cur.getCount() + "",
Toast.LENGTH_LONG).show();
if (cur.moveToFirst()) {
String name = null;
do {
name = cur.getString(cur.getColumnIndex(Cours.COURS_ID)) + " " +
cur.getString(cur.getColumnIndex(Cours.COURS_NAME)) + " " +
cur.getString(cur.getColumnIndex(Cours.COURS_DESC));
Toast.makeText(this, name + " ", Toast.LENGTH_LONG).show();
} while (cur.moveToNext());
}
}
private void insertRecords() {
ContentValues contact = new ContentValues();
contact.put(Cours.COURS_NAME, "Android");
contact.put(Cours.COURS_DESC, "Introduction à la programmation sous Android");
getContentResolver().insert(TutosAndroidProvider.CONTENT_URI, contact);
contact.clear();
contact.put(Cours.COURS_NAME, "Java");
contact.put(Cours.COURS_DESC, "Introduction à la programmation Java");
getContentResolver().insert(TutosAndroidProvider.CONTENT_URI, contact);
contact.clear();
contact.put(Cours.COURS_NAME, "Iphone");
contact.put(Cours.COURS_DESC, "Introduction à l'objectif C");
getContentResolver().insert(TutosAndroidProvider.CONTENT_URI, contact);
}
}
Cette classe posséde deux méthodes :
-
InsertRecords On crée un ContentValue, puis on appelle la méthode getContentResolver pour récupérer une instance de ContentProvider, puis on appelle la méthode insert avec l’uri et les valeurs à insérer.
-
DisplayContentProvider Cette méthode sert à récupérer tous les éléments contenus dans notre ContentProvider Puis on parcours le Cursor et on affiche chaque élément AndroidManifest.xml Pour l’instant cette exemple ne fonctionne pas, car il manque la partie configuration et déclaration de notre provider, dans l’AndroidManifest
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ccom.tutos.android.content.provider" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name="com.tutos.android.content.provider.ContentProviderExempleActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.tutos.android.content.provider.cours" /> </intent-filter> </activity> <provider android:name="com.tutos.android.content.provider.TutosAndroidProvider" android:authorities="com.tutos.android.content.provider.tutosandroidprovider" /> </application> <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ccom.tutos.android.content.provider" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name="com.tutos.android.content.provider.ContentProviderExempleActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.tutos.android.content.provider.cours" /> </intent-filter> </activity> <provider android:name="com.tutos.android.content.provider.TutosAndroidProvider" android:authorities="com.tutos.android.content.provider.tutosandroidprovider" /> </application> </manifest>
Dans notre activité, on déclare une balise data dans l’intent-filter avec comme valeur le MimeType de notre content provider Puis on déclare une balise provider, avec comme valeur, le chemin vers la classe du provider et l’authorité définie dans le provider
Il y a plusieurs techniques :
- On peut utiliser l’API Google pour sauvegarder des préférences dans le dépôt Google (dans le nuage / in the Cloud).
- Utiliser la méthode « POST » pour pousser des données sur le réseau.
- Possibilité de spécifier un agent de sauvegarde des données vers le cloud L'utilisateur du téléphone indique le could à utiliser souvent Google Drive
- Balise au niveau du fichier AndroidManifest.xml android:allowBackup android:backupAgent: classe Java qui hérite de BackupAgent
La classe BackupAgent appel pour:
- réaliser une sauvegarde incrémentale de oldState vers newState en écrivant les données binaires dans data * onBackup(ParcelFileDescriptor oldState,BackupDataOutput data, ParcelFileDescriptor newState)
- restaurer une sauvegarde
- onRestore(BackupDataInput data, int appVersion, ParcelFileDescriptor newState)
Il existe des BackupAgentHelpers pour chaque type de données courantes (fichier, préférence, etc)
De plus, l'application peut elle même demander un backup incrémental * BackupManager.dataChanged()
Attention à ne pas appelée cette méthode trop souvent !
pour tester que l’enregistrement dans le nuage a bien fonctionné il faut:
-
Installez le paquetage dans l’émulateur.
-
Enregistrez quelques valeurs.
-
Vérifiez dans la vue « DDMS » l’existence du paquetage « com.blundell.tut ».
-
Copiez localement le fichier des préférences « TutorialPreferences.xml » disponible dans le répertoire « shared_prefs » et examinez son contenu. Est-ce qu’il contient les enregistrements? Normalement, sans surprise, la réponse est oui.
-
Ouvrez un terminal et lancez la commande « adb shell » pour vous connectez sur l’émulateur.
-
Démarrez le service de sauvegarde.
-
Demandez l’enregistrement du paquetage « com.blundell.tut ».
-
Exécutez cette demande.
-
Désinstallez le paquetage de votre terminal.
-
Vérifier dans la vue « DDMS » que le paquetage n’existe plus.
-
Réinstallez avec Eclipse le paquetage dans le terminal.
-
Surprise! Les valeurs enregistrées sont réapparues!
exemple d'envoi de paramètres en POST grâce à la classe HttpClient
StringBuffer stringBuffer = new StringBuffer("");
BufferedReader bufferedReader = null;
try {
HttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(URL);
List<NameValuePair> parametres = new ArrayList<NameValuePair>();
parametres.add(new BasicNameValuePair("parametre1", "valeurDuParametre1"));
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parametres);
httpPost.setEntity(formEntity);
HttpResponse httpResponse = httpClient.execute(httpPost); (
bufferedReader = new BufferedReader(
new InputStreamReader(httpResponse.getEntity().getContent()));
String ligneLue = bufferedReader.readLine();
while (ligneLue != null) {
stringBuffer.append(ligneLue);
stringBuffer.append("\n");
ligneLue = bufferedReader.readLine();
}
} catch (Exception e) {
Log.e(LOG_TAG, e.getMessage());
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
Log.e(LOG_TAG, e.getMessage());
}
}
}
Log.i(LOG_TAG, stringBuffer.toString());
On crée un client de type DefaultHttpClient qui servira à envoyer la requête préconstruite . Cette dernière est formée d’une liste de paramètres sous forme de paire nom/valeur associée à un formulaire encodé dans la requête .
Grâce à une requête HTTP de type POST, on peut communiquer des paires clé/valeur, mais on peut également transmettre des paramètres plus complexes comme des fichiers : il s’agit de la requête de type POST multiparties (multipart).
Ce type de requête POST n’est pas directement disponible sous Android avec l’API HttpClient, mais c’est encore une fois grâce à des bibliothèques Apache que nous allons sortir de l’impasse :
• Commons IO : http://commons.apache.org/io/
• HttpMime : http://hc.apache.org/httpcomponents-client/httpmime/index.html
• Mime4j : http://james.apache.org/mime4j/
exemple d'envoi d’une image et d’autres données via un POST multiparties
HttpClient httpClient = new DefaultHttpClient();
HttpPost postRequest = new HttpPost("http://www.exemple.com/upload_multi.php");
// Préparation des parties
File fichier = new File("imagePrise.jpg"); )
StringBody infosFichier = new StringBody("Photo du 2 mars 2014");
// Ajout de ces parties à la requête
MultipartEntity multipartContent = new MultipartEntity();
multipartContent.addPart("infos", infosFichier);
multipartContent.addPart("fichier", new FileBody(fichier));
postRequest.setEntity(multipartContent);
HttpResponse response = httpClient.execute(postRequest);
// Fermeture
response.getEntity().getContent().close();
Vous devez tout d’abord créer une instance de la classe HttpPost dont nous utiliserons la méthode setEntity . À cette méthode, nous donnerons en paramètre une instance de MultipartEntity qui est en fait le corps de la requête multiparties. La requête est remplie grâce aux différentes parties en invoquant la méthode addPart . À noter que l’envoi d’un fichier est rendu très simple grâce aux flux et à la signature de la méthode addPart prenant ce flux en paramètre
Problématique du multitâches
Les connexions peuvent être réalisées à de nombreuses occasions et dans de multiples parties de l’application :
• vérification de mises à jour au démarrage ;
• envoi de statistiques d’utilisation ou déconnexion à la fin de l’application ;
• consultation des meilleurs scores pour des jeux en accédant au menu adéquat ;
• synchronisation régulière d’information potentiellement un peu partout ;
• chargement de pages ;
• etc.
Mais il y a un impératif à garder en tête : l’application et son lien direct avec l’utilisateur qu’est l’interface graphique doivent toujours répondre ! Si l’on n’utilise pas les capacités multitâches d’Android, on peut se retrouver à attendre qu’une connexion lente s’établisse avec un site distant ou même voir un traitement échouer en cas de coupure (le fameux effet tunnel). D’ailleurs, si vous développez sur des appareils mobiles, la connexion donnée est par définition intermittente. Il faut absolument en tenir compte. Sur ce sujet, les principes de base sont les mêmes depuis longtemps :
• réduire les connexions réseau au minimum ;
• proposer un mode dégradé de l’application afin qu’elle puisse fonctionner sans réseau et/ou prévienne l’utilisateur du comportement à adopter (quitter l’application, attendre qu’elle se reconnecte automatiquement, etc.).
Dans ce Projet, nous avons abordé différentes façons de stocker, de manipuler et d’accéder à des données depuis vos applications : préférences, fichiers et bases de données. Chaque mécanisme possède ses propres avantages et inconvénients. Pour choisir le plus approprié, vous pourrez vous baser sur plusieurs critères : portée de l’accès aux données, difficulté/délai d’implémentation, structuration ou non des données, rapidité en lecture et enfin, nécessité ou non de devoir effectuer des requêtes sur ces données.
La nécessité de partager et d’exposer vos données aux autres applications vous fera également pencher pour un stockage en particulier ou un autre. Vous vous poserez peut-être la question suivante : pourquoi est-il intéressant de partager vos données avec d’autres applications ? Sachez que c’est grâce au partage de données que fonctionnent les applications essentielles d’Android : contacts, photos, musiques, etc. Alors pourquoi ne pas rendre vos applications également indispensables ?