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, ), ), ), ); }, ), ); } }
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 :
pubspec.yaml
flutter: uses-material-design: true assets: - assets/images/produits/
Suivi de la commande flutter pub get
flutter pub get
Vous chacun des exercices, vous pouvez choisir des couleurs et thèmes différents…
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"), ), ), ], ), ), ); } }
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], ); } }
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.
StatefulWidget
CatalogueApp
Provider
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é :
Set<int>
favorisIds
onToggleFavori
setState