Les opérations CRUD avec sqflite dans Flutter
Les Opérations CRUD avec sqflite
dans Flutter
-
Configuration initiale
-
Ajoutez les dépendances dans pubspec :
-
Importez les packages :
-
Modèle de données
-
Déclaration des propriétés
- Représentez les colonnes de la table de la base de données comme des propriétés de la classe :
-
Méthode toMap()
- Convertit l’objet en Map<String, dynamic> pour l’insérer/mettre à jour dans la base de données :
-
Factory fromMap()
- Convertit une ligne de la base de données (sous forme de Map) en objet :
- Actuellement,
Map
map est trop générique et peut provoquer des erreurs si la conversion de type échoue. Il est recommandé de toujours spécifier le type des clés et des valeurs : - Cela garantit que map contient bien des chaînes comme clés et des valeurs dynamiques.
- Avant d’accéder aux valeurs du map, il est préférable de vérifier que toutes les clés attendues existent, afin d’éviter une exception si une clé est absente :
- Si une valeur attendue est null, cela peut poser problème. Voici une approche pour gérer ce cas :
- Une erreur de conversion peut survenir si les types ne correspondent pas. Un try/catch peut aider à éviter des plantages de l’application :
- Parfois, les bases de données stockent des entiers sous forme de String. On peut gérer ces cas en convertissant les types :
-
Gestion de la base de données
-
Classe DatabaseHelper
- Éviter d’ouvrir plusieurs instances de la base de données en utilisant une seule instance de DatabaseHelper.
- Cela permet d’éviter les problèmes de performance et d’intégrité des données.
- Règle : Si _database n’est pas null, il faut retourner l’instance existante.
- Cela évite d’ouvrir la base plusieurs fois, ce qui pourrait ralentir l’application.
- Problème : Flutter ne permet pas l’accès direct au système de fichiers.
- Solution : Stocker la base dans getApplicationDocumentsDirectory(), un dossier sûr.
- Règle : Toujours utiliser AUTOINCREMENT pour la clé primaire.
- Règle : Ne pas oublier les contraintes (NOT NULL).
-
Opérations CRUD
-
Insertion (Create)
-
Lecture (Read)
-
Mise à jour (Update)
-
Suppression (Delete)
-
Bonnes pratiques
-
Fermez la base de données
-
Gérez les erreurs avec try/catch
-
Mettre en Place MVVM dans Flutter
- Organiser le code source selon MVVM
- Une structure de dossier typique pour MVVM dans un projet Flutter pourrait ressembler à ceci:
- models/ : Contient les classes définissant les données.
- views/ : Contient les widgets Flutter responsables de l’affichage à l’écran.
- viewmodels/ : Contient la logique liée à la manière dont les informations sont présentées à l’utilisateur.
- services/ : (Optionnel) Utilisé pour les logiques métier externes, comme les appels API.
- Cette organisation claire facilite la navigation dans votre code et permet à chaque composant d’avoir un objectif précis.
- Conventions de Nommage
- Modèles : Les fichiers qui contiennent des classes de modèle doivent être nommés avec le suffixe _model.
- Par exemple, pour un modèle représentant un utilisateur, le fichier serait nommé :user_model.dart
- Vues : Les fichiers qui contiennent des widgets responsables de l’affichage doivent être nommés avec le suffixe _view. Par exemple : user_view.dart
- ViewModels : Les fichiers qui contiennent la logique de présentation doivent être nommés avec le suffixe _view_model. Par exemple : user_view_model.dart
- Services : Les fichiers qui contiennent des services ou des logiques métier externes peuvent être nommés simplement selon leur fonction, sans suffixe spécifique, ou avec un suffixe comme _service. Par exemple :user_service.dart
-
Bonnes Pratiques pour ViewModel
- Le ViewModel sert de pont entre la base de données et la vue. Il contient la logique métier et notifie la vue des changements.
-
Bonnes Pratiques pour la View
- La Vue (UI) affiche les données du ViewModel et envoie les interactions utilisateur.
-
Les différences entre les deux méthodes add..
- La différence entre ces deux méthodes réside dans leur responsabilité et leur contexte d’exécution :
- Méthode du service (addEtudiant dans le service)
- Cette méthode appartient au service (généralement une classe qui interagit directement avec la base de données, comme un DatabaseHelper).
- Son rôle est uniquement d’insérer un étudiant dans la base de données.
- Elle retourne un Future
correspondant à l’ID de l’étudiant inséré. - Elle ne s’occupe pas de la mise à jour de l’interface utilisateur.
- Méthode du ViewModel (addEtudiant dans le ViewModel)
- Cette méthode appartient au ViewModel, qui fait le lien entre les données et l’interface utilisateur.
- Elle appelle la méthode du service (_dbHelper.addEtudiant(etudiant)) pour insérer l’étudiant.
- Ensuite, elle appelle loadEtudiants() pour mettre à jour la liste des étudiants affichée dans l’UI après l’ajout.
- Elle ne retourne rien (Future<void>), car son but principal est d’effectuer une action et non de retourner une valeur.
dependencies:
sqflite: ^2.3.0
path_provider: ^2.1.1
path: ^1.9.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
class Student {
final int? id; // Clé primaire (nullable pour l'auto-increment)
final String name; // Colonne "name"
final int age; // Colonne "age"
Student({this.id, required this.name, required this.age});
}
Map toMap() {
return {
'id': id, // Si id est null, la base de données l'auto-génère
'name': name,
'age': age,
};
}
factory Student.fromMap(Map map) {
return Student(
id: map['id'] as int?, // Conversion explicite des types
name: map['name'] as String,
age: map['age'] as int,
);
}
factory Student.fromMap(Map<String, dynamic> map) {
factory Student.fromMap(Map<String, dynamic> map) {
if (!map.containsKey('id') || !map.containsKey('name') || !map.containsKey('age')) {
throw ArgumentError('Les clés requises sont manquantes dans le map.');
}
return Student(
id: map['id'] as int?,
name: map['name'] as String,
age: map['age'] as int,
);
}
factory Student.fromMap(Map<String, dynamic> map) {
return Student(
id: map['id'] as int?,
name: map['name'] as String? ?? 'Nom inconnu', // Valeur par défaut si null
age: map['age'] as int? ?? 0, // Valeur par défaut pour éviter les erreurs
);
}
factory Student.fromMap(Map<String, dynamic> map) {
try {
return Student(
id: map['id'] as int?,
name: map['name'] as String,
age: map['age'] as int,
);
} catch (e) {
throw ArgumentError('Erreur lors de la conversion du map en Student : $e');
}
}
factory Student.fromMap(Map<String, dynamic> map) {
return Student(
id: map['id'] is int ? map['id'] as int : int.tryParse(map['id'].toString()),
name: map['name'].toString(),
age: map['age'] is int ? map['age'] as int : int.tryParse(map['age'].toString()) ?? 0,
);
}
-
1. Utiliser un singleton pour la gestion de la base
Solution : Utilisation du singleton :
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
2. Toujours vérifier si la base de données est déjà ouverte
Solution :
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
3. Stocker la base de données dans un dossier accessible
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'students.db');
4. Définir et créer les tables proprement
Solution :
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE students(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL
)
''');
}
5. Code complet
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
DatabaseHelper._privateConstructor();
Future get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future _initDatabase() async {
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'students.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE students(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL
)
''');
}
}
Future insertStudent(Student student) async {
Database db = await instance.database;
return await db.insert('students', student.toMap());
}
Future> getAllStudents() async {
Database db = await instance.database;
List
Future updateStudent(Student student) async {
Database db = await instance.database;
return await db.update(
'students',
student.toMap(),
where: 'id = ?',
whereArgs: [student.id],
);
}
Future deleteStudent(int id) async {
Database db = await instance.database;
return await db.delete(
'students',
where: 'id = ?',
whereArgs: [id],
);
}
@override
void dispose() {
DatabaseHelper.instance.close();
super.dispose();
}
try {
await _addStudent();
} catch (e) {
print('Erreur: $e');
}
lib/
├── models/
│ └── ..._model.dart
├── views/
│ └── ..._view.dart
├── viewmodels/
│ └── ..._viewmodel.dart
├── services/
│ └── ..._service.dart
└── main.dart
💡 Règles à respecter pour un ViewModel propre :
-
✔ Séparer la logique de la vue → Aucune interaction directe avec BuildContext.
✔ Utiliser ChangeNotifier pour notifier les mises à jour UI.
✔ Éviter d’accéder directement à la base de données dans la vue.
✔ Gérer les erreurs et les états (isLoading, hasError, etc.).
import 'package:flutter/material.dart';
import '../models/student.dart';
import '../database/database_helper.dart';
class StudentViewModel extends ChangeNotifier {
List _students = [];
bool _isLoading = false;
String? _errorMessage;
List get students => _students;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// Lire tous les étudiants
Future fetchStudents() async {
_isLoading = true;
notifyListeners();
try {
_students = await DatabaseHelper.instance.getAllStudents();
_errorMessage = null;
} catch (e) {
_errorMessage = "Erreur de chargement des étudiants";
}
_isLoading = false;
notifyListeners();
}
// Ajouter un étudiant
Future addStudent(Student student) async {
try {
await DatabaseHelper.instance.insertStudent(student);
fetchStudents(); // Recharger la liste
} catch (e) {
_errorMessage = "Impossible d'ajouter l'étudiant";
notifyListeners();
}
}
// Modifier un étudiant
Future updateStudent(Student student) async {
try {
await DatabaseHelper.instance.updateStudent(student);
fetchStudents();
} catch (e) {
_errorMessage = "Erreur lors de la mise à jour";
notifyListeners();
}
}
// Supprimer un étudiant
Future deleteStudent(int id) async {
try {
await DatabaseHelper.instance.deleteStudent(id);
fetchStudents();
} catch (e) {
_errorMessage = "Erreur lors de la suppression";
notifyListeners();
}
}
}
💡 Règles à respecter pour une View propre :
-
✔ Ne pas inclure de logique métier dans la vue.
✔ Utiliser ChangeNotifierProvider pour la gestion d’état.
✔ Optimiser le rendu avec Consumer ou Selector.
✔ Afficher correctement les erreurs et états de chargement.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/student_viewmodel.dart';
import '../models/student.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => StudentViewModel()..fetchStudents(),
child: Scaffold(
appBar: AppBar(title: Text('Liste des Étudiants')),
body: Consumer(
builder: (context, studentVM, child) {
if (studentVM.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (studentVM.errorMessage != null) {
return Center(child: Text(studentVM.errorMessage!));
}
if (studentVM.students.isEmpty) {
return Center(child: Text("Aucun étudiant trouvé."));
}
return ListView.builder(
itemCount: studentVM.students.length,
itemBuilder: (context, index) {
final student = studentVM.students[index];
return ListTile(
title: Text(student.name),
subtitle: Text(student.email),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => studentVM.deleteStudent(student.id!),
),
);
},
);
},
),
),
);
}
}
Future<int> addEtudiant(Etudiant etudiant) async {
Database db = await database;
return await db.insert(_tableName, etudiant.toMap());
}
Future<void> addEtudiant(Etudiant etudiant) async {
await _dbHelper.addEtudiant(etudiant);
await loadEtudiants(); // Recharge la liste des étudiants après ajout
}