import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CompteurProvider extends ChangeNotifier { int _valeur = 0; int get valeur => _valeur; void incrementer() { _valeur++; notifyListeners(); } } void main() { runApp( ChangeNotifierProvider( create: (_) => CompteurProvider(), child: const MonApp(), ), ); } class MonApp extends StatelessWidget { const MonApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(home: PageCompteurProvider()); } } class PageCompteurProvider extends StatelessWidget { const PageCompteurProvider({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Provider : compteur")), body: const Center(child: AfficheurValeur()), // widget "frère" ou "soeur" qui affiche floatingActionButton: const BoutonIncrementer(), // widget "frère" ou " soeur" qui modifie ); } } // Widget qui LIT la valeur : utilise Consumer -> se reconstruit à chaque changement class AfficheurValeur extends StatelessWidget { const AfficheurValeur({super.key}); @override Widget build(BuildContext context) { return Consumer<CompteurProvider>( builder: (context, provider, child) => Text( "Valeur : ${provider.valeur}", style: const TextStyle(fontSize: 32), ), ); } } // Widget qui MODIFIE la valeur : utilise context.read (pas de reconstruction inutile) class BoutonIncrementer extends StatelessWidget { const BoutonIncrementer({super.key}); @override Widget build(BuildContext context) { return FloatingActionButton( onPressed: () => context.read<CompteurProvider>().incrementer(), child: const Icon(Icons.add), ); } }
Point clé : AfficheurValeur et BoutonIncrementer sont 2 widgets totalement indépendants, sans lien parent-enfant direct ni callback manuel. Ils communiquent uniquement via le CompteurProvider, exactement comme 2 @Service Spring communiqueraient via un composant injecté commun.
AfficheurValeur
BoutonIncrementer
CompteurProvider
@Service
import 'package:flutter/material.dart'; class PageConnexion extends StatefulWidget { const PageConnexion({super.key}); @override State<PageConnexion> createState() => _PageConnexionState(); } class _PageConnexionState extends State<PageConnexion> { final _formKey = GlobalKey<FormState>(); final _emailCtrl = TextEditingController(); final _mdpCtrl = TextEditingController(); @override void dispose() { _emailCtrl.dispose(); _mdpCtrl.dispose(); super.dispose(); } void _connecter() { if (_formKey.currentState!.validate()) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Bienvenue ${_emailCtrl.text} !")), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Connexion")), body: Padding( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailCtrl, decoration: const InputDecoration(labelText: "Email", border: OutlineInputBorder()), keyboardType: TextInputType.emailAddress, validator: (v) { if (v == null || v.isEmpty) return "L'email est requis"; final regex = RegExp(r'^[\w\.\-]+@[\w\-]+\.[a-zA-Z]{2,}$'); if (!regex.hasMatch(v)) return "Format d'email invalide"; return null; }, ), const SizedBox(height: 16), TextFormField( controller: _mdpCtrl, decoration: const InputDecoration(labelText: "Mot de passe", border: OutlineInputBorder()), obscureText: true, validator: (v) { if (v == null || v.isEmpty) return "Le mot de passe est requis"; return null; }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton(onPressed: _connecter, child: const Text("Se connecter")), ), ], ), ), ), ); } }
Rendu final :
Objectif illustré :
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() => runApp(const MonAppTheme()); class MonAppTheme extends StatefulWidget { const MonAppTheme({super.key}); @override State<MonAppTheme> createState() => _MonAppThemeState(); } class _MonAppThemeState extends State<MonAppTheme> { bool _modeSombre = false; bool _chargementTermine = false; @override void initState() { super.initState(); _chargerPreference(); } Future<void> _chargerPreference() async { final prefs = await SharedPreferences.getInstance(); setState(() { _modeSombre = prefs.getBool('mode_sombre') ?? false; _chargementTermine = true; }); } Future<void> _changerMode(bool valeur) async { setState(() => _modeSombre = valeur); final prefs = await SharedPreferences.getInstance(); await prefs.setBool('mode_sombre', valeur); } @override Widget build(BuildContext context) { if (!_chargementTermine) { return const MaterialApp(home: Scaffold(body: Center(child: CircularProgressIndicator()))); } return MaterialApp( themeMode: _modeSombre ? ThemeMode.dark : ThemeMode.light, theme: ThemeData.light(), darkTheme: ThemeData.dark(), home: PageParametresTheme(modeSombre: _modeSombre, onChangerMode: _changerMode), ); } } class PageParametresTheme extends StatelessWidget { final bool modeSombre; final ValueChanged<bool> onChangerMode; const PageParametresTheme({super.key, required this.modeSombre, required this.onChangerMode}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Paramètres")), body: Center( child: SwitchListTile( title: const Text("Mode sombre"), value: modeSombre, onChanged: onChangerMode, ), ), ); } }
💡 Point clé : On affiche un CircularProgressIndicator tant que la préférence n’est pas encore chargée depuis le disque (opération asynchrone). C’est un pattern que vous retrouverez systématiquement pendant la journée 4 avec les appels réseau.
CircularProgressIndicator
Illustrations de la correction :
Vous pouvez la réaliser que partiellement et la terminer la prochaine fois…
// ======================= main.dart ======================= import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { runApp( ChangeNotifierProvider( create: (_) => TacheProvider()..chargerDepuisStockage(), child: const TodoApp(), ), ); } // ---------- le Modèle ---------- class Tache { final String id; String titre; bool estFaite; Tache({required this.id, required this.titre, this.estFaite = false}); Map<String, dynamic> toJson() => {'id': id, 'titre': titre, 'estFaite': estFaite}; factory Tache.fromJson(Map<String, dynamic> json) => Tache( id: json['id'], titre: json['titre'], estFaite: json['estFaite'], ); } // ---------- notre Provider ---------- class TacheProvider extends ChangeNotifier { final List<Tache> _taches = []; bool _modeSombre = false; List<Tache> get taches => List.unmodifiable(_taches); bool get modeSombre => _modeSombre; int get nombreRestantes => _taches.where((t) => !t.estFaite).length; Future<void> chargerDepuisStockage() async { final prefs = await SharedPreferences.getInstance(); final jsonString = prefs.getString('taches'); _modeSombre = prefs.getBool('mode_sombre') ?? false; if (jsonString != null) { final List<dynamic> liste = jsonDecode(jsonString); _taches.addAll(liste.map((e) => Tache.fromJson(e))); } notifyListeners(); } Future<void> _sauvegarder() async { final prefs = await SharedPreferences.getInstance(); final jsonString = jsonEncode(_taches.map((t) => t.toJson()).toList()); await prefs.setString('taches', jsonString); } void ajouterTache(String titre) { if (titre.trim().isEmpty) return; _taches.add(Tache(id: DateTime.now().millisecondsSinceEpoch.toString(), titre: titre.trim())); notifyListeners(); _sauvegarder(); } void basculerEtat(String id) { final tache = _taches.firstWhere((t) => t.id == id); tache.estFaite = !tache.estFaite; notifyListeners(); _sauvegarder(); } void supprimerTache(String id) { _taches.removeWhere((t) => t.id == id); notifyListeners(); _sauvegarder(); } Future<void> basculerModeSombre(bool valeur) async { _modeSombre = valeur; notifyListeners(); final prefs = await SharedPreferences.getInstance(); await prefs.setBool('mode_sombre', valeur); } } // ---------- notre application ---------- class TodoApp extends StatelessWidget { const TodoApp({super.key}); @override Widget build(BuildContext context) { final provider = context.watch<TacheProvider>(); return MaterialApp( title: 'Todo List', themeMode: provider.modeSombre ? ThemeMode.dark : ThemeMode.light, theme: ThemeData.light(useMaterial3: true), darkTheme: ThemeData.dark(useMaterial3: true), home: const PageTodoList(), ); } } // ---------- l'écran principal ---------- class PageTodoList extends StatefulWidget { const PageTodoList({super.key}); @override State<PageTodoList> createState() => _PageTodoListState(); } class _PageTodoListState extends State<PageTodoList> { final _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final provider = context.watch<TacheProvider>(); return Scaffold( appBar: AppBar( title: Text("Mes tâches (${provider.nombreRestantes} restantes)"), actions: [ Switch( value: provider.modeSombre, onChanged: (v) => context.read<TacheProvider>().basculerModeSombre(v), ), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( child: TextField( controller: _controller, decoration: const InputDecoration( hintText: "Nouvelle tâche...", border: OutlineInputBorder(), ), onSubmitted: (_) => _ajouter(context), ), ), const SizedBox(width: 8), ElevatedButton(onPressed: () => _ajouter(context), child: const Icon(Icons.add)), ], ), ), Expanded( child: provider.taches.isEmpty ? const Center(child: Text("Aucune tâche. Ajoutez-en une !")) : ListView.builder( itemCount: provider.taches.length, itemBuilder: (context, index) { final tache = provider.taches[index]; return ListTile( leading: Checkbox( value: tache.estFaite, onChanged: (_) => context.read<TacheProvider>().basculerEtat(tache.id), ), title: Text( tache.titre, style: TextStyle( decoration: tache.estFaite ? TextDecoration.lineThrough : null, color: tache.estFaite ? Colors.grey : null, ), ), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => context.read<TacheProvider>().supprimerTache(tache.id), ), ); }, ), ), ], ), ); } void _ajouter(BuildContext context) { context.read<TacheProvider>().ajouterTache(_controller.text); _controller.clear(); } }
Points clés du corrigé avec quelques explications :
create: (_) => TacheProvider()..chargerDepuisStockage(),
(_)
TacheProvider()
=>
chargerDepuisStockage()
..
TacheProvider
toJson
fromJson
..chargerDepuisStockage()
main()
shared_preferences
Le _ est un paramètre volontairement ignoré : Le create: du ChangeNotifierProvider exige une fonction de fabrique avec cette signature :
_
create: (context) => ... // reçoit un BuildContext en paramètre
Cependant, ici, TacheProvider n’a pas besoin du contexte pour être créé. Dart permet de nommer un paramètre inutilisé _ **. C’est une convention universelle signifiant “je reçois quelque chose, mais je m’en fiche”. C’est purement documentaire. Que vous écriviez (context) ou (_), ça compile pareil, mais _ indique que **“ce paramètre n’est pas utilisé ici”.
(context)
create: (_) => ... // le contexte est là, mais ignoré create: (context) => ... // identique techniquement, mais suggère qu'on l'utilise
Vous retrouverez cette convention partout : (, index) =>** …, **(, titre) => …, etc. On garde _ pour le(s) paramètre(s) ignorés, et des vrais noms pour ceux qu’on utilise : (context, index) => … → (context, _) => … .
La flèche => c’est la fonction fléchée (arrow syntax) : C’est l’équivalent exact de la lambda Java (->) ou de la fonction fléchée
On pourrait écrire le code comme ceci :
create: (context) { return TacheProvider(); }
ou bien de manière plus concise comme cela :
create: (context) => TacheProvider()
2 règles :
Elle apparaît partout : itemBuilder: (context, index) => Card(…), onPressed: () => print(‘ok’), etc.
La cascade .. ou “enchaînement sur la même instance” : C’est la seule syntaxe vraiment propre à Dart des trois ! En fait, la cascade dit : “Continue à travailler sur l’objet que tu viens de créer/obtenir, sans le réassigner.”
Comparaison claire :
// Version verbeuse final provider = TacheProvider(); provider.chargerDepuisStockage(); return provider; // Version cascade TacheProvider()..chargerDepuisStockage() // └────────── retourne TOUJOURS le TacheProvider, PAS le résultat de la méthode
Attention : La nuance capitale : avec un point simple ., l’expression TacheProvider().chargerDepuisStockage() vaudrait ce que renvoie chargerDepuisStockage() (probablement void ou un Future). Alors qu’avec .., l’expression vaut le TacheProvider lui-même, enrichi de l’effet de bord. Donc, notre create: reçoit bien un TacheProvider, comme attendu.
.
create:
La cascade est aussi pratique pour configurer un objet en série :
final controller = TextEditingController() ..text = "Bonjour" ..selection = TextSelection.collapsed(offset: 8); // controller vaut bien le TextEditingController, avec les deux réglages appliqués
Un point d’attention sur ce code précis : Si chargerDepuisStockage() est async (ce qui est le cas ici avec le SharedPreferences), la cascade n’attend pas le Future :
TacheProvider()..chargerDepuisStockage() // └── la méthode async démarre, mais on n'attend pas sa fin
Ceci n’est pas une erreur : le provider est créé immédiatement, la donnée arrive quelques millisecondes plus tard, et notifyListeners() (appelé dans chargerDepuisStockage à la fin du chargement) préviendra les widgets abonnés. Le mécanisme Observer que nous avons vu fait très bien ce travail “en retard”. C’est même le pattern courant pour un chargement initial. Mais sachez que c’est du fire-and-forget (lance et oublie).