Provider Package Setup
What is Provider?
Provider is the recommended state management solution by the Flutter team for simple to moderate complexity apps. It is a wrapper around InheritedWidget that makes it easier to create, provide, and consume state objects throughout your widget tree. Instead of manually building InheritedWidget or InheritedNotifier subclasses, Provider handles all the boilerplate for you.
InheritedWidget, automatically handles disposal, supports lazy initialization, and integrates seamlessly with ChangeNotifier. It is the bridge between manual state management and more advanced solutions like Riverpod or Bloc.
Installing the Provider Package
Add the provider package to your project using the Flutter CLI or by editing pubspec.yaml directly.
Adding Provider to Your Project
# Using the Flutter CLI (recommended):
flutter pub add provider
# Or manually add to pubspec.yaml:
# dependencies:
# flutter:
# sdk: flutter
# provider: ^6.1.2
# Then run:
flutter pub get
Importing Provider
import 'package:provider/provider.dart';
ChangeNotifierProvider
ChangeNotifierProvider is the most commonly used provider type. It creates a ChangeNotifier instance, provides it to descendants, listens for changes, and automatically disposes the notifier when the provider is removed from the tree.
Creating a Model for Provider
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (_count > 0) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
Providing the Model with ChangeNotifierProvider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider Demo',
home: const CounterPage(),
);
}
}
create callback is called lazily by default — the CounterModel is only instantiated when a descendant first tries to access it. Provider also handles disposal automatically: when the ChangeNotifierProvider is removed from the tree, it calls dispose() on the CounterModel for you.
Consumer Widget
The Consumer widget is the most explicit way to read a provided value. It rebuilds its builder whenever the model calls notifyListeners(). Like ValueListenableBuilder, it accepts a child parameter for optimization.
Using Consumer to Read State
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Provider Counter')),
body: Center(
child: Consumer<CounterModel>(
builder: (context, counter, child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
child!, // Static label — never rebuilds
Text(
'\${counter.count}',
style: const TextStyle(fontSize: 48),
),
],
);
},
child: const Text(
'Current Count:',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
),
floatingActionButton: Consumer<CounterModel>(
builder: (context, counter, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'decrement',
onPressed: counter.decrement,
child: const Icon(Icons.remove),
),
const SizedBox(width: 8),
FloatingActionButton(
heroTag: 'increment',
onPressed: counter.increment,
child: const Icon(Icons.add),
),
],
);
},
),
);
}
}
context.watch vs context.read
Provider extends BuildContext with two essential methods: watch and read. Understanding when to use each is critical for correct behavior and performance.
context.watch — Reactive (Rebuilds on Change)
// Use watch inside build() to rebuild when state changes
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// This widget rebuilds whenever CounterModel notifies
final counter = context.watch<CounterModel>();
return Text(
'Count: \${counter.count}',
style: const TextStyle(fontSize: 32),
);
}
}
context.read — One-shot (No Rebuild)
// Use read in callbacks to access the model without subscribing
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// read does NOT cause this widget to rebuild
context.read<CounterModel>().increment();
},
child: const Text('Increment'),
);
}
}
context.read inside a build method to display data. The widget will not rebuild when the data changes, leading to stale UI. Conversely, never use context.watch inside event handlers (like onPressed) — it is only valid during the build phase.
Provider.of
Provider.of<T>(context) is the original API for accessing provided values. By default it behaves like context.watch (listens to changes). Pass listen: false to make it behave like context.read.
Provider.of Usage
// Equivalent to context.watch<CounterModel>()
final counter = Provider.of<CounterModel>(context);
// Equivalent to context.read<CounterModel>()
final counter = Provider.of<CounterModel>(context, listen: false);
context.watch and context.read over Provider.of. They are more concise, less error-prone, and clearly communicate intent. Use Provider.of only when you need backward compatibility or specific edge cases.
When to Use Each Access Method
Here is a practical guide for choosing the right access method:
Access Method Decision Guide
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
// 1. WATCH: Display data that should update reactively
final counter = context.watch<CounterModel>();
return Column(
children: [
// Reactive display
Text('Count: \${counter.count}'),
// 2. READ: Call methods in callbacks
ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('Add'),
),
// 3. CONSUMER: Scope rebuilds to a small subtree
Consumer<CounterModel>(
builder: (context, model, child) {
return Text('Total: \${model.count}');
},
),
],
);
}
}
// Summary:
// context.watch<T>() → Inside build(), need reactive updates
// context.read<T>() → Inside callbacks, one-time access
// Consumer<T> → Scope rebuilds to specific subtree
// Provider.of<T>() → Legacy API, use watch/read instead
Practical Example: Counter App with Provider
Here is a complete, production-ready counter application using Provider, demonstrating all the concepts covered in this lesson.
Complete Counter App with Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Model
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
bool get canDecrement => _count > 0;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (canDecrement) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
// Entry point
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider Counter',
theme: ThemeData(useMaterial3: true),
home: const CounterHomePage(),
);
}
}
class CounterHomePage extends StatelessWidget {
const CounterHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Provider Counter'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => context.read<CounterModel>().reset(),
),
],
),
body: const Center(child: CounterDisplay()),
floatingActionButton: const CounterFABs(),
);
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CounterModel>().count;
return Text(
'\$count',
style: Theme.of(context).textTheme.displayLarge,
);
}
}
class CounterFABs extends StatelessWidget {
const CounterFABs({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'inc',
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
Consumer<CounterModel>(
builder: (context, model, _) {
return FloatingActionButton.small(
heroTag: 'dec',
onPressed: model.canDecrement ? model.decrement : null,
backgroundColor: model.canDecrement ? null : Colors.grey,
child: const Icon(Icons.remove),
);
},
),
],
);
}
}
Practical Example: User Profile Provider
A more realistic example managing user profile state across multiple screens.
User Profile with Provider
class UserProfile extends ChangeNotifier {
String _name = '';
String _email = '';
String? _avatarUrl;
bool _isLoading = false;
String get name => _name;
String get email => _email;
String? get avatarUrl => _avatarUrl;
bool get isLoading => _isLoading;
bool get isLoggedIn => _email.isNotEmpty;
Future<void> loadProfile(String userId) async {
_isLoading = true;
notifyListeners();
try {
// Simulate API call
await Future.delayed(const Duration(seconds: 1));
_name = 'Edrees Salih';
_email = 'edrees@example.com';
_avatarUrl = 'https://example.com/avatar.jpg';
} catch (e) {
// Handle error
} finally {
_isLoading = false;
notifyListeners();
}
}
void updateName(String newName) {
_name = newName;
notifyListeners();
}
void logout() {
_name = '';
_email = '';
_avatarUrl = null;
notifyListeners();
}
}
// Providing at the app level:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterModel()),
ChangeNotifierProvider(create: (_) => UserProfile()),
],
child: const MyApp(),
),
);
}
// Using in a profile screen:
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final profile = context.watch<UserProfile>();
if (profile.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
if (profile.avatarUrl != null)
CircleAvatar(
backgroundImage: NetworkImage(profile.avatarUrl!),
radius: 40,
),
Text(profile.name, style: const TextStyle(fontSize: 24)),
Text(profile.email, style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<UserProfile>().logout(),
child: const Text('Logout'),
),
],
);
}
}
MultiProvider is a convenience widget that lets you provide multiple models without deep nesting. Each provider in the list is independent and can be accessed individually by descendants.
Summary
- Provider is a wrapper around
InheritedWidgetthat simplifies state management. - ChangeNotifierProvider creates, provides, and auto-disposes a
ChangeNotifier. - Consumer rebuilds its builder when the model notifies; supports a
childparameter for optimization. - context.watch subscribes to changes and triggers rebuilds — use inside
build(). - context.read accesses the model without subscribing — use inside callbacks and event handlers.
- Provider.of is the legacy API; prefer
watchandreadfor clarity. - MultiProvider lets you provide multiple models at the same level without nesting.