Implementation Deep Dive

Riverpod’s true power shines when you start implementing it in real-world scenarios. Let’s move beyond theory and explore how to wield this framework effectively—from basic setups to advanced patterns.

1. Basic Implementation

The Gateway: ProviderScope

Every Riverpod-powered app starts with ProviderScope, a widget that injects the state container into your app. Wrap your root widget with it:

void main() {  
  runApp(  
    ProviderScope(  
      child: MyApp(),  
    ),  
  );  
}  

This acts as the backbone for all providers in your app. Providers are declarative, reusable objects that manage and provide state or dependencies across a Flutter application, allowing efficient and clean state management.

Three Pillars of Providers

1. StateProvider: Ideal for ephemeral state (e.g., counters, switches).
   final counterProvider = StateProvider<int>((ref) => 0);  

Why this works: Exposes a simple state getter/setter.

2. StateNotifierProvider: Manages complex business logic with immutable state.
   class TodoNotifier extends StateNotifier<List<Todo>> {  
     TodoNotifier() : super([]);  
     void addTodo(Todo todo) => state = [...state, todo];  
   }  
   final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) => TodoNotifier());  

Why this works: Decouples logic from UI, enabling testability.

3. FutureProvider: Handles async operations like API calls.
   final weatherProvider = FutureProvider<Weather>((ref) async {  
     final location = ref.watch(locationProvider);  
     return await WeatherAPI.fetch(location);  
   });  

Why this works: Automatically manages loading/error states.

Example: From setState to Riverpod—A Paradigm Shift

Compare a counter implementation:

// Traditional setState  
class CounterPage extends StatefulWidget {  
  @override  
  _CounterPageState createState() => _CounterPageState();  
}  

// Riverpod approach  
class CounterPage extends ConsumerWidget {  
  @override  
  Widget build(BuildContext context, WidgetRef ref) {  
    final count = ref.watch(counterProvider);  
    return ElevatedButton(  
      onPressed: () => ref.read(counterProvider.notifier).state++,  
      child: Text('$count'),  
    );  
  }  
}  

Riverpod eliminates the need for StatefulWidget by managing state outside the widget tree. State becomes globally accessible through WidgetRef, making it easier to access and modify from anywhere in your app while maintaining clean architecture. This approach reduces boilerplate code and separates state management concerns from your UI logic.

2. Advanced Scenarios

Dependency Injection Made Simple

Think of providers as containers for your services. Here's how to manage API calls cleanly:

// Define your API client provider - a single source of truth
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());

// Use it to fetch user data
final userProvider = FutureProvider<User>((ref) async {
  final client = ref.watch(apiClientProvider);
  return client.fetchUser();
});

// Usage in a widget
class UserProfile extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);
    return user.when(
      data: (data) => Text(data.name),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

Key takeaway: Providers create a clean dependency injection system that makes testing straightforward - simply override providers with mocks without changing your production code.

State Persistence

Save app settings automatically across restarts by pairing StateNotifierProvider with HydratedMixin for automatic serialization, requiring minimal additional code:

// Define your settings state
class Settings {
  final ThemeMode theme;
  Settings({required this.theme});

  factory Settings.defaults() => Settings(theme: ThemeMode.system);

  Settings copyWith({ThemeMode? theme}) =>
      Settings(theme: theme ?? this.theme);

  factory Settings.fromJson(Map<String, dynamic> json) =>
      Settings(theme: ThemeMode.values[json['theme'] as int]);

  Map<String, dynamic> toJson() => {'theme': theme.index};
}

// Create a persistent provider
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>(
  (ref) => SettingsNotifier(),
);

class SettingsNotifier extends StateNotifier<Settings> with HydratedMixin {
  SettingsNotifier() : super(Settings.defaults());

  void updateTheme(ThemeMode theme) => state = state.copyWith(theme: theme);

  @override
  Settings fromJson(Map<String, dynamic> json) => Settings.fromJson(json);

  @override
  Map<String, dynamic> toJson(Settings state) => state.toJson();
}

// Usage in a widget
class ThemeToggle extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final settings = ref.watch(settingsProvider);
    return Switch(
      value: settings.theme == ThemeMode.dark,
      onChanged: (isDark) => ref.read(settingsProvider.notifier)
          .updateTheme(isDark ? ThemeMode.dark : ThemeMode.light),
    );
  }
}

Combining Providers

Calculate values based on multiple states:

// Shopping cart example
final cartItemsProvider = StateNotifierProvider<CartNotifier, List<CartItem>>(...);
final discountProvider = StateProvider<double>((ref) => 0.0);

final cartTotalProvider = Provider<double>((ref) {
  final items = ref.watch(cartItemsProvider);
  final discount = ref.watch(discountProvider);

  final subtotal = items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
  return subtotal * (1 - discount);
});

Key takeaway: Create reactive computed states that automatically update when their dependencies change - perfect for derived values like totals, filtered lists, or complex calculations.

Parameterized Providers with Family

Fetch different data instances using the same provider:

final productProvider = FutureProvider.family<Product, String>((ref, productId) async {
  final client = ref.watch(apiClientProvider);
  return client.fetchProduct(productId);
});

// Usage in a widget
class ProductTile extends ConsumerWidget {
  final String productId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final product = ref.watch(productProvider(productId));
    return product.when(
      data: (data) => ListTile(title: Text(data.name)),
      loading: () => Shimmer(),
      error: (error, _) => ErrorTile(),
    );
  }
}

When to use: Perfect for scenarios where you need multiple instances of similar data, like fetching different products by ID or managing user-specific states.

3. Common Pitfalls & Best Practices

Reactivity Traps

  • Don’t: Overuse ref.read in builders—it breaks reactivity.
  // 🚫 Avoid  
  ref.read(counterProvider.notifier).state++;  

  // ✅ Prefer  
  ref.watch(counterProvider);  
  • Do: Use autoDispose to avoid memory leaks:
  final tempDataProvider = StateProvider.autoDispose<int>((ref) => 0);  

Optimize Rebuilds with select

Prevent unnecessary widget rebuilds by listening to specific properties:

final userName = ref.watch(userProvider.select((user) => user.name));  

Only rebuilds when user.name changes, not the entire user object.

4. Testing

Building on our earlier discussion of dependency injection, here's how to put provider overrides into practice:

testWidgets('Displays mocked user', (tester) async {  
  await tester.pumpWidget(  
    ProviderScope(  
      overrides: [  
        userProvider.overrideWithValue(  
          AsyncValue.data(User(name: 'Test User')),  
        ),  
      ],  
      child: MyApp(),  
    ),  
  );  
  expect(find.text('Test User'), findsOneWidget);  
});  

No complex mocking setup—swap dependencies at the provider level.

Wrapping Up

Riverpod isn't just about managing state—it's about architecting scalable, testable, and maintainable apps. By embracing providers, families, and reactive patterns, you'll unlock a toolkit that grows with your app's complexity.

As you continue your Flutter development journey, remember that solid state management isn't just a technical choice—it's an investment in your app's future growth and your team's productivity. I hope these articles have equipped you with the knowledge and confidence to build more robust Flutter applications with Riverpod.

Source: View source