DartFlutter

Flutter State Management: Provider vs Riverpod – Which One to Choose?

Provider Vs Riverpod 1024x683

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> get(String endpoint) async {
    // HTTP implementation
    return {};
  }
}

class ProductRepository {
  final ApiService _api;
  
  ProductRepository(this._api);
  
  Future> getProducts() async {
    final response = await _api.get('/products');
    // Parse response
    return [];
  }
}

// Setup with dependencies
MultiProvider(
  providers: [
    Provider(create: (_) => ApiService('https://api.example.com')),
    ProxyProvider(
      update: (_, apiService, __) => ProductRepository(apiService),
    ),
    ChangeNotifierProxyProvider(
      create: (_) => ProductProvider(),
      update: (_, repository, provider) => provider!..repository = repository,
    ),
  ],
  child: const MyApp(),
)

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.

Leave a Comment