Aller au contenu principal

Atelier 5 : Gestionnaire de Contacts avec Persistance (SQLite + Riverpod)

Vue d'ensemble

Cet atelier reprend le Gestionnaire de Contacts de l'Atelier 2, mais avec persistance des données en base de données locale (SQLite) et gestion d'état avec Riverpod.

Objectifs

  • Implémenter SQLite pour persister les contacts
  • Utiliser Riverpod pour séparer logique et UI
  • Appliquer l'architecture montrée dans le Cours 5.3
  • Gérer les opérations CRUD (Create, Read, Update, Delete)
  • Afficher les contacts sauvegardés même après fermeture de l'app

Stack technologique

  • SQLite : Stockage persistent des données
  • Riverpod : Gestion d'état
  • flutter_riverpod : Intégration Riverpod dans Flutter

Structure du projet

Créez l'architecture suivante :

lib/
├── models/
│ └── contact.dart # Modèle Contact
├── services/
│ └── database_service.dart # Service accès BD
├── providers/
│ └── contact_providers.dart # Providers Riverpod
├── screens/
│ └── contacts_list_screen.dart # UI principale
└── main.dart # Point d'entrée

Partie 1 : Modèle de données

Créez un fichier lib/models/contact.dart avec une classe Contact qui contient :

Propriétés

  • int? id : Clé primaire (nullable pour les nouveaux contacts)
  • String firstName : Prénom
  • String lastName : Nom
  • String email : Adresse email
  • String phone : Numéro de téléphone

Getters

  • String fullName : Retourne le nom complet
  • String initials : Retourne les initiales (premières lettres du prénom et nom)

Méthodes

  • factory Contact.fromMap(Map<String, dynamic> map) : Convertir depuis la base de données
  • Map<String, dynamic> toMap() : Convertir vers la base de données
  • Contact copyWith({...}) : Créer une copie modifiée (immuabilité)

Partie 2 : Service d'accès à la Base de Données

Créez un fichier lib/services/database_service.dart avec une classe DatabaseService :

Pattern Singleton

  • Instance unique pour toute l'application
  • Constructeur privé

Propriétés

  • late Database _db : Instance de la base de données

Méthodes

  • Future<void> init() : Initialiser la BD avec table contacts
    • Colonnes : id, firstName, lastName, email, phone
  • Future<List<Contact>> getContacts() : Récupérer tous les contacts
  • Future<int> addContact(Contact contact) : Ajouter un contact
  • Future<void> updateContact(Contact contact) : Modifier un contact
  • Future<void> deleteContact(int id) : Supprimer un contact

Partie 3 : Providers (Logique métier)

Créez un fichier lib/providers/contact_providers.dart avec 3 providers :

Provider 1 : Service BD

  • dbProvider : Provider simple qui retourne l'instance de DatabaseService

Provider 2 : Liste des contacts

  • contactsProvider : FutureProvider qui récupère la liste des contacts depuis la BD

Provider 3 : Actions sur contacts

  • contactActionsProvider : StateNotifierProvider qui gère les actions
  • Classe ContactActions extends StateNotifier<AsyncValue<void>>
    • Méthode addContact(firstName, lastName, email, phone)
    • Méthode updateContact(Contact contact)
    • Méthode deleteContact(int id)
    • Chaque méthode doit appeler ref.refresh(contactsProvider) après modification

Partie 4 : Interface utilisateur

Créez un fichier lib/screens/contacts_list_screen.dart :

Widget principal

  • ContactsListScreen extends ConsumerWidget
  • Utilise ref.watch(contactsProvider) pour afficher la liste
  • Utilise ref.read(contactActionsProvider.notifier) pour les actions

Fonctionnalités UI

  • Affichage de la liste des contacts avec ListView.builder
  • Gestion des 3 états avec .when() : loading, error, data
  • FloatingActionButton pour ajouter un contact
  • Boutons pour éditer/supprimer chaque contact
  • Dialog pour ajouter un nouveau contact (4 TextFields)
  • Dialog pour modifier un contact existant (4 TextFields pré-remplis)

Widget ContactTile

  • Affiche le CircleAvatar avec initiales
  • Affiche nom complet et email
  • Boutons éditer et supprimer

Partie 5 : Point d'entrée

Créez/modifiez le fichier lib/main.dart :

Fonction main()

  • Initialiser Flutter avec WidgetsFlutterBinding.ensureInitialized()
  • Initialiser la base de données avec await DatabaseService().init()
  • Wrapper l'app avec ProviderScope
  • Lancer l'application

Widget MyApp

  • MaterialApp avec theme
  • Home : ContactsListScreen

À faire

Niveau 1 : Compléter la base

  1. Créer les 5 fichiers dans la structure proposée
  2. Implémenter le modèle Contact avec toutes les méthodes
  3. Implémenter DatabaseService avec le pattern Singleton et toutes les opérations CRUD
  4. Implémenter les 3 providers Riverpod
  5. Créer l'UI complète avec dialogs pour add/edit
  6. Tester l'ajout, la modification et la suppression
  7. Vérifier que les données persistent après fermeture de l'app

Niveau 2 : Améliorations

  1. Ajouter une recherche par nom ou email
  2. Ajouter une validation des emails et téléphones avant sauvegarde
  3. Implémenter un tri (par nom, par date d'ajout)
  4. Ajouter une confirmation (AlertDialog) avant suppression
  5. Ajouter une photo de profil (optionnel, stocker le path)

Niveau 3 : Avancé

  1. Implémenter un undo/redo (garder historique des modifications)
  2. Ajouter la synchronisation avec une API REST backend
  3. Implémenter l'export des contacts en CSV
  4. Ajouter un flag favori pour certains contacts
  5. Implémenter des groupes de contacts (table séparée avec relation)

Concepts à retenir

ConceptRôle
ModèleStructure des données (Contact)
ServiceEncapsule accès BD (DatabaseService)
ProviderSource de données (contactsProvider)
StateNotifierGère les actions (ContactActions)
UI (ConsumerWidget)Affiche et appelle les actions

Schéma de la Base de Données

Table contacts :

  • id : INTEGER PRIMARY KEY AUTOINCREMENT
  • firstName : TEXT NOT NULL
  • lastName : TEXT NOT NULL
  • email : TEXT NOT NULL
  • phone : TEXT NOT NULL

Dependencies à ajouter

dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.1.0
sqflite: ^2.2.8+4
path: ^1.8.3

dev_dependencies:
build_runner: ^2.4.6
riverpod_generator: ^2.3.6

Conseils

  • Référez-vous au Cours 5.3 (Riverpod) pour l'architecture et les exemples de code
  • Testez chaque partie séparément avant d'intégrer
  • Utilisez print() pour debugger les opérations BD
  • N'oubliez pas d'appeler ref.refresh() après modifications
  • Gérez les erreurs avec AsyncValue.guard()