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

Créer une application CRUD Flutter liée à un projet Firebase existant

Un cours complet pour maîtriser Créer une application CRUD Flutter liée à un projet Firebase existant.

0 chapitres 0 QCM Accès gratuit
 

Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider

  1. Objectif

    • Développer une nouvelle application Flutter capable d’effectuer des opérations CRUD (Create, Read, Update, Delete) sur une collection Firestore (books), en se connectant à un projet Firebase existant.
    • L’application devra partager la même base de données que celle d’une autre application Flutter utilisant déjà ce projet Firebase (mêmes collections, mêmes données).
    • Structure du projet
      lib/ ├── main.dart ├── model/ │ └── book.dart ├── service/ │ └── book_service.dart ├── view/ │ └── home_page.dart ├── viewmodel/ │ └── book_viewmodel.dart
      Model ➜ représente un livre. Service ➜ accès Firebase Firestore (CRUD). ViewModel ➜ logique métier + interface entre vue et service. View ➜ affiche l’interface + interagit via le ViewModel. main.dart ➜ init Firebase et MVVM avec Provider.
  2. 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 à un projet Firebase existant
      • a. Accéder à la console Firebase : https://console.firebase.google.com
      • b. Ouvrir le projet existant utilisé par une autre app Flutter
      • c. Ajouter une nouvelle application Android à ce projet :
        • Cliquez sur l'icône Android sous la section "Vos applications"
        • Entrez le nom du package (applicationId) de votre nouvelle app Flutter (exemple : com.example.mycrudapp, visible dans android/app/build.gradle) ==> Cliquez sur "Enregistrer l'application"
      • d. Télécharger le fichier google-services.json généré
      • e. Copier ce fichier dans : android/app/google-services.json
    3. Configurer Firebase dans le projet Flutter
      • a. Modifier android/build.gradle:
          dependencies { classpath 'com.google.gms:google-services:4.3.15' }
      • b. Modifier android/app/build.gradle:
          plugins {
              id("com.android.application")
              id("kotlin-android")
              // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
              id("dev.flutter.flutter-gradle-plugin")
             id("com.google.gms.google-services")
          }
      • c. Ajouter les dépendances dans pubspec.yaml :
          dependencies:
            firebase_core: ^2.0.0
            cloud_firestore: ^4.0.0
            flutter:
              sdk: flutter
      • d. Initialiser Firebase dans main.dart :
          void main() async {
            WidgetsFlutterBinding.ensureInitialized();
            await Firebase.initializeApp();
            runApp(MyApp());
          }
  3. 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(),
              ),
            );
          }
        }