Skip to content

Package Provider

Provider présente plusieurs avantages significatifs pour la gestion d’état dans Flutter :

  • Utilise les mécanismes natifs de Flutter : InheritedWidget
  • Moins verbeux que Bloc
  • Pas de génération de code
  • Intégration naturelle avec l’écosystème Flutter

Ce mécanisme permet à un widget de retrouver l’instance d’un widget “parent” dans l’arbre de widgets. Cela permet d’éviter de longues chaînes d’injection de paramètres.

Flutter utilise déjà ce pattern dans de nombreuses APIs :

  • MediaQuery.of(context) - Informations sur l’écran
  • Navigator.of(context) - Navigation
  • Theme.of(context) - Thème de l’application
// Exemples d'utilisation native
final screenSize = MediaQuery.of(context).size;
final theme = Theme.of(context);
Navigator.of(context).push(/*...*/);

Les providers proposent deux constructeurs principaux selon votre cas d’usage :

Pour utiliser un objet existant que vous avez déjà instancié :

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final user = User('joe@mail.com');
return Provider.value(
value: user,
child: const MaterialApp(home: MainScreen()),
);
}
}

Pour instancier un nouvel objet via une fonction create :

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => UserService(),
child: const MaterialApp(home: MainScreen()),
);
}
}

Deux syntaxes sont disponibles pour récupérer les objets depuis le provider :

  • Syntaxe classique : Provider.of<AuthService>(context);
  • Extension de méthodes : context.read<AuthService>();
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final user = context.read<User>();
return Scaffold(
appBar: AppBar(title: Text(user.mail)),
body: const MainView(),
);
}
}
class ExampleWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// read() : pour les actions (ne rebuild pas le widget)
final userService = context.read<UserService>();
// watch() : pour l'écoute (rebuild le widget automatiquement)
final user = context.watch<User>();
return Column(
children: [
Text('Utilisateur: ${user.name}'),
ElevatedButton(
onPressed: () => userService.logout(), // Utilise read()
child: Text('Déconnexion'),
),
],
);
}
}

Provider offre plusieurs types spécialisés selon le type de données que vous gérez :

Pour les objets qui implémentent ChangeNotifier ou ValueNotifier :

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => TimeController(),
child: const MaterialApp(home: MainScreen()),
);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final timeController = context.watch<TimeController>();
return Scaffold(
appBar: AppBar(
title: Text(timeController.value.toIso8601String()),
),
body: const MainView(),
);
}
}

Pour les flux de données (Stream) :

StreamProvider<List<Message>>(
create: (context) => messageService.messagesStream,
initialData: const [],
child: MessageListWidget(),
)

Pour les opérations asynchrones uniques :

FutureProvider<UserProfile>(
create: (context) => userService.loadProfile(),
initialData: UserProfile.empty(),
child: ProfileWidget(),
)

Pour les données statiques ou les services :

Provider<ApiService>(
create: (context) => ApiService(apiKey: 'your-key'),
child: MyApp(),
)

Pour déclarer plusieurs Providers en une fois et éviter l’imbrication excessive :

MultiProvider(
providers: [
Provider.value(value: userService),
Provider.value(value: messageService),
ChangeNotifierProvider(create: (context) => CartController()),
StreamProvider<List<Notification>>(
create: (context) => notificationService.stream,
initialData: const [],
),
],
child: MaterialApp(home: MainScreen()),
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Services
Provider<ApiService>(
create: (context) => ApiService(),
),
Provider<AuthService>(
create: (context) => AuthService(context.read<ApiService>()),
),
// Controllers avec état
ChangeNotifierProvider<AuthController>(
create: (context) => AuthController(context.read<AuthService>()),
),
ChangeNotifierProvider<CartController>(
create: (context) => CartController(),
),
// Streams
StreamProvider<ConnectivityStatus>(
create: (context) => ConnectivityService().statusStream,
initialData: ConnectivityStatus.unknown,
),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

Pour définir des dépendances entre Providers, utilisez ProxyProvider :

MultiProvider(
providers: [
ChangeNotifierProvider.value(value: authController),
ProxyProvider<AuthController, FeedService>(
update: (_, controller, service) =>
FeedService(FeedClient(controller.user?.token)),
lazy: true,
),
],
child: widget.child,
);
ProxyProvider2<AuthController, ApiService, UserRepository>(
update: (context, auth, api, previous) => UserRepository(
apiService: api,
authToken: auth.user?.token,
),
child: MyWidget(),
)

Pour écouter les changements dans une partie spécifique du widget :

class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Panier')),
body: Consumer<CartController>(
builder: (context, cart, child) {
return ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return ListTile(
title: Text(item.name),
subtitle: Text('\$${item.price}'),
trailing: IconButton(
icon: Icon(Icons.remove),
onPressed: () => cart.removeItem(item.id),
),
);
},
);
},
),
);
}
}
Selector<CartController, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Badge(
label: Text('$itemCount'),
child: Icon(Icons.shopping_cart),
);
},
)

Provider gère automatiquement le cycle de vie des objets qu’il crée :

ChangeNotifierProvider(
create: (context) => MyController(),
// Provider appellera automatiquement dispose() sur MyController
// quand le provider sera supprimé de l'arbre de widgets
child: MyWidget(),
)
ChangeNotifierProvider(
create: (context) => MyController(),
dispose: (context, controller) {
// Logique de nettoyage personnalisée
controller.cleanup();
controller.dispose();
},
child: MyWidget(),
)
class ConditionalProviderExample extends StatelessWidget {
final bool useLocalData;
ConditionalProviderExample({required this.useLocalData});
@override
Widget build(BuildContext context) {
return Provider<DataService>(
create: (context) => useLocalData
? LocalDataService()
: RemoteDataService(),
child: DataWidget(),
);
}
}
class ParameterizedProvider extends StatelessWidget {
final String userId;
ParameterizedProvider({required this.userId});
@override
Widget build(BuildContext context) {
return Provider<UserProfileService>(
create: (context) => UserProfileService(userId: userId),
child: UserProfileWidget(),
);
}
}
// ✅ Bon : hiérarchie logique
MultiProvider(
providers: [
// Services de base
Provider<ApiService>(create: (context) => ApiService()),
// Services dépendants
ProxyProvider<ApiService, AuthService>(
update: (context, api, previous) => AuthService(api),
),
// Controllers
ChangeNotifierProxyProvider<AuthService, UserController>(
create: (context) => UserController(),
update: (context, auth, controller) => controller..updateAuth(auth),
),
],
child: MyApp(),
)
// ❌ Mauvais : provider pour données statiques simples
Provider<String>.value(
value: 'Hello World',
child: MyWidget(),
)
// ✅ Bon : passer directement en paramètre
MyWidget(message: 'Hello World')
void main() {
testWidgets('should display user name', (tester) async {
final user = User(name: 'John Doe');
await tester.pumpWidget(
Provider<User>.value(
value: user,
child: MaterialApp(
home: UserWidget(),
),
),
);
expect(find.text('John Doe'), findsOneWidget);
});
}

Le package Provider offre une solution robuste et flexible pour la gestion d’état dans Flutter. Il combine :

  • Simplicité d’utilisation avec les extensions de méthodes
  • Performance optimisée grâce aux mécanismes natifs Flutter
  • Flexibilité avec de nombreux types de providers
  • Testabilité excellente grâce à l’injection de dépendances

Provider est idéal pour les applications de moyenne à grande complexité où vous avez besoin de partager l’état entre plusieurs widgets tout en maintenant une architecture claire et testable.