Aller au contenu
⬅️ Retour au sommaire ⬅️ Retour au Chapitre 2

✅ Corrigés — Journée 2

TP carte interactive avec une seconde page expérience alternance

L’objectif, ici est de reprendre soit la correction du TP du jour1 ou bien votre propre code et d’ajouter une page pour pouvoir naviguer entre les 2 pages. ça permet de ne pas tout réécrire. Je vous propose pour cet exercice de construire une structure différente avec des packages correspondants aux classes de widgets utilisées.

lib/
├── main.dart                      point d'entrée avec "MaterialApp" (minimum pour lancer notre application)
├── pages/
│   ├── page_carte_visite.dart     votre "pageCarteVisite" déjà existante
│   └── page_experience.dart       votre nouvelle "PageExperience" (à créer)
└── widgets/
    └── ligne_contact.dart         votre "LigneContact" (réutilisable) et existant

Voici les rendus sur l’émulateur Android Pixel7:

Page 1

représentation graphique catalogue produits

Page 2

représentation graphique catalogue produits

codes

main.dart

import 'package:flutter/material.dart';
import 'pages/page_carte_visite.dart';

void main() => runApp(const CarteVisiteApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Ma Carte de Visite',
      theme: ThemeData(primarySwatch: Colors.indigo, useMaterial3: true),
      home: const PageCarteVisite(),  // appel constructeur de la page carte de visite
    );
  }
}

page_carte_visite.dart (dans le dossier pages)

import 'package:flutter/material.dart';
import '../widgets/ligne_contact.dart';
import 'page_experience.dart';          // votre nouvelle page sur l'expérience en alternance

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

  @override
  State<PageCarteVisite> createState() => _PageCarteVisiteState();
}

class _PageCarteVisiteState extends State<PageCarteVisite> {
  int _vues = 0;

// ici, on utilise une SnackBar pour afficher un message temporaire lorsque l'utilisateur appuie longuement sur l'avatar.
  void _incrementerVues() {
    setState(() => _vues++);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text("Vue n°$_vues enregistrée !"),
        duration: const Duration(seconds: 1),
      ),
    );
  }

  void _envoyerMessage() {
    ScaffoldMessenger.of(context)
        .showSnackBar(const SnackBar(content: Text("Message envoyé !")));
  }

  void _ouvrirExperience() {             // navigation pour aller vers la page d'expérience en alternance
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const PageExperience()), // on appel le constructeur de la page d'expérience
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Ma Carte de Visite")),
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              GestureDetector(
                onLongPress: _incrementerVues,
                child: const CircleAvatar(
                  radius: 60,
                  backgroundColor: Colors.indigo,
                  child: Icon(Icons.person, size: 70, color: Colors.white),
                ),
              ),
              const SizedBox(height: 8),
              Text("Vues de la carte : $_vues",
                  style: TextStyle(color: Colors.grey[600])),
              const SizedBox(height: 20),
              const Text("Camille Dupont",
                  style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold)),
              Text("Développeuse Mobile Flutter",
                  style: TextStyle(fontSize: 16, color: Colors.grey[600])),
              const SizedBox(height: 24),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: const [
                  LigneContact(icone: Icons.email, texte: "camille.dupont@email.com"),
                  LigneContact(icone: Icons.phone, texte: "+33 6 12 34 56 78"),
                  LigneContact(icone: Icons.location_on, texte: "Paris, France"),
                ],
              ),
              const SizedBox(height: 24),          // ← remis ici (il manquait)

              ElevatedButton.icon(
                onPressed: _envoyerMessage,
                icon: const Icon(Icons.send),
                label: const Text("Me contacter"),
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                ),
              ),
              const SizedBox(height: 12),

              // votre nouveau bouton de navigation pour aller vers la page d'expérience en alternance
              OutlinedButton.icon(
                onPressed: _ouvrirExperience,
                icon: const Icon(Icons.work_outline),
                label: const Text("Voir mon expérience en alternance"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

page_experience.dart (dans le dossier pages)

import 'package:flutter/material.dart';

// Page pour afficher l'expérience en alternance
// Cette page est accessible depuis la page carte de visite via un bouton de navigation.
// Elle présente les détails de l'expérience professionnelle, y compris l'entreprise, le poste, la période et les missions réalisées.
// Vous pouvez personnaliser le contenu de cette page selon vos besoins.
// Ici, nous avons utilisé un widget StatelessWidget car cette page ne nécessite pas de gestion d'état complexe !
class PageExperience extends StatelessWidget {
  const PageExperience({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Expérience en Alternance")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // en-tête : entreprise + intitulé
            Row(
              children: const [
                CircleAvatar(
                  radius: 28,
                  backgroundColor: Colors.indigo,
                  child: Icon(Icons.business, color: Colors.white, size: 30),
                ),
                SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        "TechSolutions Ivry",
                        style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(height: 4),
                      Text(
                        "Développeuse Full-Stack en alternance (Spring Boot / Flutter)",
                        style: TextStyle(fontSize: 15, color: Colors.indigo),
                      ),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Text("Septembre 2025 – Aujourd'hui",
                style: TextStyle(color: Colors.grey[600], fontStyle: FontStyle.italic)),
            const SizedBox(height: 24),

            // Description des tâches
            const Text(
              "Missions réalisées",
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            _Tache(texte: "Développement de microservices REST avec Spring Boot "
                "(Java 17, Spring Data JPA, Spring Security) pour la gestion des données clients."),
            _Tache(texte: "Création d'une application mobile Flutter consommant ces API "
                "(gestion d'état avec Provider, appels HTTP avec Dio, stockage local avec Hive)."),
            _Tache(texte: "Écriture de tests unitaires (JUnit, Mockito) et de tests "
                "d'intégration pour les composants backend."),
            _Tache(texte: "Participation aux revues de code et à la mise en place "
                "d'une pipeline CI/CD (Git, GitLab CI)."),
          ],
        ),
      ),
    );
  }
}

// class pour afficher une tâche avec une icône de check
// Cette classe est utilisée dans la page d'expérience pour lister les missions réalisées.
// Elle prend un texte en paramètre et l'affiche à côté d'une icône de check.
class _Tache extends StatelessWidget {
  final String texte;
  const _Tache({required this.texte});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.only(top: 3),
            child: Icon(Icons.check_circle, size: 18, color: Colors.indigo),
          ),
          const SizedBox(width: 10),
          Expanded(child: Text(texte, style: const TextStyle(fontSize: 15))),
        ],
      ),
    );
  }
}

ligne_contact.dart (dans le dossier widgets)

import 'package:flutter/material.dart';

// Widget pour afficher une ligne de contact avec une icône et un texte
// Cette classe est utilisée dans la page carte de visite pour afficher les informations de contact (email, téléphone, LinkedIn, etc.).
// Elle prend une icône et un texte en paramètre et les affiche dans une ligne avec un espacement entre eux.
class LigneContact extends StatelessWidget {
  final IconData icone;
  final String texte;

  const LigneContact({super.key, required this.icone, required this.texte});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icone, color: Colors.indigo, size: 20),
          const SizedBox(width: 10),
          Text(texte, style: const TextStyle(fontSize: 15)),
        ],
      ),
    );
  }
}

dossier sous VS-Code

représentation graphique catalogue produits

Exercice 5.1 — Liste de produits

représentation graphique catalogue produits

import 'package:flutter/material.dart';

void main() {
  runApp(const MonApplication());
}

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

/* ---------------------------------------------------------
Quelques explications sur le code ci-dessous sur le fromseed()
En Flutter, ColorScheme.fromSeed permet de générer automatiquement toute la palette de couleurs de votre application à partir
d'une seule couleur de base (la "graine" ou seed).
Cette méthode s'appuie sur le système de design "Material 3" de Google.
Elle calcule scientifiquement les nuances de couleurs complémentaires (les couleurs principales, secondaires, de fond, de texte, etc.) 
pour qu'elles s'harmonisent parfaitement entre elles et respectent les standards d'accessibilité et de contraste.
------------------------------------------------------------*/
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false, // désactive la bannière de debug
      title: 'Catalogue produits',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.green,
        ),
        useMaterial3: true,
      ),
      home: PageListeProduits(), // page d'accueil de l'application statelessWidget
    );
  }
}

// ---------------------------------------------------------
// Notre MODÈLE de Produit (nom, prix, catégorie, emoji)
// ---------------------------------------------------------

class Produit {
  final String nom;
  final double prix;
  final String categorie;

  // représentation visuelle simple du produit.
  final String emoji;

  const Produit(
    this.nom,
    this.prix,
    this.categorie,
    this.emoji,
  );
}

// ---------------------------------------------------------
// Pour la page de "LISTE DES PRODUITS"
// ---------------------------------------------------------

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

  final List<Produit> produits = const [
    Produit(
      "Pomme",
      0.50,
      "Fruits",
      "🍏",
    ),
    Produit(
      "Banane",
      0.30,
      "Fruits",
      "🍌",
    ),
    Produit(
      "Jus d'orange",
      2.90,
      "Boissons",
      "🧃",
    ),
    Produit(
      "Eau minérale",
      0.80,
      "Boissons",
      "💧",
    ),
    Produit(
      "Baguette",
      1.10,
      "Boulangerie",
      "🥖",
    ),
    Produit(
      "Croissant",
      1.30,
      "Boulangerie",
      "🥐",
    ),
    Produit(
      "Kiwi",
      0.60,
      "Fruits",
      "🥝",
    ),
    Produit(
      "Café",
      3.50,
      "Boissons",
      "☕",
    ),
    Produit(
      "Pain au chocolat",
      1.40,
      "Boulangerie",
      "🍫",
    ),
    Produit(
      "Mangue",
      1.90,
      "Fruits",
      "🥭",
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Nos produits"),
      ),

      body: ListView.builder(
        itemCount: produits.length,

        itemBuilder: (context, index) {
          final produit = produits[index];

          return Card(
            margin: const EdgeInsets.symmetric(
              horizontal: 12,
              vertical: 6,
            ),

            child: ListTile(

              // --------------------------------------------
              // Emoji représentant le produit pour éviter de télécharger des images
              // --------------------------------------------

              leading: CircleAvatar(
                backgroundColor: Colors.green.shade50,

                child: Text(
                  produit.emoji,
                  style: const TextStyle(
                    fontSize: 28,
                  ),
                ),
              ),

              // Nom du produit
              title: Text(
                produit.nom,
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),

              // Catégorie
              subtitle: Text(
                produit.categorie,
              ),

              // Prix
              trailing: Text(
                "${produit.prix.toStringAsFixed(2)} €",
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Liste des produits sur émulateur Android


Autre version avec images réelles de produits

Et voici en bonus du code avec des images à télécharger (à ne pas faire pour le moment, nous verrons cela plus tard dans la formation).

import 'package:flutter/material.dart';

void main() {
  runApp(const MonApplication());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Catalogue produits',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.green,
        ),
        useMaterial3: true,
      ),
      home: PageListeProduits(),
    );
  }
}

// ---------------------------------------------------------
// MODÈLE
// ---------------------------------------------------------

class Produit {
  final String nom;
  final double prix;
  final String categorie;

  // Chemin de l'image locale.
  final String image;

  const Produit(
    this.nom,
    this.prix,
    this.categorie,
    this.image,
  );
}

// ---------------------------------------------------------
// PAGE LISTE DES PRODUITS
// ---------------------------------------------------------

class PageListeProduits extends StatelessWidget {
  PageListeProduits({super.key});

  final List<Produit> produits = const [
    Produit(
      "Pomme",
      0.50,
      "Fruits",
      "assets/images/produits/pomme.png",
    ),
    Produit(
      "Banane",
      0.30,
      "Fruits",
      "assets/images/produits/banane.png",
    ),
    Produit(
      "Jus d'orange",
      2.90,
      "Boissons",
      "assets/images/produits/jus-orange.png",
    ),
    Produit(
      "Eau minérale",
      0.80,
      "Boissons",
      "assets/images/produits/eau.png",
    ),
    Produit(
      "Baguette",
      1.10,
      "Boulangerie",
      "assets/images/produits/baguette.png",
    ),
    Produit(
      "Croissant",
      1.30,
      "Boulangerie",
      "assets/images/produits/croissant.png",
    ),
    Produit(
      "Kiwi",
      0.60,
      "Fruits",
      "assets/images/produits/kiwi.png",
    ),
    Produit(
      "Café",
      3.50,
      "Boissons",
      "assets/images/produits/cafe.png",
    ),
    Produit(
      "Pain au chocolat",
      1.40,
      "Boulangerie",
      "assets/images/produits/pain-chocolat.png",
    ),
    Produit(
      "Mangue",
      1.90,
      "Fruits",
      "assets/images/produits/mangue.png",
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Nos produits"),
      ),

      body: ListView.builder(
        itemCount: produits.length,

        itemBuilder: (context, index) {
          final produit = produits[index];

          return Card(
            margin: const EdgeInsets.symmetric(
              horizontal: 12,
              vertical: 6,
            ),

            child: ListTile(

              // --------------------------------------------
              // Image locale du produit
              // --------------------------------------------

              leading: CircleAvatar(
                radius: 26,
                backgroundColor: Colors.green.shade50,

                child: ClipOval(
                  child: Image.asset(
                    produit.image,

                    width: 46,
                    height: 46,

                    fit: BoxFit.cover,
                  ),
                ),
              ),

              title: Text(
                produit.nom,
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),

              subtitle: Text(
                produit.categorie,
              ),

              trailing: Text(
                "${produit.prix.toStringAsFixed(2)} €",
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Cette version necéssite de récupérer des images à mettre dans le répertoire assets/images/produits et de modifier le fichier pubspec.yaml :

mon_application/
│
├── android/
├── ios/
├── lib/
│   └── main.dart
│
├── assets/
│   └── images/
│       └── produits/
│           ├── pomme.png
│           ├── banane.png
│           ├── jus-orange.png
│           ├── eau.png
│           ├── baguette.png
│           ├── croissant.png
│           ├── kiwi.png
│           ├── cafe.png
│           ├── pain-chocolat.png
│           └── mangue.png
│
└── pubspec.yaml

Vos images doivent avoir les propriétés suivantes :

Dans pubspec.yaml :

flutter:
  uses-material-design: true

  assets:
    - assets/images/produits/

Suivi de la commande flutter pub get


Exercice 5.2 — Navigation liste vers détail

Vous chacun des exercices, vous pouvez choisir des couleurs et thèmes différents…

représentation graphique liste vers détail

import 'package:flutter/material.dart';

class PageListeProduitsNav extends StatelessWidget {
  final List<Produit> produits;
  const PageListeProduitsNav({super.key, required this.produits});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Produits")),
      body: ListView.builder(
        itemCount: produits.length,
        itemBuilder: (context, index) {
          final produit = produits[index];
          return ListTile(
            title: Text(produit.nom),
            trailing: Text("${produit.prix.toStringAsFixed(2)} €"),
            onTap: () {
              // Navigation avec passage de données au constructeur
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => PageDetailProduit(produit: produit)),
              );
            },
          );
        },
      ),
    );
  }
}

class PageDetailProduit extends StatelessWidget {
  final Produit produit;
  const PageDetailProduit({super.key, required this.produit});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(produit.nom)),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(produit.nom, style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Text("Catégorie : ${produit.categorie}", style: const TextStyle(fontSize: 16)),
            const SizedBox(height: 8),
            Text("Prix : ${produit.prix.toStringAsFixed(2)} €",
                style: const TextStyle(fontSize: 20, color: Colors.green)),
            const Spacer(),
            Center(
              child: ElevatedButton.icon(
                onPressed: () => Navigator.pop(context), // retour à la liste
                icon: const Icon(Icons.arrow_back),
                label: const Text("Retour"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Exercice 5.3 — Menu latéral (Drawer) à 3 pages

représentation graphique liste vers détail

import 'package:flutter/material.dart';

void main() => runApp(const MonAppDrawer());

class MonAppDrawer extends StatelessWidget {
  const MonAppDrawer({super.key});
  @override
  Widget build(BuildContext context) => const MaterialApp(home: PageAvecDrawer());
}

class PageAvecDrawer extends StatefulWidget {
  const PageAvecDrawer({super.key});
  @override
  State<PageAvecDrawer> createState() => _PageAvecDrawerState();
}

class _PageAvecDrawerState extends State<PageAvecDrawer> {
  int _indexActif = 0;

  final List<String> _titres = ["Accueil", "Favoris", "Profil"];
  final List<IconData> _icones = [Icons.home, Icons.favorite, Icons.person];
  // nos 3 widgets qui correspondent à nos 3 pages
  final List<Widget> _pages = const [
    Center(child: Text("Bienvenue sur l'accueil", style: TextStyle(fontSize: 20))),
    Center(child: Text("Vos favoris apparaîtront ici", style: TextStyle(fontSize: 20))),
    Center(child: Text("Votre profil", style: TextStyle(fontSize: 20))),
  ];

  void _selectionnerPage(int index) {
    setState(() => _indexActif = index);
    Navigator.pop(context); // ferme le Drawer après sélection
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(_titres[_indexActif])),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.indigo),
              child: Text("Menu principal", style: TextStyle(color: Colors.white, fontSize: 22)),
            ),
            for (int i = 0; i < _titres.length; i++)
              ListTile(
                leading: Icon(_icones[i]),
                title: Text(_titres[i]),
                // Mise en évidence de l'élément actif :
                selected: _indexActif == i,
                selectedTileColor: Colors.indigo.withOpacity(0.1),
                onTap: () => _selectionnerPage(i),
              ),
          ],
        ),
      ),
      body: _pages[_indexActif],
    );
  }
}

TP 6 de synthèse — Catalogue de livres (pour ceux et celle qui ont terminé)

représentation graphique catalogue des livres

Architecture retenue : un widget racine StatefulWidget (CatalogueApp) conserve la liste des identifiants favoris dans son état, et la transmet aux écrans enfants par constructeur + callback : c’est le mécanisme de remontée d’état (dit « lifting state up »), la base de la gestion d’état avant d’introduire Provider la prochaine journée de formation.

import 'package:flutter/material.dart';

void main() => runApp(const CatalogueApp());

// ---------- MODELE ----------
class Livre {
  final int id;
  final String titre;
  final String auteur;
  final String resume;
  final String imageUrl;
  final double note; // sur 5

  const Livre({
    required this.id,
    required this.titre,
    required this.auteur,
    required this.resume,
    required this.imageUrl,
    required this.note,
  });
}

final List<Livre> catalogueLivres = [
  Livre(
    id: 1, titre: "1984", auteur: "George Orwell",
    resume: "Une dystopie glaçante sur la surveillance de masse et le totalitarisme.",
    imageUrl: "https://picsum.photos/seed/1984/200/300", note: 4.8,
  ),
  Livre(
    id: 2, titre: "Dune", auteur: "Frank Herbert",
    resume: "Sur la planète désertique Arrakis, une lutte de pouvoir façonne l'univers.",
    imageUrl: "https://picsum.photos/seed/dune/200/300", note: 4.7,
  ),
  Livre(
    id: 3, titre: "Fondation", auteur: "Isaac Asimov",
    resume: "L'effondrement d'un empire galactique prédit par la psychohistoire.",
    imageUrl: "https://picsum.photos/seed/fondation/200/300", note: 4.5,
  ),
  Livre(
    id: 4, titre: "Sapiens", auteur: "Yuval Noah Harari",
    resume: "Une brève histoire de l'humanité, de la préhistoire à nos jours.",
    imageUrl: "https://picsum.photos/seed/sapiens/200/300", note: 4.6,
  ),
  Livre(
    id: 5, titre: "Clean Code", auteur: "Robert C. Martin",
    resume: "Les bonnes pratiques pour écrire du code lisible et maintenable.",
    imageUrl: "https://picsum.photos/seed/cleancode/200/300", note: 4.3,
  ),
  Livre(
    id: 6, titre: "Le Petit Prince", auteur: "Antoine de Saint-Exupéry",
    resume: "Un conte poétique et philosophique sur l'enfance et l'essentiel.",
    imageUrl: "https://picsum.photos/seed/petitprince/200/300", note: 4.9,
  ),
  Livre(
    id: 7, titre: "Fahrenheit 451", auteur: "Ray Bradbury",
    resume: "Dans un futur où les livres sont interdits, un pompier brûle... des livres.",
    imageUrl: "https://picsum.photos/seed/f451/200/300", note: 4.4,
  ),
  Livre(
    id: 8, titre: "L'Étranger", auteur: "Albert Camus",
    resume: "Meursault, un homme indifférent au monde, confronté à l'absurde.",
    imageUrl: "https://picsum.photos/seed/etranger/200/300", note: 4.2,
  ),
];

// ---------- Racine de l'appli ----------
class CatalogueApp extends StatefulWidget {
  const CatalogueApp({super.key});

  @override
  State<CatalogueApp> createState() => _CatalogueAppState();
}

class _CatalogueAppState extends State<CatalogueApp> {
  final Set<int> _favorisIds = {}; // état central des favoris (remonté ici)

  void _toggleFavori(int livreId) {
    setState(() {
      if (_favorisIds.contains(livreId)) {
        _favorisIds.remove(livreId);
      } else {
        _favorisIds.add(livreId);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Catalogue de livres',
      theme: ThemeData(primarySwatch: Colors.brown, useMaterial3: true),
      home: PageAccueilCatalogue(
        favorisIds: _favorisIds,
        onToggleFavori: _toggleFavori,
      ),
    );
  }
}

// ---------- écran avec le Drawer (Liste et favoris) ----------
class PageAccueilCatalogue extends StatefulWidget {
  final Set<int> favorisIds;
  final void Function(int) onToggleFavori;

  const PageAccueilCatalogue({
    super.key,
    required this.favorisIds,
    required this.onToggleFavori,
  });

  @override
  State<PageAccueilCatalogue> createState() => _PageAccueilCatalogueState();
}

class _PageAccueilCatalogueState extends State<PageAccueilCatalogue> {
  bool _afficherFavorisUniquement = false;

  @override
  Widget build(BuildContext context) {
    final livresAffiches = _afficherFavorisUniquement
        ? catalogueLivres.where((l) => widget.favorisIds.contains(l.id)).toList()
        : catalogueLivres;

    return Scaffold(
      appBar: AppBar(title: Text(_afficherFavorisUniquement ? "Mes favoris" : "Catalogue")),
      drawer: Drawer(
        child: ListView(
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.brown),
              child: Text("Bibliothèque", style: TextStyle(color: Colors.white, fontSize: 24)),
            ),
            ListTile(
              leading: const Icon(Icons.list),
              title: const Text("Tout le catalogue"),
              selected: !_afficherFavorisUniquement,
              onTap: () {
                setState(() => _afficherFavorisUniquement = false);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.favorite),
              title: const Text("Mes favoris"),
              selected: _afficherFavorisUniquement,
              onTap: () {
                setState(() => _afficherFavorisUniquement = true);
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
      body: livresAffiches.isEmpty
          ? const Center(child: Text("Aucun livre favori pour le moment."))
          : ListView.builder(
              itemCount: livresAffiches.length,
              itemBuilder: (context, index) {
                final livre = livresAffiches[index];
                final estFavori = widget.favorisIds.contains(livre.id);
                return Card(
                  margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                  child: ListTile(
                    leading: ClipRRect(
                      borderRadius: BorderRadius.circular(4),
                      child: Image.network(livre.imageUrl, width: 50, fit: BoxFit.cover),
                    ),
                    title: Text(livre.titre, style: const TextStyle(fontWeight: FontWeight.bold)),
                    subtitle: Text(livre.auteur),
                    trailing: Icon(
                      estFavori ? Icons.favorite : Icons.favorite_border,
                      color: estFavori ? Colors.red : null,
                    ),
                    onTap: () async {
                      // On navigue vers le détail ET on écoute un éventuel changement de favori
                      await Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => PageDetailLivre(
                            livre: livre,
                            estFavori: widget.favorisIds.contains(livre.id),
                            onToggleFavori: () => widget.onToggleFavori(livre.id),
                          ),
                        ),
                      );
                      setState(() {}); // rafraîchit la liste au retour (si favori changé)
                    },
                  ),
                );
              },
            ),
    );
  }
}

// ---------- écran pour le détail ----------
class PageDetailLivre extends StatefulWidget {
  final Livre livre;
  final bool estFavori;
  final VoidCallback onToggleFavori; // callback vers le parent

  const PageDetailLivre({
    super.key,
    required this.livre,
    required this.estFavori,
    required this.onToggleFavori,
  });

  @override
  State<PageDetailLivre> createState() => _PageDetailLivreState();
}

class _PageDetailLivreState extends State<PageDetailLivre> {
  late bool _estFavori;

  @override
  void initState() {
    super.initState();
    _estFavori = widget.estFavori;
  }

  void _basculerFavori() {
    widget.onToggleFavori(); // notifie le parent (remontée d'état)
    setState(() => _estFavori = !_estFavori); // rafraîchit l'icône localement
  }

  @override
  Widget build(BuildContext context) {
    final livre = widget.livre;
    final etoilesPleines = livre.note.round();

    return Scaffold(
      appBar: AppBar(title: Text(livre.titre)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.network(livre.imageUrl, height: 220),
            ),
            const SizedBox(height: 16),
            Text(livre.titre, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            Text(livre.auteur, style: TextStyle(fontSize: 16, color: Colors.grey[600])),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: List.generate(5, (i) {
                return Icon(
                  i < etoilesPleines ? Icons.star : Icons.star_border,
                  color: Colors.amber,
                );
              }),
            ),
            const SizedBox(height: 16),
            Text(livre.resume, textAlign: TextAlign.center, style: const TextStyle(fontSize: 15)),
            const SizedBox(height: 24),
            ElevatedButton.icon(
              onPressed: _basculerFavori,
              icon: Icon(_estFavori ? Icons.favorite : Icons.favorite_border),
              label: Text(_estFavori ? "Retirer des favoris" : "Ajouter aux favoris"),
              style: ElevatedButton.styleFrom(
                backgroundColor: _estFavori ? Colors.red : null,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Points clés du corrigé :


⬅️ Retour au Chapitre 2 ➡️ Chapitre 3