Développer une application de notes CRUD avec Flutter et Firebase
Sommaire
- 1- Objectifs pédagogiques
- 2- Énoncé de l'application : Mes Notes
- 3- Tâche à réaliser
- 3.1- Créer une nouvelle application Flutter
- 3.2- Se connecter à Firebase et configurer FlutterFire CLI
- 3.3- Configurer Firebase dans le projet Flutter
- 4- Fichiers réalisés
- 4.1- Fichier : model/book.dart
- 4.2- Fichier : service/book_service.dart
- 4.3- Fichier : view/home_page.dart
- 4.4- Fichier : viewmodel/book_view_model.dart
- 4.5- Fichier : main.dart
- 4.5.1- Cours Flutter
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é)
-
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.dart
dans lib/. -
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 :
- c. Modifier android/app/build.gradle :
- d. Modifier le fichier :android/app/build.gradle.kts
- Dans ton fichier android/app/build.gradle.kts, ajoute (ou modifie) ce bloc dans android
- d. Ajouter les dépendances dans pubspec.yaml :
- e. Initialiser Firebase dans main.dart :
-
Fichiers réalisés
-
Fichier : model/book.dart
-
Fichier : service/book_service.dart
-
Fichier : view/home_page.dart
-
Fichier : viewmodel/book_view_model.dart
-
Fichier : main.dart
Structure du projet
-
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
buildscript {
dependencies {
classpath("com.google.gms:google-services:4.3.15")
}
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
ndkVersion = "27.0.12077973"
// ... le reste de ta configuration Android
}
dependencies:
firebase_core: ^2.0.0
cloud_firestore: ^4.0.0
flutter:
sdk: flutter
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());
}
class Book {
String id;
String title;
String author;
Book({this.id = '', required this.title, required this.author});
factory Book.fromMap(Map data, String id) {
return Book(
id: id,
title: data['title'],
author: data['author'],
);
}
Map toMap() {
return {
'title': title,
'author': author,
};
}
}
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();
}
}
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)),
),
);
}
}
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);
}
}
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(),
),
);
}
}