Développer une application de notes CRUD avec Flutter et Firebase
-
Objectifs pédagogiques
- Au terme de ce projet, l’étudiant sera capable de :
- Compétences Firebase :
- Reconnecter une nouvelle application Flutter à un projet Firebase déjà existant.
- Gérer une nouvelle collection Firestore (notes) tout en partageant le même projet Firebase que d'autres apps.
- Compétences Flutter :
- Structurer une application Flutter en architecture MVVM avec Provider.
- Utiliser les widgets Flutter adaptés à une application de prise de notes : TextField, Card, ListView, etc.
- Implémenter les opérations CRUD complètes avec Firestore.
- Compétences transversales :
- Réutilisation d’une infrastructure cloud existante.
- Structuration propre du code en couches : Modèle / Vue / Service / ViewModel.
- Gestion d’état fluide et centralisée avec ChangeNotifier
Énoncé de l'application : Mes Notes
- Vous devez développer une nouvelle application Flutter permettant de gérer des notes personnelles en utilisant Firebase Firestore. Cette application s’intègre dans un environnement partagé, car elle doit se connecter à un projet Firebase déjà existant, utilisé par une autre application (celle de gestion des livres).
- L’application doit utiliser la même base de données Firebase que le projet "Books", mais interagir avec une nouvelle collection nommée notes.
- Fonctionnalités attendues
- L’application "Mes Notes" doit permettre à l’utilisateur de :
- Créer une nouvelle note (titre + contenu).
- Afficher la liste des notes.
- Modifier une note existante.
- Supprimer une note.
- Chaque note est composée des champs suivants :
- id: identifiant unique auto-généré
- title : le titre de la note
- content: le contenu de la note
- date: date de création/modification (optionnel mais recommandé)
-
lib/
├── main.dart # Initialisation Firebase + Provider
├── model/
│ └── note.dart # Représente une note
├── service/
│ └── note_service.dart # Fonctions d'accès Firestore (CRUD)
├── view/
│ └── home_page.dart # Interface utilisateur
├── viewmodel/
│ └── note_viewmodel.dart # Logique métier & interaction avec la vue
- L’application "Mes Notes" doit permettre à l’utilisateur de :
Tâche à réaliser
Créer une nouvelle application Flutter
- flutter create nom_de_votre_app
- Ouvrez le projet dans un IDE (Android Studio, VS Code…)
-
Se connecter à Firebase et configurer FlutterFire CLI
- Si ce n’est pas encore fait, installe FlutterFire CLI :
dart pub global activate flutterfire_cli - Puis connecte-toi :
flutterfire login - Et configure Firebase pour ce projet :
flutterfire configure - Lors de la configuration :
- Choisis le projet Firebase existant (celui utilisé par l'app Books).
- Sélectionne Android comme plateforme.
- Entre le bon nom de package (ex: com.example.mesnotes).
- Cela génère un fichier
firebase_options.dartdans lib/.
- Si ce n’est pas encore fait, installe FlutterFire CLI :
Configurer Firebase dans le projet Flutter
- a. Copier le google-services.json:
- Téléchargé depuis la console Firebase (section Android).
- Colle-le dans : android/app/google-services.json
- b. Modifier android/build.gradle:
buildscript { dependencies { classpath("com.google.gms:google-services:4.3.15") } repositories { google() mavenCentral() } } - c. Modifier android/app/build.gradle:
plugins { id("com.android.application") id("kotlin-android") id("dev.flutter.flutter-gradle-plugin") id("com.google.gms.google-services") } - d. Modifier le fichier :android/app/build.gradle.kts
- Dans ton fichier android/app/build.gradle.kts, ajoute (ou modifie) ce bloc dans android
android { ndkVersion = "27.0.12077973" // ... le reste de ta configuration Android } - d. Ajouter les dépendances dans pubspec.yaml :
dependencies: firebase_core: ^2.0.0 cloud_firestore: ^4.0.0 flutter: sdk: flutter - e. Initialiser Firebase dans main.dart :
import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp(MyApp()); }
- a. Copier le google-services.json:
Fichiers réalisés
Fichier : model/book.dart
class Book { String id; String title; String author; Book({this.id = '', required this.title, required this.author}); factory Book.fromMap(Mapdata, String id) { return Book( id: id, title: data['title'], author: data['author'], ); } Map toMap() { return { 'title': title, 'author': author, }; } } Fichier : service/book_service.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import '../model/book.dart'; class BookService { final CollectionReference _books = FirebaseFirestore.instance.collection('books'); Stream<List<Book>> getBooks() { return _books.snapshots().map((snapshot) { return snapshot.docs.map((doc) => Book.fromMap(doc.data() as Map<String, dynamic>, doc.id)).toList(); }); } Future<void> addBook(Book book) async { await _books.add(book.toMap()); } Future<void> updateBook(Book book) async { await _books.doc(book.id).update(book.toMap()); } Future<void> deleteBook(String id) async { await _books.doc(id).delete(); } }Fichier : view/home_page.dart
import 'package:flutter/material.dart'; import '../viewmodel/book_viewmodel.dart'; import 'package:provider/provider.dart'; import '../model/book.dart'; class HomePage extends StatelessWidget { final TextEditingController titleController = TextEditingController(); final TextEditingController authorController = TextEditingController(); final ColorScheme _colorScheme = ColorScheme.fromSeed(seedColor: Colors.deepPurple); void _showAddDialog(BuildContext context, BookViewModel vm) { titleController.clear(); authorController.clear(); showDialog( context: context, builder: (_) => AlertDialog( backgroundColor: _colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text('Ajouter un livre', style: TextStyle( fontWeight: FontWeight.bold, color: _colorScheme.onSurface, )), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: titleController, decoration: InputDecoration( labelText: 'Titre', labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: _colorScheme.outline), ), ), ), SizedBox(height: 16), TextField( controller: authorController, decoration: InputDecoration( labelText: 'Auteur', labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: _colorScheme.outline), ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Annuler', style: TextStyle(color: _colorScheme.onSurface)), ), ElevatedButton( onPressed: () { vm.addBook(titleController.text, authorController.text); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: _colorScheme.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), child: Text('Enregistrer', style: TextStyle(color: _colorScheme.onPrimary)), ), ], ), ); } void _showEditDialog(BuildContext context, BookViewModel vm, Book book) { titleController.text = book.title; authorController.text = book.author; showDialog( context: context, builder: (_) => AlertDialog( backgroundColor: _colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text('Modifier un livre', style: TextStyle( fontWeight: FontWeight.bold, color: _colorScheme.onSurface, )), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: titleController, decoration: InputDecoration( labelText: 'Titre', labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: _colorScheme.outline), ), ), ), SizedBox(height: 16), TextField( controller: authorController, decoration: InputDecoration( labelText: 'Auteur', labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: _colorScheme.outline), ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Annuler', style: TextStyle(color: _colorScheme.onSurface)), ), ElevatedButton( onPressed: () { vm.updateBook(book.id, titleController.text, authorController.text); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: _colorScheme.secondary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), child: Text('Modifier', style: TextStyle(color: _colorScheme.onSecondary)), ), ], ), ); } @override Widget build(BuildContext context) { final vm = Provider.of<BookViewModel>(context); return Scaffold( backgroundColor: _colorScheme.background, appBar: AppBar( title: Text('Flutter Firestore CRUD', style: TextStyle( color: _colorScheme.onPrimary, fontWeight: FontWeight.w600, )), backgroundColor: _colorScheme.primary, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( bottom: Radius.circular(16), ), ), ), body: StreamBuilder<List<Book>>( stream: vm.books, builder: (context, snapshot) { if (snapshot.hasError) return Center(child: Text('Erreur')); if (!snapshot.hasData) return Center(child: CircularProgressIndicator()); final books = snapshot.data!; return ListView.builder( padding: EdgeInsets.all(16), itemCount: books.length, itemBuilder: (_, i) { final book = books[i]; return Container( margin: EdgeInsets.only(bottom: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 6, offset: Offset(0, 2), ) ], ), child: Card( color: _colorScheme.surface, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), child: ListTile( title: Text( book.title, style: TextStyle( fontWeight: FontWeight.w500, color: _colorScheme.onSurface, ), ), subtitle: Text(book.author, style: TextStyle( color: _colorScheme.onSurface.withOpacity(0.7))), onTap: () => _showEditDialog(context, vm, book), trailing: IconButton( icon: Icon(Icons.delete_outline, color: _colorScheme.error), onPressed: () => vm.deleteBook(book.id), ), contentPadding: EdgeInsets.symmetric( horizontal: 24, vertical: 12), ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddDialog(context, vm), child: Icon(Icons.add, color: _colorScheme.onPrimary), backgroundColor: _colorScheme.primary, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), ), ); } }Fichier : viewmodel/book_view_model.dart
import 'package:flutter/material.dart'; import '../model/book.dart'; import '../service/book_service.dart'; class BookViewModel extends ChangeNotifier { final BookService _bookService = BookService(); Stream<List<Book>> get books => _bookService.getBooks(); Future<void> addBook(String title, String author) async { await _bookService.addBook(Book(title: title, author: author)); } Future<void> updateBook(String id, String title, String author) async { await _bookService.updateBook(Book(id: id, title: title, author: author)); } Future<void> deleteBook(String id) async { await _bookService.deleteBook(id); } }Fichier : main.dart
import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:provider/provider.dart'; import 'view/home_page.dart'; import 'viewmodel/book_view_model.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // important! runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => BookViewModel(), child: MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), home: HomePage(), ), ); } }