Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Les opérations CRUD avec sqflite dans Flutter

Un cours complet pour maîtriser Les opérations CRUD avec sqflite dans Flutter.

0 chapitres 0 QCM Accès gratuit

Les Opérations CRUD avec sqflite dans Flutter

  1. Configuration initiale

    1. Ajoutez les dépendances dans pubspec :
    2. dependencies:
        sqflite: ^2.3.0
        path_provider: ^2.1.1
        path: ^1.9.0
    3. Importez les packages :
    4. import 'package:sqflite/sqflite.dart';
      import 'package:path/path.dart';
      import 'package:path_provider/path_provider.dart';
  2. Modèle de données

    1. 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});
        }
    2. Méthode toMap()
      • Convertit l’objet en Map<String, dynamic> pour l’insérer/mettre à jour dans la base de données :
      • Map toMap() {
          return {
            'id': id,     // Si id est null, la base de données l'auto-génère
            'name': name,
            'age': age,
          };
        }
    3. Factory fromMap()
      • Convertit une ligne de la base de données (sous forme de Map) en objet :
      • 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,
          );
        }
      • 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 :
      • factory Student.fromMap(Map<String, dynamic> map) {
      • 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 :
      • 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,
          );
        }
      • Si une valeur attendue est null, cela peut poser problème. Voici une approche pour gérer ce cas :
      • 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
          );
        }
      • Une erreur de conversion peut survenir si les types ne correspondent pas. Un try/catch peut aider à éviter des plantages de l'application :
      • 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');
          }
        }
      • 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(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,
          );
        }
  3. Gestion de la base de données

    1. 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 :
          static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
        2. Toujours vérifier si la base de données est déjà ouverte
        • 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 :
          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
        • 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.
        • final directory = await getApplicationDocumentsDirectory();
          final path = join(directory.path, 'students.db');
        4. Définir et créer les tables proprement
        • Règle: Toujours utiliser AUTOINCREMENT pour la clé primaire.
        • Règle: Ne pas oublier les contraintes (NOT NULL).
        • 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
                )
              ''');
            }
          }
  4. Opérations CRUD

    1. Insertion (Create)
    2. Future insertStudent(Student student) async {
        Database db = await instance.database;
        return await db.insert('students', student.toMap());
      }
    3. Lecture (Read)
    4. Future> getAllStudents() async {
        Database db = await instance.database;
        List> maps = await db.query('students');
        return List.generate(maps.length, (i) => Student.fromMap(maps[i]));
      }
    5. Mise à jour (Update)
    6. Future updateStudent(Student student) async {
        Database db = await instance.database;
        return await db.update(
          'students',
          student.toMap(),
          where: 'id = ?',
          whereArgs: [student.id],
        );
      }
    7. Suppression (Delete)
    8. Future deleteStudent(int id) async {
        Database db = await instance.database;
        return await db.delete(
          'students',
          where: 'id = ?',
          whereArgs: [id],
        );
      }
  5. Bonnes pratiques

    1. Fermez la base de données
    2. @override
      void dispose() {
        DatabaseHelper.instance.close();
        super.dispose();
      }
    3. Gérez les erreurs avec try/catch
    4. try {
        await _addStudent();
      } catch (e) {
        print('Erreur: $e');
      }
  6. 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:
      • lib/
          ├── models/
          │   └── ..._model.dart
          ├── views/
          │   └── ..._view.dart
          ├── viewmodels/
          │   └── ..._viewmodel.dart
          ├── services/
          │   └── ..._service.dart
          └── main.dart
        
        • 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
  7. 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();
          }
        }
      }
      
  8. 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!),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          );
        }
      }
      
  9. 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)
          Future<int> addEtudiant(Etudiant etudiant) async {
            Database db = await database;
            return await db.insert(_tableName, etudiant.toMap());
          }
          • 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)
          Future<void> addEtudiant(Etudiant etudiant) async {
            await _dbHelper.addEtudiant(etudiant);
            await loadEtudiants();  // Recharge la liste des étudiants après ajout
          }
        • 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.