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

Développer une application de notes CRUD avec Flutter et Firebase

Un cours complet pour maîtriser Développer une application de notes CRUD avec Flutter et Firebase.

0 chapitres 0 QCM Accès gratuit
 

Développer une application de notes CRUD avec Flutter et Firebase

  1. 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
  2. É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é)
      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
  3. Tâche à réaliser

    1. Créer une nouvelle application Flutter
      • flutter create nom_de_votre_app
      • Ouvrez le projet dans un IDE (Android Studio, VS Code…)
    2. 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/.
    3. 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());
          }
          
  4. Fichiers réalisés

    1. Fichier : model/book.dart
        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,
            };
          }
        }
    2. 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();
          }
        }
    3. 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)),
              ),
            );
          }
        }
    4. 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);
          }
        }
        
    5. 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(),
              ),
            );
          }
        }