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

Comment utiliser SQLite dans Flutter

Un cours complet pour maîtriser Comment utiliser SQLite dans Flutter.

0 chapitres 0 QCM Accès gratuit
Dans cet article

SQLite dans Flutter

  1. Objectifs du cours

    • À la fin de ce cours, vous serez capable de :
      • expliquer ce qu'est SQLite et pourquoi l'utiliser dans une application mobile ;
      • installer DB Browser for SQLite et l'utiliser pour créer, remplir et interroger une base à la main ;
      • ajouter les packages sqflite et path à un projet Flutter ;
      • écrire une classe modèle avec ses méthodes toMap() et fromMap(), en respectant la null safety de Dart ;
      • créer une classe DatabaseService (patron Singleton) qui ouvre la base et crée les tables ;
      • insérer, lire, modifier et supprimer des données depuis Flutter ;
      • ouvrir dans DB Browser la base créée par l'application pour vérifier son contenu.
    • Prérequis : les bases de Dart (classes, Future, async / await) et la leçon Flutter – Concepts de base de données.
    • Durée estimée : 3 heures.
  2. Présentation de SQLite

    • SQLite est une base de données relationnelle libre et très légère. Contrairement à MySQL, elle ne nécessite aucun serveur: toute la base (tables, données, index) est enregistrée dans un seul fichier (par exemple people.db) stocké sur l'appareil.
    • Elle est intégrée à Android et à iOS : c'est la solution classique pour conserver des données structurées hors connexion (liste de courses, carnet de notes, panier, historique…) et les retrouver après la fermeture de l'application.
    • On la manipule avec le langage SQL: CREATE TABLE, INSERT, SELECT, UPDATE, DELETE.
    • Dans Flutter, on utilise le package sqflite. Dans ce cours, nous construisons pas à pas la gestion d'une liste de personnes (Person), puis vous réaliserez la même chose pour des étudiants (Student).
Plateforme SQLite avec sqflite
Android, iOS, macOS Fonctionne directement.
Windows, Linux Fonctionne avec le package complémentaire sqflite_common_ffi (voir la section 5).
Web (Chrome) Ne fonctionne pas. Ne lancez pas ces exemples dans Chrome.
  1. Rappel : classes et objets en Dart

    • Une classe Dart ressemble beaucoup à une classe Java :
      • déclaration avec le mot-clé class;
      • attributs typés : String, int, double, bool… souvent déclarés final s'ils ne changent plus ;
      • constructeur portant le nom de la classe, avec la forme courte this.attribut pour affecter les paramètres ;
      • instanciation en appelant le constructeur. Le mot-clé new est facultatif en Dart : on ne l'écrit plus.
Notions de classes et POO
  • Exemple : la classe Personne possède deux attributs, name (String) et age (int), initialisés par le constructeur.
// Définition de la classe Personne
class Personne {
  // Attributs de la classe
  final String name;
  final int age;
  // Constructeur : affecte directement les paramètres aux attributs
  Personne(this.name, this.age);
}
void main() {
  final pers = Personne('Mohamed', 23);   // pas besoin de new
  print('${pers.name} est âgé de ${pers.age} ans !');
  // Affiche : Mohamed est âgé de 23 ans !
}
  1. Installer et découvrir DB Browser for SQLite

    • DB Browser for SQLite est un logiciel gratuit qui permet d'ouvrir un fichier .db, de voir ses tables comme dans un tableur et d'exécuter des requêtes SQL. Il vous servira tout au long du chapitre à tester vos requêtes avant de les écrire en Dart, puis à vérifier ce que votre application a réellement enregistré.
    • 4.1. Installation
      1. Aller sur sqlitebrowser.org/dl.
      2. Télécharger la version adaptée à votre système :
        • Windows : Standard installer for 64-bit Windows, puis lancer l'installateur et garder les options par défaut. Sur un poste sans droits administrateur (salle de cours), prendre la version PortableApp: elle fonctionne sans installation ;
        • macOS : le fichier .dmg, puis glisser l'application dans le dossier Applications (ou brew install --cask db-browser-for-sqlite) ;
        • Linux (Debian / Ubuntu) : sudo apt install sqlitebrowser.
      3. Lancer DB Browser for SQLite. Pour l'avoir en français : Edit → Preferences → General → Language, choisir Français, puis relancer le logiciel.
    • 4.2. Les quatre onglets à connaître
Onglet (français) Onglet (anglais) À quoi il sert
Structure de la base de données Database Structure Voir les tables, leurs colonnes et leur instruction CREATE TABLE.
Parcourir les données Browse Data Voir et modifier les lignes d'une table, comme dans un tableur.
Éditer les pragmas Edit Pragmas Réglages de la base, dont la version du schéma (User Version).
Exécuter le SQL Execute SQL Écrire et tester des requêtes SQL.
Important : DB Browser n'enregistre rien dans le fichier tant que vous n'avez pas cliqué sur Écrire les modifications (Ctrl + S). Le bouton Annuler les modifications revient au dernier enregistrement : pratique pour tester une requête sans risque.
  • 4.3. Premier essai : créer la base à la main
    1. Cliquer sur Nouvelle base de données et enregistrer le fichier sous le nom essai.db. Dans la fenêtre « Éditer la définition de la table » qui s'ouvre, cliquer sur Annuler: on va créer la table en SQL.
    2. Onglet Exécuter le SQL: coller le script ci-dessous et l'exécuter avec le bouton ▶ (ou Ctrl + Entrée).
    3. Cliquer sur Écrire les modifications, puis afficher la table dans l'onglet Parcourir les données.
CREATE TABLE persons (
  id   INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT    NOT NULL,
  age  INTEGER NOT NULL
);
INSERT INTO persons (name, age) VALUES ('Mohamed', 23), ('Sarra', 21), ('Youssef', 25);
SELECT * FROM persons WHERE age > 22 ORDER BY name;
Point de contrôle : la requête SELECT renvoie 2 lignes (Mohamed et Youssef) et la table persons apparaît dans l'onglet Structure de la base de données. C'est exactement cette table que l'application Flutter créera à la section 7.
  1. Ajouter les packages SQLite au projet Flutter

    • Étape 1 : ajouter sqflite et path
      • Dans le terminal, à la racine du projet, exécuter :
flutter pub add sqflite path

Cette commande ajoute les deux packages dans pubspec.yaml, avec leur dernière version, puis exécute automatiquement flutter pub get.

# pubspec.yaml (extrait, versions indicatives)
dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.4.2
  path: ^1.9.1
Package Rôle
sqflite Ouvre la base SQLite et exécute les requêtes (insert, query, update, delete…). Toutes ses méthodes sont asynchrones.
path Fournit join() pour construire le chemin du fichier .db correctement sur tous les systèmes.
path_provider (facultatif) Donne d'autres dossiers de l'appareil (documents, cache). Il n'est pas nécessaire pour SQLite : sqflite fournit déjà getDatabasesPath().
  • Pour mettre à jour plus tard tous les packages vers leur dernière version majeure :
flutter pub upgrade --major-versions
  • Étape 2 (Windows et Linux uniquement) : activer SQLite sur le bureau
    • Si vous lancez l'application en mode bureau (flutter run -d windows), ajoutez sqflite_common_ffi et initialisez-le dans main():
// flutter pub add sqflite_common_ffi
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) {
    sqfliteFfiInit();
    databaseFactory = databaseFactoryFfi;
  }
  runApp(const MyApp());
}
  • Étape 3 : importer les bibliothèques dans le fichier Dart
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
    • import 'package:path/path.dart'; donne accès à la fonction join(), qui assemble un dossier et un nom de fichier avec le bon séparateur (/ ou \).
    • import 'package:sqflite/sqflite.dart'; donne accès à openDatabase(), getDatabasesPath(), à la classe Database et à toutes les méthodes de requête.
    • Il n'est plus nécessaire d'importer dart:async: les types Future et Stream sont disponibles par défaut en Dart.
  • Créer une classe modèle (model class)

    • Une classe modèle représente une ligne d'une table : chaque attribut de la classe correspond à une colonne de la table. Elle sert d'intermédiaire entre vos objets Dart et la base.
    • Pour créer une classe modèle :
      1. identifier les colonnes de la table et leur type ;
      2. créer la classe avec un attribut par colonne et un constructeur ;
      3. ajouter toMap() pour transformer un objet en Map avant de l'enregistrer ;
      4. ajouter fromMap() pour recréer un objet à partir d'une ligne lue dans la base.
    • Pourquoi des Map? Parce que sqflite ne connaît pas vos classes : il reçoit et renvoie chaque ligne sous la forme d'une Map<String, dynamic> dont les clés sont les noms des colonnes.
    Méthodes toMap et fromMap
    • 6.1. La méthode toMap(): de l'objet vers la base
      • Elle convertit l'objet en Map: chaque attribut devient une paire nom de colonne → valeur. C'est cette Map que l'on passe à db.insert() ou db.update().
    • 6.2. Le constructeur fromMap(): de la base vers l'objet
      • Il fait l'inverse : il lit les valeurs de la Map renvoyée par db.query() et crée un objet. On l'écrit en général avec un constructeur factory (voir la leçon Comprendre le concept du mot-clé « factory »).
    • 6.3. Exemple complet : la classe Person
    class Person {
      final int? id;      // null tant que la personne n'est pas enregistrée (AUTOINCREMENT)
      final String name;  // colonne name
      final int age;      // colonne age
      const Person({this.id, required this.name, required this.age});
      // Objet -> Map : utilisé pour insert() et update()
      Map<String, dynamic> toMap() {
        return {
          if (id != null) 'id': id,   // on laisse SQLite générer l'id d'une nouvelle personne
          'name': name,
          'age': age,
        };
      }
      // Map -> objet : utilisé après query()
      factory Person.fromMap(Map<String, dynamic> map) {
        return Person(
          id: map['id'] as int?,
          name: map['name'] as String,
          age: map['age'] as int,
        );
      }
      @override
      String toString() => 'Person(id: $id, name: $name, age: $age)';
    }
    void main() {
      const person = Person(name: 'John', age: 30);
      final personMap = person.toMap();
      print('Person as Map: $personMap');          // {name: John, age: 30}
      final copie = Person.fromMap({'id': 1, 'name': 'John', 'age': 30});
      print('Person from Map: $copie');            // Person(id: 1, name: John, age: 30)
    }
    Pourquoi int? id? Avec AUTOINCREMENT, c'est SQLite qui choisit l'identifiant au moment de l'insertion. Avant l'enregistrement, l'objet n'a donc pas encore d'id: il vaut null. C'est aussi pour cela que toMap() n'ajoute la clé id que si elle existe.
    Erreur fréquente : les clés de la Map doivent être exactement les noms des colonnes. Une clé 'Name' au lieu de 'name' provoque l'erreur table persons has no column named Name.
    • 6.4. Le type dynamic dans Map<String, dynamic>
      • Les clés sont toujours des String (les noms des colonnes), mais les valeurs ont des types différents d'une colonne à l'autre : texte, entier, décimal… dynamic permet de les ranger dans la même Map.
      • Exemple :
    Map<String, dynamic> person = {
      'name': 'John Doe',     // String
      'age': 30,              // int
      'isEmployed': true,     // bool
      'height': 1.82,         // double
      'favorites': ['pizza', 'movies', 'coding'],  // List<String>
    };
    Comme dynamic désactive le contrôle des types, on écrit map['age'] as int dans fromMap(): si la valeur n'est pas du bon type, l'erreur apparaît tout de suite, au bon endroit.
    • 6.5. Les types que SQLite ne connaît pas
      • SQLite ne stocke que des entiers (INTEGER), des décimaux (REAL), du texte (TEXT) et des données binaires (BLOB). Les autres types Dart doivent être convertis dans toMap() et fromMap():
    Type Dart Colonne SQLite Dans toMap() Dans fromMap()
    bool INTEGER (0 ou 1) 'actif': actif ? 1 : 0 actif: map['actif'] == 1
    DateTime TEXT (date ISO 8601) 'date': date.toIso8601String() date: DateTime.parse(map['date'] as String)
    double REAL 'prix': prix prix: (map['prix'] as num).toDouble() (un prix de 5 peut revenir en int)
    List, objets TEXT (JSON) ou une autre table jsonEncode(liste) jsonDecode(map['liste'])
    • Activité 01 : écrire toMap()
      • Soit le constructeur fromMap suivant :
    factory Book.fromMap(Map map) {
      return Book(
        title: map['title'],
        author: map['author'],
        pageCount: map['pageCount'],
      );
    }
      • Créer la classe modèle Book avec les attributs nécessaires et leur type.
      • Écrire la méthode toMap() correspondante.
      • Améliorer ce fromMap: préciser le type de la Map (Map<String, dynamic>) et convertir chaque valeur avec as. Expliquer en une phrase ce que l'on gagne.
    Bonne pratique : dans Book.fromMap(Map map), le type des clés et des valeurs n'est pas précisé : la méthode accepte n'importe quoi et les erreurs n'apparaissent qu'à l'exécution. Écrivez toujours Map<String, dynamic> dans toMap() et fromMap(): le code est plus clair et l'éditeur peut vous aider.
    • Activité 02 : la classe Product
      1. Définir une classe modèle Product avec les attributs id (int?), name (String), price (double) et description (String?).
      2. Écrire un constructeur avec des paramètres nommés.
      3. Écrire toMap() et le constructeur factory Product.fromMap(), en convertissant correctement le prix.
      4. Dans main(), créer deux produits, les convertir en Map, les recréer avec fromMap() et afficher le résultat.
    • Activité 03 : gérer un bool et une date
      1. Créer une classe Tache avec les attributs id (int?), titre (String), terminee (bool) et echeance (DateTime).
      2. Écrire toMap() et fromMap() en appliquant les conversions du tableau 6.5.
      3. Dans DB Browser, écrire le CREATE TABLE taches correspondant, insérer deux tâches à la main et vérifier comment sont stockées les colonnes terminee et echeance.
    • 6.6. Un autre exemple : la classe Movie
      • Pour une table movies (identifiant, titre, année, langue), la classe modèle s'écrit sur le même principe :
    class Movie {
      final int? id;          // colonne id INTEGER PRIMARY KEY AUTOINCREMENT
      final String title;     // colonne title TEXT
      final int year;         // colonne year INTEGER
      final String language;  // colonne language TEXT
      const Movie({this.id, required this.title, required this.year, required this.language});
      Map<String, dynamic> toMap() => {
            if (id != null) 'id': id,
            'title': title,
            'year': year,
            'language': language,
          };
      factory Movie.fromMap(Map<String, dynamic> map) => Movie(
            id: map['id'] as int?,
            title: map['title'] as String,
            year: map['year'] as int,
            language: map['language'] as String,
          );
    }
    fromMap ou fromJson? Vous rencontrerez aussi fromJson() et toJson(): le principe est identique. On réserve en général fromJson / toJson aux données venant d'une API ou d'un fichier JSON, et fromMap / toMap aux lignes d'une base de données. Le type de l'id doit être le même dans la classe et dans la table (ici int et INTEGER).
    1. Créer la base de données et les tables

      • Pour utiliser SQLite, il faut ouvrir le fichier de la base avec openDatabase(). S'il n'existe pas encore, il est créé et la fonction onCreate est appelée pour créer les tables. On regroupe tout ce code dans une classe unique, DatabaseService, rangée dans lib/services/database_service.dart.
      • 7.1. Une seule instance : le patron Singleton
        • Un Singleton est une classe qui n'a qu'une seule instance dans toute l'application : chaque appel à DatabaseService() renvoie le même objet, donc la même connexion à la base.
        • Ce n'est pas obligatoire, mais c'est la bonne pratique pour une base de données :
          • gestion centralisée : tout le code d'accès à la base est au même endroit ;
          • pas de connexions multiples : la base n'est ouverte qu'une fois, ce qui évite les conflits et le gaspillage de ressources ;
          • maintenance facilitée : une modification de la base n'a qu'un seul point d'impact.
    // L'unique instance, créée une seule fois
    static final DatabaseService _instance = DatabaseService._internal();
    // Constructeur factory : DatabaseService() renvoie toujours _instance
    factory DatabaseService() => _instance;
    // Constructeur privé : impossible de créer une autre instance depuis l'extérieur
    DatabaseService._internal();
    // La connexion à la base, créée au premier besoin
    static Database? _database;
    • 7.2. Ouvrir la base au premier besoin
      • Le getter database ouvre la base au premier appel, puis renvoie toujours la même connexion. Il renvoie une Future, car l'ouverture d'un fichier prend du temps et ne doit pas bloquer l'affichage (voir Les fonctions asynchrones en Dart).
    Future<Database> get database async {
      _database ??= await _initDatabase();   // ouvre la base seulement si elle ne l'est pas déjà
      return _database!;
    }
    Future<Database> _initDatabase() async {
      // Dossier prévu par le système pour les bases de l'application
      final path = join(await getDatabasesPath(), 'people.db');
      debugPrint('Base SQLite : $path');
      return openDatabase(
        path,
        version: 1,
        onCreate: _createTables,
      );
    }
    • 7.3. Les principaux paramètres de openDatabase()
    Paramètre Rôle
    path Chemin du fichier .db, construit avec join(await getDatabasesPath(), 'nom.db').
    version Numéro de version du schéma. Il faut l'augmenter quand on modifie la structure des tables.
    onConfigure Appelé à chaque ouverture, en premier. Sert par exemple à activer les clés étrangères : PRAGMA foreign_keys = ON.
    onCreate Appelé une seule fois, quand le fichier n'existe pas : on y crée les tables.
    onUpgrade Appelé quand version augmente : on y modifie les tables sans perdre les données (ALTER TABLE).
    onDowngrade Appelé quand version diminue (rare).
    onOpen Appelé à chaque ouverture, une fois la base prête.
    readOnly Ouvre la base en lecture seule.
    singleInstance Vrai par défaut : deux appels avec le même chemin renvoient la même connexion.
    • 7.4. Créer les tables dans onCreate
    Future<void> _createTables(Database db, int version) async {
      await db.execute('''
        CREATE TABLE persons (
          id   INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT    NOT NULL,
          age  INTEGER NOT NULL
        )
      ''');
      debugPrint('onCreate : table persons créée');
    }
    Attention au SQL dans les chaînes Dart : un commentaire Dart (// ...) écrit à l'intérieur de la chaîne '''...''' est envoyé tel quel à SQLite et provoque une erreur de syntaxe, tout comme une virgule après la dernière colonne. Testez toujours votre CREATE TABLE dans DB Browser avant de le copier dans le code.
    • 7.5. Le fichier complet database_service.dart
      • Voici la classe complète pour la table persons, avec les quatre opérations CRUD. Chaque méthode récupère la connexion avec await database, puis utilise toMap() ou fromMap().
    import 'package:flutter/foundation.dart';
    import 'package:path/path.dart';
    import 'package:sqflite/sqflite.dart';
    import '../models/person.dart';
    class DatabaseService {
      // ---------- Singleton ----------
      static final DatabaseService _instance = DatabaseService._internal();
      factory DatabaseService() => _instance;
      DatabaseService._internal();
      static Database? _database;
      Future<Database> get database async {
        _database ??= await _initDatabase();
        return _database!;
      }
      // ---------- Ouverture et création ----------
      Future<Database> _initDatabase() async {
        final path = join(await getDatabasesPath(), 'people.db');
        debugPrint('Base SQLite : $path');
        return openDatabase(path, version: 1, onCreate: _createTables);
      }
      Future<void> _createTables(Database db, int version) async {
        await db.execute('''
          CREATE TABLE persons (
            id   INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT    NOT NULL,
            age  INTEGER NOT NULL
          )
        ''');
      }
      // ---------- Create ----------
      Future<int> addPerson(Person person) async {
        final db = await database;
        return db.insert('persons', person.toMap());        // renvoie l'id créé
      }
      // ---------- Read ----------
      Future<List<Person>> getAllPersons() async {
        final db = await database;
        final rows = await db.query('persons', orderBy: 'name');
        return rows.map((row) => Person.fromMap(row)).toList();
      }
      Future<Person?> getPerson(int id) async {
        final db = await database;
        final rows = await db.query('persons', where: 'id = ?', whereArgs: [id], limit: 1);
        return rows.isEmpty ? null : Person.fromMap(rows.first);
      }
      // ---------- Update ----------
      Future<int> updatePerson(Person person) async {
        final db = await database;
        return db.update('persons', person.toMap(),
            where: 'id = ?', whereArgs: [person.id]);        // renvoie le nombre de lignes modifiées
      }
      // ---------- Delete ----------
      Future<int> deletePerson(int id) async {
        final db = await database;
        return db.delete('persons', where: 'id = ?', whereArgs: [id]);
      }
    }
    Pourquoi where: 'id = ?' et whereArgs? Le ? est remplacé de façon sûre par la valeur de whereArgs. Ne collez jamais une valeur directement dans le texte de la requête ("id = $id") : c'est la porte ouverte aux erreurs et aux injections SQL. Les opérations CRUD sont détaillées dans la leçon suivante, Comment gérer les opérations sur une base de données SQLite dans Flutter.
    1. Utiliser la base depuis main.dart

      • Avant d'utiliser SQLite au démarrage, Flutter doit être initialisé : on appelle WidgetsFlutterBinding.ensureInitialized() au début de main().
      • Le petit programme suivant insère deux personnes, les relit, en modifie une, en supprime une, et affiche le résultat dans la console (Debug Console de VS Code) :
    import 'package:flutter/material.dart';
    import 'models/person.dart';
    import 'services/database_service.dart';
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      final db = DatabaseService();
      // Create
      final idSarra = await db.addPerson(const Person(name: 'Sarra', age: 21));
      await db.addPerson(const Person(name: 'Youssef', age: 25));
      debugPrint('Sarra enregistrée avec l\'id $idSarra');
      // Read
      debugPrint('Toutes les personnes : ${await db.getAllPersons()}');
      // Update
      await db.updatePerson(Person(id: idSarra, name: 'Sarra', age: 22));
      // Delete
      final persons = await db.getAllPersons();
      await db.deletePerson(persons.last.id!);
      debugPrint('Après modification et suppression : ${await db.getAllPersons()}');
      runApp(const MaterialApp(
        home: Scaffold(body: Center(child: Text('Voir la console'))),
      ));
    }
    Point de contrôle : la console affiche le chemin de la base, puis les listes de personnes. Relancez l'application : les personnes de l'exécution précédente sont toujours là, la base est bien conservée sur l'appareil.
    1. Vérifier la base de l'application dans DB Browser

      • Le fichier people.db est créé sur l'appareil, au chemin affiché dans la console. Pour l'ouvrir dans DB Browser, il faut d'abord le récupérer sur l'ordinateur :
    Environnement Méthode
    Bureau (Windows, Linux, macOS) Le fichier est directement sur votre disque, au chemin affiché dans la console. Dans DB Browser : Ouvrir une base de données en lecture seule (flèche à côté du bouton « Ouvrir »).
    Émulateur, avec Android Studio View → Tool Windows → Device Explorer, dossier data/data/<nom.du.paquet>/databases/, clic droit sur le dossier databases → Save As pour copier tout le dossier, puis ouvrir people.db.
    Émulateur ou téléphone, avec adb Dans l'Invite de commandes (cmd), pas dans PowerShell, exécuter les commandes ci-dessous.
    rem Remplacer com.example.mon_projet par le nom du paquet (applicationId dans android/app/build.gradle.kts)
    adb exec-out run-as com.example.mon_projet cat databases/people.db > people.db
    adb exec-out run-as com.example.mon_projet cat databases/people.db-wal > people.db-wal
    • Dans DB Browser, vérifier :
      • onglet Structure: la table persons et ses trois colonnes, identiques à celles de essai.db (section 4.3) ;
      • onglet Parcourir les données: les lignes insérées par l'application, avec leur id généré automatiquement ;
      • onglet Éditer les pragmas: la ligne User Version vaut 1, la valeur du paramètre version de openDatabase().
    Pièges fréquents :
    1) PowerShell modifie les données redirigées avec > et produit un fichier illisible (« file is not a database ») : utilisez cmd.
    2) Android écrit les dernières modifications dans un fichier people.db-wal: copiez-le avec le .db, sinon les dernières lignes manquent.
    3) Sur émulateur, vous travaillez sur une copie: une modification faite dans DB Browser ne change pas la base de l'application.
    4) Si vous versionnez votre projet avec Git, ajoutez *.db et *.db-wal au fichier .gitignore.
    1. Erreurs fréquentes et solutions

    Message ou symptôme Cause Solution
    MissingPluginException ou databaseFactory not initialized Application lancée sur le web, ou sur Windows/Linux sans sqflite_common_ffi Lancer sur Android ou iOS, ou initialiser sqflite_common_ffi (section 5, étape 2).
    no such table: persons La base existait déjà avant l'ajout de la table : onCreate n'est pas rappelé Pendant le développement : désinstaller l'application. Sinon : augmenter version et créer la table dans onUpgrade.
    table persons has no column named ... Clé de toMap() différente du nom de la colonne Comparer les clés de toMap() avec l'onglet Structure de DB Browser.
    near "/": syntax error ou near ")": syntax error Commentaire // ou virgule en trop dans le CREATE TABLE Corriger le SQL et le tester d'abord dans DB Browser.
    type 'int' is not a subtype of type 'double' Un nombre décimal enregistré sans décimale revient en int Écrire (map['prix'] as num).toDouble().
    Null check operator used on a null value sur id! Objet créé en Dart mais jamais relu depuis la base Utiliser l'id renvoyé par insert(), ou relire l'objet avec getPerson().
    UNIQUE constraint failed Insertion d'un id (ou d'une valeur UNIQUE) déjà présent Laisser l'id à null pour une nouvelle ligne, ou utiliser update().
    1. Activité de synthèse : gérer des étudiants

      • Objectif : refaire seul toute la démarche du cours pour une table students.
      • Consignes :
        1. Dans DB Browser : créer une base brouillon.db et y tester le CREATE TABLE students avec les colonnes id (entier, clé primaire auto-incrémentée), name (texte obligatoire), age (entier), major (spécialité, texte), email (texte unique) et actif (booléen stocké en 0/1). Insérer trois étudiants et écrire une requête qui affiche les étudiants actifs triés par nom.
        2. Classe modèle : créer lib/models/student.dart avec la classe Student, son constructeur, toMap(), factory Student.fromMap() et toString().
        3. Service : créer lib/services/database_service.dart sur le modèle de la section 7.5 (Singleton, base school.db, table students) avec les méthodes addStudent, getAllStudents, getStudent, updateStudent et deleteStudent.
        4. Test : dans main.dart, insérer trois étudiants, en modifier un, en supprimer un et afficher la liste dans la console après chaque opération.
        5. Erreur volontaire : insérer deux étudiants avec le même e-mail. Relever le message d'erreur et l'intercepter avec un try / catch qui affiche « Cet e-mail est déjà utilisé ».
        6. Vérification : récupérer school.db et l'ouvrir dans DB Browser. Faire une capture de l'onglet Parcourir les données montrant la table students.