
Introduction
State management is one of the most crucial aspects of Flutter app development. As your app grows beyond a few screens, managing state becomes increasingly complex—data needs to flow between widgets, user actions need to trigger updates, and the UI must stay in sync with your business logic.
Among the various options available, Provider and Riverpod are two of the most popular choices. Both were created by Remi Rousselet, with Riverpod being the spiritual successor designed to address Provider’s limitations. But which one should you choose for your project?
In this comprehensive guide, we’ll compare Provider and Riverpod with real code examples, covering their features, patterns, testing capabilities, and best practices to help you make an informed decision.
What is Provider?
Provider is a state management solution built on top of InheritedWidget. It is an official Flutter recommendation that makes state management simpler and more scalable. It wraps complex widget tree manipulation into a simple, declarative API.
Provider Setup
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1
Basic Provider Example
// lib/providers/counter_provider.dart
import 'package:flutter/foundation.dart';
class CounterProvider 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();
}
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
// Wrap app with providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CounterScreen(),
);
}
}
// lib/screens/counter_screen.dart
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Provider Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Consumer rebuilds only this widget when count changes
Consumer(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.headlineLarge,
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Using context.read for write-only operations
ElevatedButton(
onPressed: () => context.read().decrement(),
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () => context.read().increment(),
child: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}
Advanced Provider: Async Data with FutureProvider
// lib/providers/user_provider.dart
import 'package:flutter/foundation.dart';
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
class UserProvider extends ChangeNotifier {
User? _user;
bool _isLoading = false;
String? _error;
User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
bool get isAuthenticated => _user != null;
Future fetchUser(String userId) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Simulate API call
await Future.delayed(const Duration(seconds: 1));
_user = User(
id: userId,
name: 'John Doe',
email: 'john@example.com',
);
_isLoading = false;
notifyListeners();
} catch (e) {
_error = e.toString();
_isLoading = false;
notifyListeners();
}
}
void logout() {
_user = null;
notifyListeners();
}
}
// Usage in widget
class UserProfileScreen extends StatelessWidget {
const UserProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, userProvider, child) {
if (userProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (userProvider.error != null) {
return Center(child: Text('Error: ${userProvider.error}'));
}
final user = userProvider.user;
if (user == null) {
return const Center(child: Text('No user logged in'));
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
ElevatedButton(
onPressed: () => context.read().logout(),
child: const Text('Logout'),
),
],
),
);
},
);
}
}
Provider with ProxyProvider for Dependencies
// When one provider depends on another
class ApiService {
final String baseUrl;
ApiService(this.baseUrl);
Future
What is Riverpod?
Riverpod is a newer state management solution developed by the same author as Provider. It aims to resolve Provider’s limitations by offering compile-time safety, independence from the widget tree, and better testability.
Riverpod Setup
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0
Basic Riverpod Example
// lib/providers/counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
// Simple state provider
@riverpod
class Counter extends _$Counter {
@override
int build() => 0; // Initial state
void increment() => state++;
void decrement() {
if (state > 0) state--;
}
void reset() => state = 0;
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// Wrap app with ProviderScope
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CounterScreen(),
);
}
}
// lib/screens/counter_screen.dart
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch automatically rebuilds when state changes
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Riverpod Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$count',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).decrement(),
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}
Async Data with Riverpod
// lib/providers/user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_provider.g.dart';
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
// FutureProvider for async data - much cleaner than Provider
@riverpod
Future user(UserRef ref, String userId) async {
// Automatic cancellation when widget is disposed
// Automatic caching and deduplication
await Future.delayed(const Duration(seconds: 1));
return User(
id: userId,
name: 'John Doe',
email: 'john@example.com',
);
}
// Usage in widget - AsyncValue handles all states
class UserProfileScreen extends ConsumerWidget {
final String userId;
const UserProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
// AsyncValue automatically handles loading, error, and data states
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (user) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
ElevatedButton(
onPressed: () => ref.invalidate(userProvider(userId)),
child: const Text('Refresh'),
),
],
),
),
);
}
}
Riverpod with Dependencies
// Dependencies are handled automatically and type-safe
@riverpod
ApiService apiService(ApiServiceRef ref) {
return ApiService('https://api.example.com');
}
@riverpod
ProductRepository productRepository(ProductRepositoryRef ref) {
// Automatically disposes when no longer needed
// Compile-time error if apiService doesn't exist
final api = ref.watch(apiServiceProvider);
return ProductRepository(api);
}
@riverpod
Future> products(ProductsRef ref) async {
final repository = ref.watch(productRepositoryProvider);
return repository.getProducts();
}
// Family providers for parameterized data
@riverpod
Future product(ProductRef ref, String productId) async {
final repository = ref.watch(productRepositoryProvider);
return repository.getProduct(productId);
}
Riverpod StateNotifier Pattern
// For complex state with multiple operations
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'cart_provider.freezed.dart';
part 'cart_provider.g.dart';
@freezed
class CartState with _$CartState {
const factory CartState({
@Default([]) List items,
@Default(false) bool isCheckingOut,
String? error,
}) = _CartState;
}
@freezed
class CartItem with _$CartItem {
const factory CartItem({
required String productId,
required String name,
required double price,
required int quantity,
}) = _CartItem;
}
@riverpod
class Cart extends _$Cart {
@override
CartState build() => const CartState();
void addItem(Product product) {
final existingIndex = state.items.indexWhere(
(item) => item.productId == product.id,
);
if (existingIndex >= 0) {
// Update quantity
final items = [...state.items];
items[existingIndex] = items[existingIndex].copyWith(
quantity: items[existingIndex].quantity + 1,
);
state = state.copyWith(items: items);
} else {
// Add new item
state = state.copyWith(
items: [
...state.items,
CartItem(
productId: product.id,
name: product.name,
price: product.price,
quantity: 1,
),
],
);
}
}
void removeItem(String productId) {
state = state.copyWith(
items: state.items.where((item) => item.productId != productId).toList(),
);
}
void updateQuantity(String productId, int quantity) {
if (quantity <= 0) {
removeItem(productId);
return;
}
state = state.copyWith(
items: state.items.map((item) {
if (item.productId == productId) {
return item.copyWith(quantity: quantity);
}
return item;
}).toList(),
);
}
Future checkout() async {
state = state.copyWith(isCheckingOut: true, error: null);
try {
// Call checkout API
await Future.delayed(const Duration(seconds: 2));
state = const CartState(); // Clear cart on success
} catch (e) {
state = state.copyWith(isCheckingOut: false, error: e.toString());
}
}
}
// Computed values
@riverpod
double cartTotal(CartTotalRef ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (sum, item) => sum + (item.price * item.quantity));
}
@riverpod
int cartItemCount(CartItemCountRef ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (sum, item) => sum + item.quantity);
}
Provider vs Riverpod: Feature Comparison
| Feature | Provider | Riverpod |
|---|---|---|
| Compile-time safety | No – runtime errors | Yes – caught at compile time |
| Widget tree dependency | Required (InheritedWidget) | Independent |
| Auto-disposal | Manual | Automatic |
| Async support | Manual state management | Built-in AsyncValue |
| Family providers | Not native | First-class support |
| Computed values | ProxyProvider (verbose) | Simple provider composition |
| Testing | Requires mock widget tree | Override providers easily |
| Code generation | None | Optional but recommended |
| Learning curve | Lower | Moderate |
| Community size | Larger (older) | Growing rapidly |
Testing Comparison
Testing with Provider
// test/counter_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
void main() {
group('CounterProvider', () {
test('initial count is 0', () {
final counter = CounterProvider();
expect(counter.count, 0);
});
test('increment increases count', () {
final counter = CounterProvider();
counter.increment();
expect(counter.count, 1);
});
});
// Widget testing requires mock setup
testWidgets('Counter screen displays count', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ChangeNotifierProvider(
create: (_) => CounterProvider(),
child: const CounterScreen(),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
}
Testing with Riverpod
// test/counter_test.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CounterProvider', () {
test('initial count is 0', () {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(counterProvider), 0);
});
test('increment increases count', () {
final container = ProviderContainer();
addTearDown(container.dispose);
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
});
// Widget testing with easy overrides
testWidgets('Counter screen with mocked initial value', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Easy to override for testing
counterProvider.overrideWith(() => MockCounter()),
],
child: const MaterialApp(home: CounterScreen()),
),
);
expect(find.text('10'), findsOneWidget); // Mocked value
});
// Testing async providers
test('user provider returns user data', () async {
final container = ProviderContainer(
overrides: [
// Override API service for testing
apiServiceProvider.overrideWithValue(MockApiService()),
],
);
addTearDown(container.dispose);
final user = await container.read(userProvider('123').future);
expect(user.name, 'Mock User');
});
}
Which One Should You Choose?
Choose Provider When:
- You’re working on a small to medium-sized project with straightforward state needs
- Your team is new to Flutter and needs an easy learning curve
- You’re maintaining an existing codebase already using Provider
- You want minimal dependencies and no code generation
- The project has simple state requirements without complex async operations
Choose Riverpod When:
- You’re building a large-scale application that needs robust state management
- You want compile-time safety to catch errors before runtime
- Your app has complex async data flows (API calls, streams, caching)
- You need easy testing with provider overrides
- You want automatic disposal and memory management
- You’re starting a new project and can invest in learning the newer approach
Common Mistakes to Avoid
Using context.watch in callbacks: In Provider, always use context.read for event handlers and context.watch only in build methods.
Not disposing providers properly: With Provider, forgetting to dispose ChangeNotifiers leads to memory leaks. Riverpod handles this automatically.
Overusing notifyListeners: In Provider, calling notifyListeners() too frequently causes unnecessary rebuilds. Batch state changes when possible.
Watching when you should read: In Riverpod, use ref.read in callbacks and ref.watch in build methods. Using watch in callbacks creates subscription issues.
Not using select for granular updates: Both libraries support selecting specific parts of state to minimize rebuilds.
Conclusion
Both Provider and Riverpod are excellent choices for state management in Flutter. If you’re just starting or working on smaller projects, Provider is easier to grasp and implement—its integration with Flutter’s widget tree is natural and intuitive.
However, if you’re building production applications that require scalability, testability, and robust async handling, Riverpod is the way to go. Its compile-time safety, automatic disposal, and elegant async support (via AsyncValue) make complex state management significantly easier.
The trend in the Flutter community is moving toward Riverpod for new projects, while Provider remains the reliable choice for simpler apps and existing codebases.
For more on Flutter state management patterns, check out our guide on Provider vs Riverpod vs BLoC. For official documentation, visit riverpod.dev.