IUT de Lannion P.
Nerzic
Dept Informatique Programmation Android 2015-16
Semaine 6
Bases de données SQLite3 <>
Après avoir représenté une liste d’items sous forme d’un tableau en semaine 4, nous allons la stocker
dans un SGBD SQL.
Figure 39: Logo de SQLite3
• SQLite3
• Requêtes et curseurs
• WebServices
6.1. SQLite3
6.1.1. Stockage d’informations
Il n’est pas pertinent d’enregistrer des informations dans un tableau stocké en mémoire vive, c’est à
dire sur un support volatile.
Android contient un SGBD SQL appelé SQLite3, parfait pour stocker des informations. On peut le
lancer à partir de bash :
bash$ sqlite3 test.db
sqlite> CREATE TABLE Planetes (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
distance REAL,
idType INTEGER NOT NULL,
FOREIGN KEY(idType) REFERENCES Types(_id));
sqlite> INSERT INTO Planetes VALUES (1, "Sedna", 3.5, 1);
6.1.2. SQLite3
SQLite3 est un vrai SGBD relationnel SQL, mais simplifié pour tenir sur une tablette.
Ce qui lui manque :
98
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
• Aucune gestion des utilisateurs (autorisations), pas de sécurité.
• Pas de réglages pour améliorer les performances car
• Peu de types de données, ex: date = entier ou chaîne, un seul type d’entiers. . .
SQLite3 fonctionne sans serveur. Il stocke ses données dans un seul fichier. Ce fichier est portable,
c’est à dire copiable sur n’importe quelle autre machine.
6.1.3. Exemples SQL
Toutes les requêtes SQL que vous connaissez fonctionnent, p. ex. :
SELECT COUNT(*) FROM Planetes WHERE nom LIKE 'Ter%';
SELECT * FROM Planete WHERE distance > 200.0 ORDER BY distance;
SELECT AVG(distance) AS moyenne FROM Planetes GROUP BY idType;
DELETE FROM Planetes WHERE distance IS NULL;
ALTER TABLE Planetes ADD COLUMN date INTEGER;
UPDATE Planetes SET date=strftime('%s','now');
DROP TABLE Planetes;
Jointures, groupements, requêtes imbriquées, transactions, index, triggers. . . tout cela existe.
Consulter la documentation.
6.1.4. Autres usages de SQLite3
Ce SGBD est utilisé dans de nombreuses applications ailleurs que dans Android, p. ex. dans Firefox
pour stocker les marque-pages, l’historique de navigation, etc.
SQLite3 est aussi une API pour différents langages de programmation : C, Python, PHP, Java. . . On
peut exécuter des requêtes SQL en appelant des fonctions.
Android le propose aux programmeurs pour stocker des informations structurées, plutôt que bricoler
avec des fichiers. Il est assez facile à utiliser une fois le cadre mis en place.
6.1.5. Lancement de sqlite3 en shell
sqlite3 est une commande qui ouvre un shell SQL, pour saisir directement des requêtes, sans
connexion. On peut fournir un paramètre, le nom d’un fichier qui contient la base, soit aucun et dans
ce cas, la base n’est qu’en mémoire, perdue quand on quitte.
bash$ sqlite3 test.db
sqlite>
Cette commande est dans le dossier du SDK, sous-dossier platform-tools (Linux et Windows). Elle
n’est pas forcément incluse dans le système Linux de la tablette.
6.1.6. Commandes internes
Le shell de SQLite3 possède des commandes internes, p. ex. :
.help affiche la liste des commandes internes
99
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
.dump table affiche le contenu de la table ou de toute la base si la table est omise
.schema table affiche la structure de la table
.headers mettre on ou off pour écrire les noms des colonnes en tête de tous les select
.exit sort du shell sqlite3
6.2. SQLite dans une application Android
6.2.1. Bases de données Android
Chaque application peut créer une base de données. C’est un fichier *.db placé dans le dossier
/data/data/PAQUETAGE/databases/NOM_BDD
Vous pourrez échanger ce fichier avec le PC (adb push ou pull). Consulter cette page pour des
détails sur la marche à suivre.
Dans une application Android, ces fichiers sont manipulés à l’aide de classes de l’API.
NB: ce cours commence par une grande simplification (l’ouverture d’une BDD). Lisez la totalité pour
savoir comment on procède en réalité.
6.2.2. Classes pour travailler avec SQLite
Il faut connaître au moins deux classes :
• SQLiteDatabase : elle représente une BDD. Ses méthodes permettent d’exécuter une requête,
par exemple :
– void execSQL(String sql) pour CREATE, ALTER, DROP. . . qui ne retournent pas de
données.
– Cursor rawQuery(String sql, ...) pour des SELECT qui retournent des n-uplets.
– D’autres méthodes pour des requêtes spécialisées.
• Cursor : représente un n-uplet. Il y a des méthodes pour récupérer les colonnes.
Voyons les détails.
6.2.3. Principes
Voici les étapes du travail avec une BDD en Java :
1. Ouverture de la base, création du fichier si besoin :
SQLiteDatabase bdd;
bdd = SQLiteDatabase.openOrCreateDatabase(...);
2. Exécution de requêtes :
1. Obtention d’un Curseur sur le résultat des select
Cursor cursor = bdd.rawQuery(requete, ...);
2. Parcours des n-uplets du curseur à l’aide d’une boucle for
3. Fermeture du curseur (indispensable, prévoir les exceptions, voir plus loin)
cursor.close()
3. Fermeture de la base (indispensable, prévoir les exceptions) :
bdd.close()
100
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.2.4. Base ouverte dans une activité
Si vous ouvrez la base pour toute la vie de l’activité dans onCreate, alors vous devez la fermer dans
la méthode onDestroy.
class MonActivite extends Activity
{
private SQLiteDatabase bdd;
void onCreate(...) {
...
bdd = SQLiteDatabase.openOrCreateDatabase(...);
}
void onDestroy() {
...
bdd.close();
}
}
6.2.5. Recommandations
Il est préférable de définir une classe associée à chaque table (et même chaque jointure). Ça permet de
faire évoluer le logiciel assez facilement. Cette classe regroupe toutes les requêtes SQL la concernant :
création, suppression, mise à jour, parcours, insertions. . . sous forme de méthodes de classe. La base
de données est passée en premier paramètre de toutes les méthodes.
Cette démarche s’inspire du patron de conception Active Record qui représente une table par une
classe. Ses instances sont les n-uplets de la table. Les attributs de la table sont gérés par des accesseurs.
Des méthodes comme find permettent de récupérer des n-uplets et des méthodes permettent la mise
à jour de la base : new, save et delete.
Voir le projet ActiveAndroid pour une implantation Android.
6.2.6. Noms des colonnes
Dans ce cours, on va mettre en œuvre une simplification de ce patron de conception.
On définit une classe ne contenant que des méthodes statiques.
public class TablePlanetes
{
// méthodes statiques pour gérer les données
public static create(...) ...
public static drop(...) ...
public static insert(...) ...
...
}
101
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.2.7. Classe pour une table
Par exemple, pour créer et supprimer la table :
public static void create(SQLiteDatabase bdd)
{
bdd.execSQL( "CREATE TABLE Planetes (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"nom TEXT NOT NULL," +
"distance REAL)");
}
public static void drop(SQLiteDatabase bdd)
{
bdd.execSQL("DROP TABLE IF EXISTS Planetes");
}
Gare à la syntaxe, ça doit créer du SQL correct !
6.2.8. Exemples de méthodes
Voici des méthodes pour créer ou supprimer des n-uplets :
public static void insert(SQLiteDatabase bdd, Planete pl)
{
bdd.execSQL(
"INSERT INTO Planetes VALUES (null, ?, ?)",
new Object[] { pl.getNom(), pl.getDistance() });
}
public static void delete(SQLiteDatabase bdd, long id)
{
bdd.execSQL(
"DELETE FROM Planetes WHERE _id=?",
new Object[] {id});
}
On va aussi rajouter des méthodes de consultation des données. Voyons d’abord la méthode execSQL.
6.2.9. Méthodes SQLiteDatabase.execSQL
Cette méthode exécute une requête SQL qui ne retourne pas d’informations : CREATE, INSERT. . .
• void execSQL (String sql) : on doit juste fournir la requête sous forme d’une chaîne. C’est
une requête constante. Ne pas mettre de ; à la fin. Par exemple :
bdd.execSQL("DROP TABLE Planetes");
• void execSQL (String sql, Object[] bindArgs) : c’est pour le même type de requête mais
contenant des jokers ? à affecter avec les objets fournis en paramètre.
102
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
bdd.execSQL("DELETE FROM Planetes WHERE _id BETWEEN ? AND ?",
new Object[] { 3, 8 });
6.2.10. Méthodes spécialisées
Android propose des méthodes spécifiques pour insérer, modifier, supprimer des n-uplets :
• int insert(String table, null, ContentValues valeurs)
• int update(String table, ContentValues valeurs, String whereClause, String[]
whereArgs)
• int delete(String table, String whereClause, String[] whereArgs)
La différence avec execSQL, c’est qu’elles demandent un tableau de String. Il faut donc convertir
toutes les données en chaînes.
6.2.11. Méthode insert
insert(table, null, valeurs) effectue une requête du type :
INSERT INTO table (col1, col2...) VALUES (val1, val2...);
Elle retourne l’identifiant du nouveau n-uplet. Ses paramètres sont :
• table : fournir le nom de la table
• null (ça correspond à une bidouille)
• valeurs : c’est une structure du type ContentValues qui associe des noms et des valeurs
quelconques :
ContentValues valeurs = new ContentValues();
valeurs.putNull("_id");
valeurs.put("nom", "Sedna");
int id = bdd.insert("Planetes", null, valeurs);
6.2.12. Méthodes update et delete
update(table, valeurs, whereClause, whereArgs) fait UPDATE table SET col1=val1,
col2=val2 WHERE ...; et delete(table, whereClause, whereArgs) effectue DELETE FROM
table WHERE ...; Elles retournent le nombre de n-uplets altérés. Les paramètres sont :
• table : fournir le nom de la table
• valeurs : ce sont les couples (colonne, valeur à mettre)
• whereClause : une condition contenant des jokers ?
• whereArgs : chaînes à mettre à la place des ?
ContentValues valeurs = new ContentValues();
valeurs.put("nom", "Sedna");
bdd.update("Planetes", valeurs, "_id=?", new String[] { "4" });
bdd.delete("Planetes", "_id BETWEEN ? AND ?",new String[]{"5","8"});
103
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.2.13. Méthode rawQuery
Cette méthode, rawQuery permet d’exécuter des requêtes de type SELECT (pas de ; à la fin). Elle
retourne un objet Java de type Cursor qui permet de parcourir les n-uplets un à un :
Cursor cursor = bdd.rawQuery("SELECT * FROM table WHERE...");
try {
if (cursor.moveToFirst()) { // obligatoire
while (!cursor.isAfterLast()) { // test de fin
/** utiliser le curseur... **/
cursor.moveToNext(); // n-uplet suivant
}
}
} finally {
if (cursor != null) cursor.close();
}
Remarquez le finally obligatoire qui permet de fermer le curseur.
6.2.14. rawQuery pour un seul n-uplet
S’il n’y a qu’un seul n-uplet dans la réponse, il n’est pas nécessaire de faire une boucle, mais il faut
quand même initialiser puis fermer le curseur :
Cursor cursor = bdd.rawQuery("SELECT * FROM table WHERE...");
try {
if (cursor.moveToFirst()) { // obligatoire
/** utiliser le curseur... **/
}
} finally {
if (cursor != null) cursor.close();
}
6.2.15. Classe Cursor
La classe Cursor propose deux types de méthodes :
• celles qui permettent de parcourir les n-uplets :
– getCount() : retourne le nombre de n-uplets,
– getColumnCount() : retourne le nombre de colonnes,
– moveToFirst() : met le curseur sur le premier (à faire !),
– isAfterLast() : retourne vrai si le parcours est fini,
– moveToNext() : passe au n-uplet suivant.
• celles qui permettent d’obtenir la valeur de la colonne nc (0..getColumnCount()-1 ) du n-uplet
courant :
– getColumnName(nc) : retourne le nom de la colonne nc,
– isNull(nc) : vrai si la colonne nc est nulle,
– getInt(nc), getLong(nc), getFloat(nc), getString(nc), etc. : valeur de la colonne nc.
104
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.2.16. Exemple de requête, classe TablePlanetes
public static String getNom(... bdd, long id)
{
Cursor cursor = bdd.rawQuery(
"SELECT nom FROM Planetes WHERE _id=?",
new String[] {Long.toString(id)});
try {
if (cursor.moveToFirst() && !cursor.isNull(0)) {
return cursor.getString(0);
} else
return null;
} finally {
if (cursor != null) cursor.close();
}
}
6.2.17. Autre type de requête
Cette autre méthode retourne non pas une valeur, mais directement un curseur. Elle est utilisée pour
afficher tous les éléments de la table dans une liste, voir page 109.
public static Cursor getAll(SQLiteDatabase bdd)
{
return bdd.rawQuery("SELECT * FROM Planetes", null);
}
Attention, votre application doit prendre soin de fermer ce curseur dès qu’il ne sert plus, ou alors de
le fournir à un objet (ex: un adaptateur) qui sait le fermer automatiquement.
6.2.18. Méthodes query
Android propose également des méthodes « pratiques » pour effectuer des requêtes, telles que :
query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy, String limit)
mais je ne vois pas l’intérêt de recoder en Java ce qui se fait parfaitement en SQL, sans compter les
risques d’erreur si on permute involontairement les paramètres de ces méthodes.
6.2.19. Ouverture d’une base
Revenons vers les aspects gestion interne de la base de données. L’ouverture d’une base se fait ainsi :
105
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
String dbpath =
this.getFilesDir().getPath().concat("/test.db");
SQLiteDatabase bdd =
SQLiteDatabase.openOrCreateDatabase(dbpath, null);
NB: cela ne crée pas les tables, seulement le fichier qui contient la base.
Il faut fournir le chemin d’accès à la base. Mais en faisant ainsi, la base est créée dans
/data/data/*package*/files et non pas .../databases. Voir page 105 pour la véritable façon de
faire.
6.2.20. Première ouverture et ouvertures suivantes
Ensuite, après avoir ouvert la base, si c’est la première fois, il faut créer les tables. Cependant, ça
cause une erreur de créer une table qui existe déjà et il serait coûteux de tester l’existence des tables.
Une possibilité consiste à rajouter IF NOT EXISTS à la requête de création. Par exemple :
bdd.execSQL(
"CREATE TABLE IF NOT EXISTS Planetes (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"nom TEXT NOT NULL)");
Un autre problème, c’est la mise à jour de l’application. Qu’allez-vous proposer à vos clients si vous
changez le schéma de la base entre la V1 et la V2, la V2 et la V3. . . ?
6.2.21. Un helper pour gérer l’ouverture/création/màj
Android propose la classe supplémentaire SQLiteOpenHelper qui facilite la gestion des bases de
données. Cette classe est une sorte d’écouteur avec deux méthodes à surcharger :
• onCreate(bdd) : cette méthode est appelée quand la base de données n’existe pas encore. Son
rôle est de créer les tables. C’est là que vous mettez les CREATE TABLE...
• onUpgrade(bdd, int oldV, int newV) : cette méthode est appelée quand la version de
l’application est supérieure à celle de la base. Son rôle peut être de faire des ALTER TABLE...,
UPDATE... ou carrément DROP TABLE... suivis de onCreate.
Les méthodes getReadableDatabase et getWritableDatabase de SQLiteOpenHelper ouvrent la
base et appellent automatiquement onCreate et onUpgrade si nécessaire.
6.2.22. Exemple de helper
public class MySQLiteHelper extends SQLiteOpenHelper
{
// nom du fichier contenant la base de données
private static final String DB_NAME = "test.db";
// version du schéma de la base de données
106
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
private static final int DB_VERSION = 1;
// constructeur du helper = ouvre et crée/màj la base
public MySQLiteHelper(Context context)
{
super(context, DB_NAME, null, DB_VERSION);
}
...
6.2.23. Exemple de helper, suite
@Override
public void onCreate(SQLiteDatabase bdd)
{
// création avec la méthode de la classe TablePlanetes
TablePlanetes.create(bdd);
}
@Override
public void onUpgrade(SQLiteDatabase bdd,
int oldVersion, int newVersion)
{
// suppression de toutes les données !
TablePlanetes.drop(bdd);
// re-création de la base
onCreate(bdd);
}
}
6.2.24. méthode onUpgrade
public void onUpgrade(SQLiteDatabase bdd,
int oldVersion, int newVersion)
Dans le cas d’une application sérieuse, on ne détruit pas toutes les données utilisateur quand on change
le schéma. C’est à vous de déterminer les modifications minimales qui permettent de transformer les
données présentes, de leur version actuelle oldVersion à la version newVersion.
Il est indiqué de procéder par étapes :
• passer de la version oldVersion à la oldVersion+1
• passer de la version oldVersion+1 à la oldVersion+2
• ainsi de suite, jusqu’à arriver à la newVersion.
107
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.2.25. méthode onUpgrade, suite
Cela donne quelque chose comme ça :
@Override
public void onUpgrade(..., int oldVersion, int newVersion){
while (oldVersion < newVersion) {
oldVersion++;
switch (oldVersion) {
case 2: // amener la base de la V1 à la V2
bdd.execSQL("ALTER TABLE Planetes ADD COLUMN taille REAL");
break;
case 3: // amener la base de la V2 à la V3
...
break;
}
}
}
6.2.26. Retour à l’application
Avec un helper, cela devient très simple d’ouvrir une base, en consultation seule ou en modification :
MySQLiteHelper helper = new MySQLiteHelper(this);
SQLiteDatabase bdd = helper.getReadableDatabase();
/* ou bien */
SQLiteDatabase bdd = helper.getWritableDatabase();
// requêtes SQL sur l'objet bdd
...
A la terminaison de l’application, c’est le helper qu’il faut fermer, et c’est lui qui ferme la base :
helper.close();
6.3. CursorAdapter et Loaders
6.3.1. Lien entre une BDD et un ListView
On revient vers l’application qui affiche une liste. Cette fois, la liste doit être le résultat d’une requête
SELECT. Comment faire ?
Les choses sont devenues relativement complexes depuis Android 3. Afin d’éviter que l’application se
bloque lors du calcul de la requête et voir le message « l’application ne répond pas », Android emploie
un mécanisme appelé chargeur, loader en anglais.
Le principe est de rendre le calcul de la requête SQL asynchrone, désynchronisé de l’interface. On
lance la requête SELECT et en même temps, on affiche une liste vide. Lorsque la requête sera finie, la
liste sera mise à jour, mais en attendant, l’interface ne reste pas bloquée.
108
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.3.2. Étapes à suivre
• Méthode onCreate de l’activité qui affiche la liste :
1. Définir un adaptateur de curseur pour la liste
2. Ouvrir la base de données
3. Créer un chargeur de curseur et l’associer à this
4. Démarrer le chargeur
• Définir la classe MonCursorLoader, sous-classe de CursorLoader qui effectue la requête SQL
• Définir trois callbacks :
– onCreateLoader : retourne un MonCursorLoader
– onLoadFinished et onLoaderReset : reçoivent un curseur à jour et mettent à jour
l’adaptateur
Voici le détail.
6.3.3. Activité ou fragment d’affichage d’une liste
Cette activité hérite de ListActivity (ou ListFragment) et elle implémente les méthodes d’un
« chargeur de curseur » :
public class MainActivity extends ListActivity
implements LoaderManager.LoaderCallbacks<Cursor>
{
private MySQLiteHelper helper;
private SQLiteDatabase bdd;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
...
6.3.4. Création d’un adaptateur de curseur
Ça ressemble à l’adaptateur d’un tableau, mais on fournit deux listes : les noms des colonnes et les
identifiants des vues dans lesquelles il faut mettre les valeurs.
// créer un adaptateur curseur-liste
adapter = new SimpleCursorAdapter(this,
// layout des éléments de la liste
android.R.layout.simple_list_item_2,
// le curseur sera chargé par le loader
null,
// noms des colonnes à afficher
new String[]{"_id", "nom"},
// identifiants des TextView qui affichent les colonnes
109
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
new int[]{android.R.id.text1, android.R.id.text2},
0); // options, toujours mettre 0
setListAdapter(adapter);
6.3.5. Ouverture de la base et création d’un chargeur
Ensuite, toujours dans la méthode onCreate de l’activité, on ouvre la base, ici en consultation car
cette activité ne modifie pas les données, puis on crée un chargeur associé à this.
// identifiant du chargeur (utile s'il y en a plusieurs)
private static final int LOADER_ID = 1;
// ouvrir la base de données SQLite
helper = new MySQLiteHelper(this);
bdd = helper.getReadableDatabase();
// crée et démarre un chargeur pour cette liste
getLoaderManager().initLoader(LOADER_ID, null, this);
Cette dernière instruction exige de définir trois callbacks dans l’activité : onCreateLoader,
onLoadFinished et onLoaderReset. Voyons d’abord la première.
6.3.6. Callback onCreateLoader de l’activité
Toujours dans la classe d’activité qui affiche la liste :
@Override
public Loader<Cursor> onCreateLoader(int loaderID,
Bundle bundle)
{
return new MonCursorLoader(getApplicationContext(), bdd);
}
Cette callback fait instancier un MonCursorLoader qui est une sous-classe de CursorLoader, définie
dans notre application. Son rôle est de lancer la requête qui retourne le curseur contenant les données
à afficher.
6.3.7. classe MonCursorLoader
static private class MonCursorLoader extends CursorLoader
{
private SQLiteDatabase bdd;
public MonCursorLoader(Context context, SQLiteDatabase bdd) {
super(context);
this.bdd = bdd;
}
110
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
@Override
protected Cursor onLoadInBackground() {
return TablePlanetes.getAll(bdd);
}
Voir page 104 pour la méthode getAll, elle fait seulement return bdd.rawQuery("SELECT * FROM
Planetes", null);
6.3.8. Callback onLoadFinished de l’activité
Pour finir, la callback qui est appelée lorsque les données sont devenues disponibles ; elle met à jour
l’adaptateur, ce qui affiche les n-uplets dans la liste. L’autre callback est appelée si le chargeur doit
être supprimé. On met donc toujours ceci :
@Override
public void onLoadFinished(Loader<Cursor> loader,
Cursor cursor) {
adapter.changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.changeCursor(null);
}
6.3.9. Mise à jour de la liste
Quand il faut mettre à jour la liste, si les données ont changé, il faut relancer le chargeur de curseur
et non pas l’adaptateur. Cela se fait de la manière suivante :
// le chargeur doit recommencer son travail
getLoaderManager().restartLoader(LOADER_ID, null, this);
6.4. ContentProviders
6.4.1. Présentation rapide
Les Fournisseurs de contenu sont des sortes de tables de données disponibles d’une application à
l’autre et accessibles à l’aide d’un URI (généralisation d’un URL). Un exemple est le carnet d’adresse
de votre téléphone. D’autres applications que la téléphonie peuvent y avoir accès.
Un ContentProvider possède différentes méthodes ressemblant à celles des bases de données :
• query : retourne un Cursor comme le fait un SELECT,
• insert, update, delete : modifient les données,
• D’autres méthodes permettent de consulter le type MIME des données.
Comme c’est très compliqué à mettre en œuvre et que ça ressemble à une simple table SQL sans
jointure, on n’en parlera pas plus ici.
111
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.5. WebServices
6.5.1. Echange entre un serveur SQL et une application Android
On arrive au plus intéressant, faire en sorte qu’une application Android stocke ses données sur un
serveur distant. Pour commencer, révisez vos cours de Web Design, PHP, PDO. . .
Soit un serveur HTTP connecté à une base de données (PostgreSQL en TP). Ce serveur possède des
scripts PHP qui vont répondre aux demandes de l’application Android à l’aide d’au moins deux types
d’échanges HTTP3 :
• Les SELECT vont être traitées par des GET,
• Les INSERT, UPDATE, DELETE. . . vont être envoyés par des POST.
Chaque requête sera associée à un script spécifique.
6.5.2. Principe général
Soit la requête SELECT * FROM Planetes WHERE _id=3. On va envoyer l’identifiant 3 sur le réseau
et c’est un script PHP qui va effectuer la requête. Il y a un script par sorte de requête, donc chacun
sait exactement quels paramètres il va recevoir.
1. L’application construit une requête HTTP, p. ex. de type GET
• URL = http://serveur/script?paramètres
• paramètres = conditions du select, p. ex. identifiant=3.
2. L’application (cliente) envoie cette requête au serveur puis attend la réponse,
3. Le script PHP exécute la requête puis retourne le résultat encodé en JSON à l’application,
4. L’application décode le résultat et l’affiche.
Les autres requêtes suivent le même principe client-serveur.
6.5.3. Exemple de script PHP Post
Voici un script qui modifie un n-uplet. Il est lancé par un POST.
// connexion au serveur SQL par PDO
$db = new PDO("pgsql:host=$hostname;dbname=$dbname",
$login, $passwd);
// paramètres de la requête (TODO: tester la présence)
$id = $_POST['_id'];
$nom = $_POST['nom'];
// préparation et exécution
$query = $db->prepare(
"UPDATE Planetes SET nom=? WHERE _id=?");
$query->execute(array($nom, $id));
NB: ici, on se ne préoccupe pas de sécurité.
3
En fait, un vrai WebService Restful est plus complexe, voir wikipedia
112
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.5.4. Exemple de script PHP Get
Voici get_all_planetes.php qui retourne tous les n-uplets :
// connexion au serveur SQL par PDO
$db = new PDO("pgsql:host=$hostname;dbname=$dbname",
$login, $passwd);
// liste de tous les types
$query = $db->prepare("SELECT * FROM Planetes ORDER BY _id;");
$query->execute();
// encodage JSON de toutes les réponses
echo json_encode($query->fetchAll(PDO::FETCH_NUM));
6.5.5. Format JSON JavaScript Object Notation
C’est un format pour transporter des tableaux et des objets à travers le réseau. Ils sont écrits sous
forme d’un texte. JSON est une alternative au XML.
Par exemple la liste des n-uplets présents dans la table Planetes :
[[1,"Mercure",58],[2,"Venus",108],[3,"Terre",150],...
En PHP, c’est très simple :
// encodage : tableau -> jsondata
$jsondata = json_encode($tableau);
// décodage : jsondata -> tableau
$tableau = json_decode($jsondata);
Le tableau peut venir d’un fetchAll(PDO::FETCH_NUM).
6.5.6. JSON en Java
En Java, c’est plus compliqué. Il faut employer une instance de JSONArray. Elle possède des setters
et des getters pour chaque type de données.
// encodage : tableau -> jsondata
int[] tableau = ...;
JSONArray ja = new JSONArray();
for (int v: tableau) ja.put(v);
String jsondata = ja.toString();
// décodage : jsondata -> tableau
JSONArray ja = new JSONArray(jsondata);
final int nb = ja.length();
int[] tableau = new int[nb];
for (int i=0; i<nb; i++) tableau[i] = ja.getInt(i);
C’est à adapter aux données à échanger : entiers, chaînes. . .
113
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
6.5.7. Dans l’application Android
Tout le problème est de construire une requête HTTP, d’attendre la réponse, de la décoder et de
l’afficher.
Pour commencer, il faut que l’application soit autorisée à accéder à internet. Rajouter cette ligne
dans le manifeste :
<uses-permission android:name="android.permission.INTERNET"/>
Ensuite, il faut transformer tout ce qui est requête SQL :
• Affichage des données : changer le chargeur de curseur,
• Modifications des données.
Voyons cela dans l’ordre.
6.5.8. Affichage d’une liste
Il suffit de reprogrammer la méthode getAll de la classe TablePlanetes, voir page 104 et 112 :
public static Cursor getAll(RemoteDatabase bdd) {
// requête Get à l'aide de la classe RemoteDatabase
String jsondata = bdd.get("get_all_planetes.php", null);
// décoder les n-uplets et en faire un curseur
return bdd.cursorFromJSON(jsondata,
new String[] { "_id", "nom", "distance" });
}
J’ai retiré les tests d’erreur et traitements d’exceptions.
6.5.9. La classe RemoteDatabase
C’est une classe que je vous propose. Elle fait un peu tout : le café, les croissants. . . C’est elle qui
organise la communication avec le serveur, dont ces méthodes :
• get("script.php", params) appelle le script PHP par un GET en lui passant les paramètres
indiqués et retourne un String contenant la réponse du serveur sous forme de données JSON.
• cursorFromJSON(jsondata, noms_des_colonnes) construit un curseur avec la réponse JSON
du serveur. On est obligé de fournir les noms des colonnes car ils ne sont pas présents dans les
données JSON.
Cette classe est assez complexe. Une partie des explications viendra la semaine prochaine.
6.5.10. Modification d’un n-uplet
Voici maintenant une requête POST pour modifier un n-uplet :
114
IUT de Lannion P. Nerzic
Dept Informatique Programmation Android 2015-16
public static void update(RemoteDatabase bdd,
RemoteDatabaseListener listener, Planete pl)
{
// paramètres de la requête
ContentValues params = new ContentValues();
params.put("_id", pl.getId());
params.put("nom", pl.getNom());
// requête Post asynchrone
bdd.post(listener, "update_planete.php", params);
}
Elle appelle le script update_planete.php suivant.
6.5.11. Script update_planete.php
Avant d’expliquer la méthode post de la classe RemoteDatabase, voici le script update_planete.php
qui est déclenché par la requête du transparent précédent :
// connexion à la base de données
$db = new PDO("pgsql:host=".$hostname.";dbname=".$dbname,
$login, $passwd);
// paramétres de la requête
$id = $_POST['_id'];
$nom = $_POST['libelle'];
// préparation et exécution
$query = $db->prepare("UPDATE Planetes SET nom=? WHERE _id=?");
$query->execute(array($nom, $id));
6.5.12. Méthode post(écouteur, script, params)
Cette méthode appelle un script PHP en lui fournissant des paramètres. Par exemple, c’est le script
update_type.php avec les paramètres _id et libelle.
Elle a une particularité : cette méthode est asynchrone. C’est à dire qu’elle lance un échange réseau
en arrière-plan, et n’attend pas qu’il se termine. C’est obligatoire, sinon Android affiche une erreur :
l’application ne répond pas, dialogue « ANR ».
Le principe pour cela est de créer une AsyncTask. Elle gère une action qui est exécutée dans un autre
thread que celui de l’interface. On verra cela la semaine prochaine.
Du coup, il faut un écouteur à prévenir quand l’action est terminée. C’est le premier paramètre passé
à la méthode post. Par exemple, c’est l’activité d’affichage de liste qui peut alors mettre à jour la
liste affichée.
6.5.13. C’est tout pour aujourd’hui
C’est fini pour cette semaine, rendez-vous la semaine prochaine pour un cours sur les accès réseau
asynchrones ainsi que la cartographie OpenStreetView.
115