Flutter Riverpod

Modern, compile-safe state management

๐Ÿš€ What is Riverpod?

Riverpod is a complete rewrite of Provider that fixes its limitations. It offers compile-time safety, no BuildContext dependency, better testability, and powerful features like auto-dispose and family modifiers for modern Flutter development.


// Simple provider declaration
final counterProvider = StateProvider((ref) => 0);
                                    

Key Concepts

โœ…

Compile Safe

Catch errors at compile time

// Type-safe access
ref.watch(counterProvider)
๐ŸŽฏ

No Context

Access providers anywhere

// No BuildContext needed
ref.read(provider)
๐Ÿงช

Testable

Easy to mock and test

ProviderContainer(
  overrides: [...])
โ™ป๏ธ

Auto-dispose

Automatic memory management

final provider = 
  StateProvider.autoDispose

๐Ÿ”น Installation

Add Riverpod to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.0

Then run:

flutter pub get

๐Ÿ”น Basic Counter Example

Create a simple counter with Riverpod:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. Create a provider (outside any class)
final counterProvider = StateProvider<int>((ref) => 0);

// 2. Wrap app with ProviderScope
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterPage(),
    );
  }
}

// 3. Use ConsumerWidget to access providers
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Watch the provider
    final count = ref.watch(counterProvider);
    
    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Counter')),
      body: Center(
        child: Text(
          '$count',
          style: TextStyle(fontSize: 48),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Update the provider
          ref.read(counterProvider.notifier).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Output:

A counter app that increments when the floating button is pressed.

๐Ÿ”น StateNotifierProvider Example

For more complex state logic:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. Create a StateNotifier
class Counter extends StateNotifier<int> {
  Counter() : super(0);
  
  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// 2. Create a provider
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

// 3. Use in widget
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    final counter = ref.read(counterProvider.notifier);
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$count', style: TextStyle(fontSize: 48)),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: counter.decrement,
                  child: Text('-'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: counter.increment,
                  child: Text('+'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: counter.reset,
                  child: Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Output:

A counter with increment, decrement, and reset buttons.

๐Ÿ”น FutureProvider Example

Handle async data with FutureProvider:

// Simulate API call
Future<String> fetchUserName() async {
  await Future.delayed(Duration(seconds: 2));
  return 'John Doe';
}

// Create FutureProvider
final userProvider = FutureProvider<String>((ref) async {
  return await fetchUserName();
});

// Use in widget
class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    
    return Scaffold(
      appBar: AppBar(title: Text('User Profile')),
      body: Center(
        child: userAsync.when(
          data: (name) => Text('Hello, $name!', 
            style: TextStyle(fontSize: 24)),
          loading: () => CircularProgressIndicator(),
          error: (error, stack) => Text('Error: $error'),
        ),
      ),
    );
  }
}

Output:

Shows loading spinner, then displays the user name, or shows error if failed.

๐Ÿ”น Family Modifier

Create providers with parameters:

// Provider that takes a parameter
final todoProvider = Provider.family<String, int>((ref, id) {
  return 'Todo #$id';
});

class TodoList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
      itemCount: 5,
      itemBuilder: (context, index) {
        // Access provider with parameter
        final todo = ref.watch(todoProvider(index));
        return ListTile(
          title: Text(todo),
        );
      },
    );
  }
}

Output:

A list displaying "Todo #0", "Todo #1", etc.

๐Ÿ”น Consumer vs ConsumerWidget

Two ways to consume providers:

// Option 1: ConsumerWidget (entire widget rebuilds)
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

// Option 2: Consumer (partial rebuild)
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Static text'), // Won't rebuild
        Consumer(
          builder: (context, ref, child) {
            final count = ref.watch(counterProvider);
            return Text('$count'); // Only this rebuilds
          },
        ),
      ],
    );
  }
}

Output:

Both approaches display the counter, but Consumer offers more granular control.

๐Ÿ”น When to Use Riverpod

โœ… Good For:

  • New Flutter projects
  • Apps requiring compile-time safety
  • Complex state management needs
  • Apps with lots of async operations
  • When testability is important

โŒ Consider Alternatives For:

  • Very simple apps (use setState)
  • Existing Provider apps (migration effort)
  • Teams unfamiliar with reactive programming
  • When you need Redux DevTools

๐Ÿ”น Riverpod vs Provider

Riverpod Advantages:

  • No BuildContext: Access providers anywhere
  • Compile-safe: Errors caught at compile time
  • Better testing: Easy to override providers
  • Auto-dispose: Automatic cleanup
  • Family modifier: Parameterized providers

๐Ÿง  Test Your Knowledge

What must wrap your app to use Riverpod?