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

✅ Corrigés — Journée 2

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});

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

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

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 {
  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,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

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