Les Opérations CRUD avec sqflite dans Flutter
Configuration initiale
Ajoutez les dépendances dans pubspec :
Importez les packages :
dependencies: sqflite: ^2.3.0 path_provider: ^2.1.1 path: ^1.9.0import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart';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 :
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}); }Méthode toMap()
- Convertit l’objet en Map<String, dynamic> pour l’insérer/mettre à jour dans la base de données :
MaptoMap() { return { 'id': id, // Si id est null, la base de données l'auto-génère 'name': name, 'age': age, }; } Factory fromMap()
- Convertit une ligne de la base de données (sous forme de Map) en objet :
- Actuellement,
Mapmap 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 :
factory Student.fromMap(Mapmap) { 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, ); }
Gestion de la base de données
Classe DatabaseHelper
-
1. Utiliser un singleton pour la gestion de la base
- É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. Solution: Utilisation du singleton :
- 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. Solution :
- 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). Solution :
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();Future<Database> get database async { if (_database != null) return _database!; _database = await _initDatabase(); return _database!; }final directory = await getApplicationDocumentsDirectory(); final path = join(directory.path, 'students.db');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 ) '''); }class DatabaseHelper { static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); static Database? _database; DatabaseHelper._privateConstructor(); Futureget 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 ) '''); } }
Opérations CRUD
Insertion (Create)
Lecture (Read)
Mise à jour (Update)
Suppression (Delete)
FutureinsertStudent(Student student) async { Database db = await instance.database; return await db.insert('students', student.toMap()); } Future- > getAllStudents() async {
Database db = await instance.database;
List
FutureupdateStudent(Student student) async { Database db = await instance.database; return await db.update( 'students', student.toMap(), where: 'id = ?', whereArgs: [student.id], ); } FuturedeleteStudent(int id) async { Database db = await instance.database; return await db.delete( 'students', where: 'id = ?', whereArgs: [id], ); } Bonnes pratiques
Fermez la base de données
Gérez les erreurs avec try/catch
@override void dispose() { DatabaseHelper.instance.close(); super.dispose(); }try { await _addStudent(); } catch (e) { print('Erreur: $e'); }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.
lib/ ├── models/ │ └── ..._model.dart ├── views/ │ └── ..._view.dart ├── viewmodels/ │ └── ..._viewmodel.dart ├── services/ │ └── ..._service.dart └── main.dart - 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
- Organiser le code source selon MVVM
-
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. ? 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(); } } } -
Bonnes Pratiques pour la View
- La Vue (UI) affiche les données du ViewModel et envoie les interactions utilisateur. ? 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!), ), ); }, ); }, ), ), ); } } -
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.
Future<int> addEtudiant(Etudiant etudiant) async { Database db = await database; return await db.insert(_tableName, etudiant.toMap()); } - 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.
Future<void> addEtudiant(Etudiant etudiant) async { await _dbHelper.addEtudiant(etudiant); await loadEtudiants(); // Recharge la liste des étudiants après ajout }
- Méthode du service (addEtudiant dans le service)
- La différence entre ces deux méthodes réside dans leur responsabilité et leur contexte d'exécution :