Ce guide est conçu pour les débutants. Nous allons créer une application de gestion de tâches où les données sont stockées dans le cloud (Cloud Firestore) et synchronisées en temps réel sur tous les appareils.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  1. Le SDK Flutter installé et configuré.
  2. Un éditeur de code (VS Code ou Android Studio).
  3. Un compte Google pour accéder à la console Firebase.
  4. Node.js installé (pour utiliser la ligne de commande Firebase).

Étape 1 : Création du projet Flutter

Ouvrez votre terminal et créez un nouveau projet :

flutter create ma_super_todo
cd ma_super_todo

Étape 2 : Configuration de Firebase (La méthode moderne)

Nous allons utiliser FlutterFire CLI, la méthode recommandée par Google.

Installez les outils Firebase (si ce n'est pas déjà fait) :

npm install -g firebase-tools

Connectez-vous à votre compte Google :

firebase login

Activez le CLI FlutterFire :

dart pub global activate flutterfire_cli

Configurez le projet :Dans le dossier de votre projet Flutter (ma_super_todo), lancez :

flutterfire configure
  • Sélectionnez "Create a new project".
  • Donnez-lui un nom (ex: ma-super-todo-db).
  • Sélectionnez les plateformes (Android, iOS, Web).

Cela va générer automatiquement un fichier firebase_options.dart dans votre dossier lib. C'est la clé de liaison entre votre app et Firebase.

Étape 3 : Configuration de la Base de Données (Console Firebase)

  1. Allez sur la Console Firebase.
  2. Sélectionnez votre projet nouvellement créé.
  3. Dans le menu de gauche, cliquez sur Build > Firestore Database.
  4. Cliquez sur Créer une base de données.
  5. Choisissez un emplacement (ex: eur3 pour l'Europe ou nam5 pour les USA).

Important pour le développement : Choisissez "Démarrer en mode test".

  • Note : Cela permet d'écrire/lire sans authentification pendant 30 jours. Pour une vraie app, il faudra sécuriser les règles plus tard.

Étape 4 : Installation des dépendances

Ouvrez votre terminal dans le dossier du projet et ajoutez les paquets nécessaires :

flutter pub add firebase_core cloud_firestore
  • firebase_core : Le cœur de Firebase.
  • cloud_firestore : Pour interagir avec la base de données.

Étape 5 : Le Code - Initialisation

Ouvrez lib/main.dart. Nous devons initialiser Firebase avant de lancer l'application.

Remplacez tout le contenu de main.dart par ceci :

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'firebase_options.dart'; // Généré automatiquement par flutterfire configure

void main() async {
  // 1. On s'assure que les widgets sont prêts
  WidgetsFlutterBinding.ensureInitialized();
  
  // 2. On initialise Firebase avec les options de notre projet
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Ma Super Todo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.deepPurple,
        useMaterial3: true,
      ),
      home: const TodoListPage(),
    );
  }
}

Étape 6 : L'interface et la Logique (TodoListPage)

À la suite du code précédent (ou dans un nouveau fichier), créez la page principale. C'est ici que toute la magie opère.

Nous allons utiliser un StreamBuilder. C'est un widget puissant qui écoute la base de données en temps réel. Si vous ajoutez une tâche depuis la console Firebase, elle apparaîtra instantanément dans l'app sans recharger !

class TodoListPage extends StatefulWidget {
  const TodoListPage({super.key});

  @override
  State<TodoListPage> createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  // Contrôleur pour récupérer le texte saisi
  final TextEditingController _taskController = TextEditingController();

  // --- FONCTION : Ajouter une tâche ---
  void _addTask() {
    if (_taskController.text.isEmpty) return;

    // On ajoute un document dans la collection 'todos'
    FirebaseFirestore.instance.collection('todos').add({
      'title': _taskController.text, // Le titre de la tâche
      'isDone': false,               // Par défaut, pas terminée
      'createdAt': Timestamp.now(),  // Pour trier par date
    });

    _taskController.clear(); // On vide le champ de texte
    Navigator.of(context).pop(); // On ferme la fenêtre de dialogue
  }

  // --- FONCTION : Supprimer une tâche ---
  void _deleteTask(String docId) {
    FirebaseFirestore.instance.collection('todos').doc(docId).delete();
  }

  // --- FONCTION : Basculer l'état (Fait / Pas fait) ---
  void _toggleTask(String docId, bool currentStatus) {
    FirebaseFirestore.instance.collection('todos').doc(docId).update({
      'isDone': !currentStatus,
    });
  }

  // --- UI : Fenêtre pour ajouter une tâche ---
  void _showAddDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Nouvelle tâche'),
        content: TextField(
          controller: _taskController,
          decoration: const InputDecoration(hintText: "Ex: Acheter du pain"),
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Annuler'),
          ),
          ElevatedButton(
            onPressed: _addTask,
            child: const Text('Ajouter'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Mes Tâches Firebase'),
        backgroundColor: Colors.deepPurple.shade100,
      ),
      // Le bouton flottant pour ajouter
      floatingActionButton: FloatingActionButton(
        onPressed: _showAddDialog,
        child: const Icon(Icons.add),
      ),
      // Le corps de la page : StreamBuilder
      body: StreamBuilder<QuerySnapshot>(
        // On écoute la collection 'todos' triée par date
        stream: FirebaseFirestore.instance
            .collection('todos')
            .orderBy('createdAt', descending: true)
            .snapshots(),
        builder: (context, snapshot) {
          // Cas 1 : Erreur
          if (snapshot.hasError) {
            return const Center(child: Text('Une erreur est survenue'));
          }

          // Cas 2 : Chargement
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          // Cas 3 : Liste vide
          if (snapshot.data!.docs.isEmpty) {
            return const Center(child: Text('Aucune tâche pour le moment !'));
          }

          // Cas 4 : Affichage de la liste
          final docs = snapshot.data!.docs;

          return ListView.builder(
            itemCount: docs.length,
            itemBuilder: (context, index) {
              // On récupère les données du document
              final doc = docs[index];
              final data = doc.data() as Map<String, dynamic>;
              final String docId = doc.id; // L'ID unique généré par Firestore

              return Card(
                margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
                child: ListTile(
                  // Checkbox pour marquer comme fait
                  leading: Checkbox(
                    value: data['isDone'] ?? false,
                    onChanged: (val) => _toggleTask(docId, data['isDone']),
                  ),
                  // Le titre de la tâche (barré si fini)
                  title: Text(
                    data['title'] ?? 'Sans titre',
                    style: TextStyle(
                      decoration: (data['isDone'] ?? false)
                          ? TextDecoration.lineThrough
                          : null,
                      color: (data['isDone'] ?? false) ? Colors.grey : Colors.black,
                    ),
                  ),
                  // Bouton supprimer
                  trailing: IconButton(
                    icon: const Icon(Icons.delete, color: Colors.redAccent),
                    onPressed: () => _deleteTask(docId),
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Étape 7 : Lancer l'application

  1. Assurez-vous que votre émulateur est lancé ou que votre téléphone est branché.

Exécutez la commande :

flutter run

Ce que vous devriez voir :

  • Une liste vide au début.
  • Un bouton "+" qui ouvre une fenêtre.
  • Quand vous ajoutez une tâche, elle apparaît instantanément.
  • Si vous allez dans votre console Firebase (dans le navigateur), vous verrez les données apparaître dans "Firestore Database" en temps réel.
  • Si vous cochez la case, le champ isDone passe à true dans la base de données.

Résumé des concepts clés appris

  1. FirebaseFirestore.instance : Le point d'entrée pour parler à la base de données.
  2. collection('...').add(...) : Créer une nouvelle donnée (Create).
  3. snapshots() + StreamBuilder : La méthode magique pour lire les données et mettre à jour l'écran automatiquement quand la base de données change (Read).
  4. doc(id).update(...) : Modifier une donnée existante (Update).
  5. doc(id).delete() : Supprimer une donnée (Delete).

Félicitations ! Vous venez de créer votre première application avec Flutter.