Flutter Provider
Flutter Provider

-
Objectif
- Simplifier la gestion d’état dans une application Flutter.
- Partager des données entre widgets sans avoir besoin de passer des paramètres manuellement (lifting state up).
- Assurer une interface réactive, toujours synchronisée avec les données.
- Proposer une approche claire, simple et évolutive par rapport à
setState(). -
Qu’est-ce que « l’état » (State) ?
- Pour bien comprendre pourquoi le package Provider est si populaire, il faut d’abord définir ce qu’est « l’état » (ou State) dans le monde de Flutter.
- Dans Flutter, l’état représente toutes les données qui peuvent changer au cours de la vie de votre application et qui influencent ce que l’utilisateur voit à l’écran.
- On distingue généralement deux types d’états :
- L’état local (
Ephemeral State) : Des données qui ne concernent qu’un seul widget (ex: l’onglet actuellement sélectionné dans une barre de navigation, ou le texte en cours de saisie dans un champ). On utilise souvent un simple setState(). - L’état partagé (
App State) : Des données qui doivent être accessibles dans plusieurs parties de l’application (ex: les infos de l’utilisateur connecté, le contenu d’un panier d’achat, les préférences de thème). C’est ici que Provider intervient. -
Définition du package Provider
- Flutter Provider est une solution de gestion d’état pour les applications Flutter. Elle permet de gérer facilement l’état de l’application et de le partager entre plusieurs widgets, facilitant ainsi l’accès aux données là où elles sont nécessaires.
- Vous stockez les données dans un emplacement central appelé
provideret y accédez depuis n’importe quel widget de votre application. - Dès que les données changent, le
providerles met à jour, garantissant la cohérence et la réactivité de l’UI. - Les principaux avantages :
- Centralisation de l’état
- Découplage des widgets
- Optimisation des performances (rebuild ciblé)
-
Les types de Provider les plus courants
-
Provider (de base)
- Fournit un objet simple (service ou donnée en lecture seule).
- Ne notifie pas automatiquement les changements.
- Description :
-
Utilisé lorsque les données ne changent pas pendant l’exécution
(configuration, utilisateur connecté, service). - Exemple de vie réelle :
- Une carte d’identité : le nom est lu partout mais ne change pas.
- Exemple Flutter :
-
ChangeNotifierProvider <T>
- Utilisé pour des données modifiables.
- Notifie automatiquement les widgets quand l’état change.
- Description :
-
Basé sur la classe
ChangeNotifieret la méthode
notifyListeners(). - Exemple de vie réelle :
- Un compteur de personnes dans une salle : chaque entrée modifie le total.
- Cas typiques : panier, compteur, favoris, todo-list.
-
StreamProvider <T>
- Expose un
Streamde données en temps réel. - Description :
-
Les widgets se mettent à jour automatiquement dès qu’une nouvelle
donnée arrive dans le flux. - Exemple de vie réelle :
- Un tableau d’affichage dans une gare : les horaires changent en direct.
-
FutureProvider <T>
- Expose un
Futurepour des données asynchrones. - Description :
- Les données sont chargées une seule fois (appel API, chargement initial).
- Exemple de vie réelle :
- Commander un repas : on attend, puis on reçoit le résultat.
-
MultiProvider
- Regroupe plusieurs providers en un seul point.
- Description :
- Permet de garder un code propre dans les applications complexes.
- Exemple de vie réelle :
- Une multiprise : plusieurs appareils branchés sur une seule prise.
-
NotifierProvider <T extends Notifier>
- Gestion moderne de l’état.
- Ne nécessite pas
notifyListeners(). - Description :
- À chaque modification, un nouvel état est produit automatiquement.
- Exemple de vie réelle :
- Un tableau de score : l’écran affiche directement la nouvelle valeur.
-
AsyncNotifierProvider <T>
- Gestion asynchrone moderne (Future / Stream).
- Description :
-
Gère automatiquement les états :
chargement, erreur et données. - Exemple de vie réelle :
- Une commande en ligne : en cours → erreur ou succès.
-
Comment utiliser un package
providerFlutter ? - Étape 1 : ajouter la dépendance dans
pubspec.yaml. - Étape 2 : créer un modèle de données.
- Étape 3 : envelopper l’application dans un Provider.
- Étape 4 : consommer les données avec
context.watch,ConsumerouSelector. -
Utilisation directe de
.addListener - Vous pouvez écouter directement les changements d’un
ChangeNotifier. - Moins utilisé, mais utile pour du code métier hors widgets.
-
Comparaison des principaux Providers
-
Bonnes pratiques
- Préférer
context.readpour déclencher une action, etcontext.watchpour reconstruire l’UI. - Découper les providers pour garder des responsabilités claires.
- Utiliser
Selectorpour éviter des rebuilds inutiles. - Pour les projets modernes, envisager
NotifierProvider/AsyncNotifierProvider. -
Exemple de vie réelle : Panier d’achat (Shopping Cart)
-
Situation réelle
- Dans un magasin :
- Un client ajoute des produits à son panier
- Le nombre d’articles change
- Le total du panier se met à jour automatiquement
- Plusieurs écrans doivent accéder au même panier :
- Liste des produits
- Icône du panier
- Page de paiement
- C’est exactement un problème de gestion d’état partagé
-
Pourquoi utiliser Provider ici ?
- Il faudrait passer le panier de widget en widget (prop drilling)
- Le code devient complexe et difficile à maintenir
- Le panier est centralisé
- Tous les widgets sont synchronisés automatiquement
- Le code reste simple et propre
-
Structure du projet Flutter
-
Les Fichiers dart
-
Créer le dossier des images
- Utilise des noms sans espace et en minuscules.
- Déclarer les images dans pubspec.yaml
- Très important :
- Respecte l’indentation (2 espaces)
- Redémarre l’application (
flutter cleanpuisflutter runsi nécessaire) - Créer un modèle Product
models/product.dart-
Les Fichiers dart
-
Créer le dossier des images
- Utilise des noms sans espace et en minuscules.
- Déclarer les images dans pubspec.yaml
- Très important :
- Respecte l’indentation (2 espaces)
- Redémarre l’application (
flutter cleanpuisflutter runsi nécessaire) - Créer un modèle Product
models/product.dart

-
Utilisation
// Création
Provider<String>(
create: (context) => "Mohamed RAYES",
child: MonWidget(),
);
// Utilisation
Text(Provider.of<String>(context));
-
Utilisation
class Counter with ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners();
}
}
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),
);
Utilisation avec Consumer :
Consumer<Counter>(
builder: (context, counter, child) {
return Text('${counter.value}');
},
);
-
Utilisation
StreamProvider<int>(
create: (_) => Stream.periodic(
Duration(seconds: 1),
(i) => i,
),
initialData: 0,
child: MyApp(),
);
-
Utilisation
FutureProvider<User>(
create: (_) async => fetchUser(),
initialData: User.empty(),
child: MyApp(),
);
-
Utilisation
MultiProvider(
providers: [
Provider(create: (_) => MyService()),
ChangeNotifierProvider(create: (_) => Counter()),
StreamProvider(
create: (_) => myStream(),
initialData: 0,
),
],
child: MyApp(),
);
-
Utilisation
class Counter extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider =
NotifierProvider<Counter, int>(() => Counter());
Utilisation :
final count = context.watch(counterProvider);
-
Utilisation
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
return await fetchUser();
}
}
final userProvider =
AsyncNotifierProvider<UserNotifier, User>(
() => UserNotifier(),
);
Utilisation :
final user = context.watch(userProvider);
if (user is AsyncLoading) {
return CircularProgressIndicator();
} else if (user is AsyncError) {
return Text('Erreur');
} else {
return Text(user.value.name);
}
final counter = Counter();
counter.addListener(() {
print("Nouveau compteur: ${counter.value}");
});
| Provider | Type d’état | Méthode de mise à jour | Cas d’utilisation |
|---|---|---|---|
| ChangeNotifierProvider | Mutable (objet modifié) | notifyListeners() |
Compteur, panier, todo-list (UI dépendante de changements fréquents) |
Exemple :
|
|||
| NotifierProvider | Immuable (nouvel état) | Réaffectation de state |
Gestion moderne et performante (états synchrones) |
Exemple :
|
|||
| AsyncNotifierProvider | Asynchrone (Future / Stream) | Résultat asynchrone | API REST, Firebase, base de données distante |
Exemple :
|
|||
-
Sans Provider :
Avec Provider :
-
lib/
│
├── main.dart
├── app.dart
│
├── providers/
│ └── cart_provider.dart
│
├── pages/
│ ├── product_page.dart
│ └── cart_page.dart
Fichier main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app.dart';
import 'providers/cart_provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartProvider(),
child: MyApp(),
),
);
}
Fichier app.dart
import 'package:flutter/material.dart';
import 'pages/product_page.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Panier avec Provider',
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.blue),
home: ProductPage(),
);
}
}
Fichier providers/cart_provider.dart
import 'package:flutter/material.dart';
class CartProvider with ChangeNotifier {
final List<String> _items = [];
List<String> get items => _items;
int get totalItems => _items.length;
void addItem(String item) {
_items.add(item);
notifyListeners();
}
void removeItem(String item) {
_items.remove(item);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
Fichier pages/product_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import 'cart_page.dart';
class ProductPage extends StatelessWidget {
final List<String> products = [
"Laptop",
"Smartphone",
"Casque",
"Clavier",
];
@override
Widget build(BuildContext context) {
final cart = context.read<CartProvider>();
return Scaffold(
appBar: AppBar(
title: Text("Produits"),
actions: [
Consumer<CartProvider>(
builder: (context, cart, child) {
return IconButton(
icon: Stack(
children: [
Icon(Icons.shopping_cart),
if (cart.totalItems > 0)
Positioned(
right: 0,
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.red,
child: Text(
cart.totalItems.toString(),
style: TextStyle(
fontSize: 10,
color: Colors.white,
),
),
),
),
],
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => CartPage()),
);
},
);
},
),
],
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (_, index) {
return ListTile(
title: Text(products[index]),
trailing: IconButton(
icon: Icon(Icons.add),
onPressed: () {
cart.addItem(products[index]);
},
),
);
},
),
);
}
}
Fichier pages/cart_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
class CartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartProvider>();
return Scaffold(
appBar: AppBar(
title: Text("Mon Panier"),
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
cart.clearCart();
},
),
],
),
body: cart.items.isEmpty
? Center(child: Text("Panier vide"))
: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (_, index) {
final item = cart.items[index];
return ListTile(
title: Text(item),
trailing: IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () {
cart.removeItem(item);
},
),
);
},
),
);
}
}
-
/assets
/images
laptop.png
smartphone.png
casque.png
clavier.png
flutter:
assets:
- assets/images/laptop.png
- assets/images/smartphone.png
- assets/images/casque.png
- assets/images/clavier.png
class Product {
final String name;
final String image;
Product({required this.name, required this.image});
}
Fichier main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app.dart';
import 'providers/cart_provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CartProvider()),
],
child: const MyApp(),
),
);
}
Fichier app.dart
import 'package:flutter/material.dart';
import 'pages/product_page.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "Exemple Provider",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ProductPage(),
);
}
}
Fichier providers/cart_provider.dart
import 'package:flutter/material.dart';
import '../models/product.dart';
class CartProvider extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => _items;
int get totalItems => _items.length;
void addItem(Product product) {
_items.add(product);
notifyListeners();
}
void removeItem(Product product) {
_items.remove(product);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
Fichier pages/product_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/product.dart';
import '../providers/cart_provider.dart';
import 'cart_page.dart';
class ProductPage extends StatelessWidget {
ProductPage({super.key});
final List<Product> products = [
Product(name: "Laptop", image: "assets/images/laptop.png"),
Product(name: "Smartphone", image: "assets/images/smartphone.png"),
Product(name: "Casque", image: "assets/images/casque.png"),
Product(name: "Clavier", image: "assets/images/clavier.png"),
];
@override
Widget build(BuildContext context) {
final cart = context.read<CartProvider>();
return Scaffold(
appBar: AppBar(
title: const Text("Nos Produits"),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 100),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
radius: 26,
backgroundImage: AssetImage(product.image),
),
title: Text(product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: const Text("Produit disponible"),
trailing: IconButton(
icon: const Icon(Icons.add_circle, color: Colors.green, size: 28),
onPressed: () => cart.addItem(product),
),
),
);
},
),
bottomNavigationBar: Consumer<CartProvider>(
builder: (context, cart, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black12,
offset: Offset(0, -2),
)
],
),
child: ElevatedButton(
onPressed: cart.totalItems == 0
? null
: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const CartPage()));
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.shopping_cart, size: 26),
const SizedBox(width: 10),
Text("Voir le panier (${cart.totalItems})", style: const TextStyle(fontSize: 18)),
],
),
),
);
},
),
);
}
}
Fichier pages/cart_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch<CartProvider>();
return Scaffold(
appBar: AppBar(
title: const Text("Mon Panier"),
centerTitle: true,
actions: [
if (cart.items.isNotEmpty)
IconButton(
tooltip: "Vider le panier",
icon: const Icon(Icons.delete_outline),
onPressed: cart.clearCart,
),
],
),
body: cart.items.isEmpty
? _buildEmptyCart()
: Column(
children: [
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: cart.items.length,
itemBuilder: (_, index) {
final product = cart.items[index];
return Card(
elevation: 3,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage(product.image),
radius: 26,
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
trailing: IconButton(
icon: const Icon(
Icons.remove_circle,
color: Colors.red,
size: 28,
),
onPressed: () => cart.removeItem(product),
),
),
);
},
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Colors.black12,
offset: Offset(0, -2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Total articles", style: TextStyle(fontSize: 16)),
Text("${cart.totalItems}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)),
],
),
),
],
),
);
}
Widget _buildEmptyCart() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart_outlined, size: 80, color: Colors.grey),
SizedBox(height: 16),
Text("Votre panier est vide", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text("Ajoutez des produits pour commencer", style: TextStyle(color: Colors.grey)),
],
),
);
}
}
Fichier models/product.dart
class Product {
final String name;
final String image;
Product({required this.name, required this.image});
}
-
/assets
/images
laptop.png
smartphone.png
casque.png
clavier.png
flutter:
assets:
- assets/images/
class Product {
final String name;
final String image;
Product({required this.name, required this.image});
}
