Données
Introduction
L’utilisation de données est une pratique indispensable. Il est nécessaire de comprendre comment sauvegarder ces données et pouvoir les réutiliser. Dans ce chapitre, plusieurs voies vont être abordées.
D’abord, nous verrons le système de stockage appelé Shared Preferences. Il s’agit d’un outil très pratique et surtout facile à mettre en œuvre. En revanche, il n’offre pas de garanties suffisantes pour le stockage de données plus sensibles ou qui ont vocation à être gardées plus longtemps. Pour cela, nous aborderons la base de données embarquée SQLite.
Enfin, puisque le modèle aujourd’hui s’y prête, nous étudierons l’exploitation de données au format JSON au travers d’une API.
Shared Preferences
1. Introduction
Les Shared Preferences sont un bon moyen d’enregistrer des données avec Flutter. Cette pratique découle de la fonctionnalité du même nom disponible sous Android et de NSUserDefaults sous iOS.
Grâce à ce système, les données peuvent être rendues persistantes sur l’appareil de l’utilisateur de manière asynchrone. Toutefois, cette méthode de persistance n’est pas garantie. Il convient donc d’éviter de sauvegarder des données importantes par ce biais.
2. Récupération des dépendances
L’utilisation de Shared Preferences nécessite le package shared_preferences. Sa déclaration s’opère en renseignant le fichier pubspec.yaml:
dependencies:
shared_preferences: ^0.5.6+3
Une fois déclarée, la dépendance sera récupérée en cliquant sur Packages get.
Ce package appartient au programme Flutter Favorite. Il est donc recommandé par l’équipe Flutter.
3. Création de l’application
Le dessein de l’application est d’enregistrer les données d’une personne dans les Shared Preferences et de pouvoir les récupérer pour les afficher à l’écran. L’application permettra également de supprimer ces données au besoin.
a. Classe Personne
Dans un nouveau projet Flutter, un fichier nommé personne.dart contenant une classe intitulée Personne est créé dans le dossier lib. Elle comprend trois attributs (nom, prénom et âge), un constructeur et les accesseurs.
class Personne {
String _nom;
String _prenom;
String _age;
Personne(this._nom, this._prenom, this._age);
String get age => _age;
set age(String value) {
_age = value;
}
String get prenom => _prenom;
set prenom(String value) {
_prenom = value;
}
String get nom => _nom;
set nom(String value) {
_nom = value;
}
}
Cette classe servira de support à la création d’une nouvelle personne qui sera utilisée...
SQLite
1. Introduction
Il existe un autre moyen de conserver des données sur un appareil de manière plus fiable qu’avec les Shared Preferences. Il s’agit d’une base de données interne SQLite. Comme son nom l’indique, SQLite est une base de données de type relationnel avec laquelle le langage SQL s’applique.
Dès lors, les informations pourront être stockées dans une base de données à l’intérieur de tables et réparties dans des colonnes. Cette structure sera plus efficace et plus sûre.
Flutter met à disposition une dépendance qui permet de manipuler la base SQLite : sqflite.
En revanche, sans connaissances en langage SQL, le recours à cet outil peut s’avérer être compliqué.
2. Récupération des dépendances
Pour récupérer le package sqflite, il faut modifier le fichier pubspec.yaml.
dependencies:
path_provider: ^1.6.5
path: ^1.6.4
sqflite: ^1.3.0
Une fois ces trois dépendances ajoutées, il faut terminer en cliquant sur Packages get.
Dans le code ci-dessus, deux autres dépendances sont récupérées en complément. Elles seront indispensables dans le cadre de l’exemple montré peu après.
Path_provider permet de trouver les chemins ou emplacements couramment utilisés sur le système de fichiers.
Path est utile dans les opérations permettant la manipulation de chemins ou d’emplacements.
3. Création de l’application
Cette application est un peu plus complexe que celle réalisée avec les Shared Preferences. Pour établir un comparatif entre les deux méthodes de persistance des données, c’est la même application qui sera reproduite.
a. Classe Personne
Dans le dossier lib, un fichier nommé personne.dart est créé.
L’application s’appuiera donc sur la classe Personne qui y est décrite :
final String tablePersonne = 'personne';
final String colonneId = 'id';
final String colonneNom = 'nom';
final String colonnePrenom = 'prenom';
final String colonneAge = 'age';
class Personne {
int id;
String nom;
String prenom;
String age;
Personne();
Personne.fromMap(Map<String, dynamic> map) {
id = map[colonneId];
nom = map[colonneNom];
prenom = map[colonnePrenom];
age = map[colonneAge];
}
Map<String, dynamic> toMap() {
var map = <String, dynamic>{
colonneNom: nom,
colonnePrenom: prenom,
colonneAge: age
};
if (id != null) {
map[colonneId] = id;
}
return map;
}
}
La classe prend une forme un peu différente que celle présentée dans l’exemple précédent. Dans le fichier, pour simplifier le code présenté, deux classes vont être intégrées. La première est donc Personne. La seconde sera dévoilée juste après. En tout état de cause, ces deux classes vont utiliser des informations communes qui sont placées en début de fichier. Elles correspondent aux éléments SQL comme le nom de la table, celui des colonnes…
La classe Personne est dotée d’un attribut supplémentaire, id, qui servira de clé primaire lors du stockage. Comme cette fois l’objectif va être de stocker des valeurs au sein de colonnes, l’utilisation d’une Map est plus judicieuse. Elle permet d’associer un couple clé/valeur. La clé correspondra au nom de la colonne.
Pour manipuler cette Map, deux méthodes sont prévues. Une première (toMap) servira à la lecture des informations contenues dans les attributs et renverra le tout sous la forme de l’objet map. La seconde (Personne.fromMap) aura pour but d’affecter les valeurs contenues dans l’objet map aux attributs de Personne. Elle agira donc comme un constructeur nommé.
Noter que dans la méthode toMap une condition est posée sur l’id. En effet, au moment où l’on voudra récupérer les informations contenues dans une instance, la valeur pour id ne sera pas toujours renseignée. Elle le sera si les données ont été insérées dans la base SQLite puisqu’elle sera générée automatiquement par celle-ci.
b. Classe PersonneProvider
L’autre classe contenue dans ce fichier va être la pierre angulaire de la manipulation de la base de données. Elle est donc assez technique. Elle est nommée PersonneProvider.
Pour faciliter son utilisation, une variante du design pattern Singleton est mise en place. Cette pratique courante consiste à doter la classe d’un constructeur privé, inaccessible donc de l’extérieur.
PersonneProvider._privateConstructor();
static final PersonneProvider instance =
PersonneProvider._privateConstructor();
Ici, le pattern n’est que partiellement respecté pour simplifier les choses. L’avantage de cette syntaxe est de pouvoir faire s’exécuter le contrôleur directement ici via l’appel à instance. En appelant instance ailleurs, ce qui est possible car la variable est static, le constructeur va s’exécuter. Ainsi, que ce soit ici, dans PersonneProvider, ou dans une autre classe où instance est appelée, c’est la même instance qui sera manipulée.
Au-delà, dans le fichier qui contient les classes Personne et PersonneProvider, plusieurs variables avaient été déclarées de manière globale pour profiter aux deux classes.
Il convient d’en rajouter deux :
final String databaseName = 'PersonneDB.db';
final int databaseVersion = 1;
Il s’agit du nom de la base de données ainsi que son numéro de version.
Maintenant, plusieurs choses restent à prévoir. Il faut, dans un premier temps, initialiser la base de données. Et pour cela, il nous faut une base :
Database _db;
Cette base de données est privée. Elle ne pourra donc pas être appelée en dehors de cette classe. Pour travailler avec, il faut mettre en place un getter qui, outre le fait de pouvoir récupérer la valeur de la variable _db, gérera son initialisation :
Future<Database> get db async {
if (_db != null) {
return _db;
} else {
_db = await _initDatabase();
return _db;
}
}
La condition vérifie si la base est initialisée. Si elle l’est, alors la valeur de _db est retournée. Si elle ne l’est pas, la méthode _initDatabase est exécutée.
Voici cette méthode qui sera expliquée juste après :
_initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, databaseName);
return await openDatabase(path, version: databaseVersion, onCreate: _open);
}
En se servant de path_provider, il est possible de récupérer le chemin de notre application :
Directory documentsDirectory = await getApplicationDocumentsDirectory();
À partir de là, grâce à la fonctionnalité join de path, ce chemin est complété par le nom de la base de données :
String path = join(documentsDirectory.path, databaseName);
Il ne reste plus qu’à exécuter la méthode openDatabase qui prend trois paramètres : le chemin de la base (path), la version de la base de données et enfin une méthode pour sa création (onCreate). Cette dernière méthode est, dans l’exemple, _open.
Future _open(Database db, int version) async {
await db.execute('''
create table $tablePersonne (
$colonneId integer primary key autoincrement,
$colonneNom text,
$colonnePrenom text,
$colonneAge text)
''');
}
La méthode _open crée la table personne dans la base de données. Celle-ci comportera quatre colonnes. La première, pour l’identifiant (id), est la clé primaire et sera auto-incrémentée. Les autres sont simplement du texte.
La configuration est presque terminée. Puisque l’ouverture de la base est réalisée, il faut prévoir l’inverse, c’est-à-dire la fermeture. Une méthode intitulée close est ajoutée :
Future close() async => _db.close();
La base de données est donc opérationnelle à partir de là. Néanmoins, il reste à prévoir les différentes fonctionnalités attendues. Pour l’application qui nous concerne, il faudra que des données puissent être enregistrées. Par la suite, il devra être possible de les lire. Et enfin, il sera nécessaire de prévoir leur suppression.
Pour l’enregistrement des informations, une méthode nommée insert est intégrée :
Future<int> insert(Personne personne) async {
Database db = awaitinstance.db;
int id = await db.insert(tablePersonne, personne.toMap());
return id;
}
Insert est de type Future. Son traitement est évidemment asynchrone. La première étape consiste à appeler le getter de _db grâce à instance. Sur cette instance de Database il devient possible alors d’appeler la méthode insert. Elle attend deux paramètres. Le premier doit indiquer la table qui est concernée par l’insertion. Le second apporte les données à insérer. Ici, ces données sont fournies sous la forme d’une Map. Cela s’avère très pratique puisque les noms de clés correspondent aux noms des colonnes de la table. La clé primaire étant auto-incrémentée, le retour de la méthode insert sera l’id qui prend la forme d’un entier.
La seconde fonctionnalité importante doit permettre de lire les données contenues dans la table. Plus précisément, de lire la dernière donnée enregistrée puisque c’est ce que l’application requiert. Voici cette méthode :
Future<Personne> getPersonne(int id) async {
Database db = awaitinstance.db;
List<Map> maps = await db.query(tablePersonne,
columns: [colonneId, colonneNom, colonnePrenom, colonneAge],
where: '$colonneId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Personne.fromMap(maps.first);
}
return null;
}
Le début ressemble à insert. C’est une méthode asynchrone qui dans un premier temps fait appel au getter de _db. La différence principale réside dans la récupération d’un paramètre. Il s’agit d’un numéro d’identifiant (id) qui va permettre de rechercher parmi l’ensemble des enregistrements contenus dans la table celui qui correspond.
Le retour attendu est une liste d’objets de type Map, chacun d’eux correspondant à une Personne.
La méthode query de Database permet d’indiquer plusieurs paramètres :
-
Le nom de la table où s’effectue la recherche.
-
columns qui attend un tableau des colonnes dont on souhaite remonter les informations.
-
Puis, where offre la possibilité de placer une condition restrictive afin d’éviter de remonter l’ensemble des lignes. Celle-ci indique, par exemple, que le filtrage va avoir lieu sur une colonne précise en lui transmettant une valeur. Ici, la restriction porte sur la colonne id. Mais attention ! La valeur n’est pas précisée directement, mais elle est représentée par un point d’interrogation. Elle est ensuite déportée dans le paramètre whereArgs. Cette syntaxe permet d’éviter une faille de sécurité en empêchant les injections SQL.
Enfin, une condition permet de vérifier qu’il existe bien au moins une correspondance dans la table. Si c’est le cas, Personne.fromMap est utilisée pour construire une Personne à partir du premier résultat obtenu.
Sinon, null est renvoyé.
La dernière fonctionnalité attendue de la base dans notre exemple est de pouvoir effacer une entrée. Une méthode nommée delete est donc créée à cet effet :
Future<int> delete(int id) async {
Database db = awaitinstance.db;
return await db
.delete(tablePersonne, where: '$colonneId = ?', whereArgs: [id]);
}
Encore une fois, il s’agit d’une méthode asynchrone. La première étape consiste toujours à récupérer une instance de Database. Le but est d’appeler sur cette dernière la méthode delete. Les paramètres attendus sont les mêmes que pour query. D’abord, le nom de la table. Puis une clause where avec les arguments de celle-ci dans le paramètre whereArgs. L’argument en question est encore une fois l’id.
Dans la mesure où cette classe est assez complexe, voici, en récapitulatif, son code complet (accompagné de celui de Personne qui se trouve avec) :
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
final String tablePersonne = 'personne';
final String colonneId = 'id';
final String colonneNom = 'nom';
final String colonnePrenom = 'prenom';
final String colonneAge = 'age';
final String databaseName = 'PersonneDB.db';
final int databaseVersion = 1;
class Personne {
int id;
String nom;
String prenom; ...
JSON
Le format JSON est très couramment utilisé de nos jours pour transmettre de l’information.
1. Définition
JSON signifie JavaScript Object Notation.
Il s’agit de données organisées sous un format précis et de manière structurée. Ce format est, à ce titre, un concurrent du XML.
Dans les fichiers JSON, les données sont travaillées autour du principe clé/valeur. Les valeurs peuvent être des objets, des tableaux ou des types plus génériques tels que des nombres, des chaînes de caractères, des booléens…
L’avantage du standard JSON réside dans le fait qu’il est facilement lisible et compréhensible pour un humain puisqu’il contient du texte. Son format permet aux systèmes de l’interpréter également sans problème (des interpréteurs existent pour presque tous les langages). Cette syntaxe (clé/valeur) est plus légère que celle du XML (balise) et les fichiers JSON sont donc moins lourds. Ce qui est important si les données proviennent du réseau.
Voici un exemple de données structurées en JSON :
{
"personne": [
{
"id": "1",
"nom": "DOE",
"prenom": "John"
},
{
"id": "2",
"nom": "DOE",
"prenom": "Jane"
}
]
}
Dans l’exemple ci-dessus, des informations contenant deux personnes sont transmises. Le fichier contient un tableau de personnes. Chacune comporte un id, un nom et un prenom.
2. Récupération des dépendances
En soi, il n’y a pas de dépendances à récupérer pour manipuler JSON avec Dart. En réalisant l’importation...