54  Riverpod (Counter App)

I’ll show you how to implement the same counter app using Riverpod, which is the next evolution of Provider with better compile-time safety and more flexible architecture.

54.1 Riverpod Solution

First, add Riverpod to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9

54.1.1 Counter State and Logic

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

// Define a StateNotifier for business logic
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0); // Initial state is 0

  void increment() {
    state = state + 1; // Riverpod automatically notifies listeners
  }
  
  void decrement() {
    state = state - 1;
  }
  
  void reset() {
    state = 0;
  }
}

// Create a provider - this is like a global variable but better
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// You can also create simple providers for derived state
final isEvenProvider = Provider<bool>((ref) {
  final count = ref.watch(counterProvider);
  return count % 2 == 0;
});

54.1.1.1 Breakdown: StateNotifier<int>

In this code, <int> is a generic type parameter that specifies the type of the state that the StateNotifier manages.

class CounterNotifier extends StateNotifier<int> {
//                                            ^^^
//                                    This is the state type

Here’s what it means:

  • StateNotifier<int> means this notifier manages state of type int
  • The state property inside the class will be of type int
  • When you call state = state + 1, you’re working with an int value
  • The initial state super(0) must also be an int

Visual Breakdown:

StateNotifier<StateType>
             ^^^^^^^^^
             This defines what type of data the state holds

// Examples:
StateNotifier<int>        // State is an integer
StateNotifier<String>     // State is a string  
StateNotifier<User>       // State is a User object
StateNotifier<List<Todo>> // State is a list of Todo objects

In Your Counter Example:

class CounterNotifier extends StateNotifier<int> {
  // state is of type int
  // state starts as 0 (an int)
  
  void increment() {
    state = state + 1; // state is int, so we can do math
  }
}

The StateNotifier class is designed to hold and manage a single piece of state of whatever type you specify in the generic parameter.

54.1.1.2 Breakdown: <CounterNotifier, int> and ref

The <CounterNotifier, int> and ref in this code serve specific purposes in Riverpod’s type system and dependency injection. Let me break them down:

StateNotifierProvider<CounterNotifier, int> This has two generic type parameters:

StateNotifierProvider<NotifierType, StateType>
                     ^^^^^^^^^^^^  ^^^^^^^^^
                     First param   Second param
  1. CounterNotifier - The Notifier Type
    • This is the class that manages the state
    • It’s the business logic container that extends StateNotifier<int>
  2. int - The State Type
    • This is the type of data the state holds
    • Must match the generic type in your StateNotifier<int>

Visual Breakdown:

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
//                                           ^^^^^^^^^^^^^  ^^^
//                                           |              |
//                                           |              └── State type (what data it holds)
//                                           └── Notifier type (the class managing state)
  return CounterNotifier();
});

The ref Parameter

The ref is Riverpod’s dependency injection system:

StateNotifierProvider<CounterNotifier, int>((ref) {
//                                           ^^^
//                                           Dependency reference
  return CounterNotifier();
});

What ref allows you to do:

// Example: Provider that depends on other providers
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  // You could read other providers here
  final someConfig = ref.read(configProvider);
  final apiService = ref.read(apiServiceProvider);
  
  return CounterNotifier(
    config: someConfig,
    apiService: apiService,
  );
});

// Or watch for changes in dependencies
final userCounterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  final userId = ref.watch(currentUserIdProvider);
  return CounterNotifier(userId: userId);
});

Type Flow Diagram:

StateNotifierProvider<CounterNotifier, int>
                     |               |
                     |               └── When you call ref.watch(counterProvider)
                     |                   you get: int (the current state value)
                     |
                     └── When you call ref.read(counterProvider.notifier)
                         you get: CounterNotifier (the notifier instance)

In Practice:

// In your widgets:
class CounterDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // This returns int (the state type)
    final count = ref.watch(counterProvider);
    
    return Text('Count: $count');
  }
}

class CounterButtons extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      // This returns CounterNotifier (the notifier type)
      onPressed: () => ref.read(counterProvider.notifier).increment(),
      child: Text('+'),
    );
  }
}

Why Two Types?:

Riverpod separates these because: - State type (int) - What widgets consume/watch - Notifier type (CounterNotifier) - What provides the methods to modify state

This separation gives you: - Type safety - Compile-time guarantees - Clear separation - Data vs behavior - Better performance - Only rebuilds when state changes, not when methods are called

The ref parameter enables dependency injection without manual wiring, making your code more testable and modular.

54.1.2 Main App Implementation

void main() {
  runApp(
    ProviderScope( // Riverpod's equivalent to ChangeNotifierProvider
      child: MyApp(),
    ),
  );
}

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Clean - no props needed!
    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Counter')),
      body: MainContent(),
    );
  }
}

class MainContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Clean - no props needed!
    return Center(
      child: CounterSection(),
    );
  }
}

class CounterSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CounterDisplay(),
        SizedBox(height: 10),
        EvenOddDisplay(),
        SizedBox(height: 20),
        CounterButtons(),
      ],
    );
  }
}

class CounterDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Watch the counter - rebuilds when it changes
    final counter = ref.watch(counterProvider);
    
    return Text(
      'Count: $counter',
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

class EvenOddDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Watch derived state
    final isEven = ref.watch(isEvenProvider);
    
    return Text(
      isEven ? 'Even' : 'Odd',
      style: TextStyle(
        fontSize: 18,
        color: isEven ? Colors.green : Colors.orange,
      ),
    );
  }
}

class CounterButtons extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        ElevatedButton(
          // Read the notifier to call methods
          onPressed: () => ref.read(counterProvider.notifier).decrement(),
          child: Text('-'),
        ),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('+'),
        ),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).reset(),
          child: Text('Reset'),
        ),
      ],
    );
  }
}

54.2 Widget Tree Diagram with Riverpod

main()
└── ProviderScope (Riverpod Container)
    ├── Manages all providers:
    │   ├── counterProvider: StateNotifierProvider<CounterNotifier, int>
    │   └── isEvenProvider: Provider<bool> (derived from counterProvider)
    │
    └── MyApp
        └── HomePage (clean - no props!)
            └── MainContent (clean - no props!)
                └── CounterSection (clean - no props!)
                    ├── CounterDisplay (ConsumerWidget)
                    │   └── ref.watch(counterProvider) ──┐
                    ├── EvenOddDisplay (ConsumerWidget)   │
                    │   └── ref.watch(isEvenProvider) ──┐ │
                    └── CounterButtons (ConsumerWidget)  │ │
                        └── ref.read(counterProvider.notifier) ─┐
                                                               │ │
┌──────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
Provider Graph:
┌─────────────────────────────────┐
│ counterProvider                 │
│ ├── StateNotifier: CounterNotifier │
│ ├── State: int (current value)  │
│ └── Methods: increment(), etc.  │
└─────────────────────────────────┘
            │
            │ ref.watch()
            ▼
┌─────────────────────────────────┐
│ isEvenProvider                  │
│ ├── Depends on: counterProvider │
│ ├── Returns: bool               │
│ └── Auto-recomputes when        │
│     counterProvider changes     │
└─────────────────────────────────┘

54.3 Data Flow and Callback Diagram

Riverpod Architecture:

┌─────────────────────────────────────────────────────────────┐
│                    ProviderScope                            │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              Provider Graph                         │    │
│  │                                                     │    │
│  │  counterProvider                                    │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │          CounterNotifier                    │    │    │
│  │  │  ┌─────────────────────────────────────┐    │    │    │
│  │  │  │ state: 0                            │    │    │    │
│  │  │  │ increment() { state = state + 1; }  │    │    │    │
│  │  │  │ decrement() { state = state - 1; }  │    │    │    │
│  │  │  │ reset() { state = 0; }              │    │    │    │
│  │  │  └─────────────────────────────────────┘    │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  │                        │                            │    │
│  │                        │ ref.watch()                │    │
│  │                        ▼                            │    │
│  │  isEvenProvider                                     │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │ (ref) => ref.watch(counterProvider) % 2 == 0│    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ Provides to all widgets via ref
                              ▼
            ┌─────────────────────────────────────┐
            │           Widget Tree               │
            │                                     │
            │ ConsumerWidget widgets access       │
            │ providers via WidgetRef:            │
            │                                     │
            │ CounterDisplay                      │
            │   └── ref.watch(counterProvider)    │
            │                                     │
            │ EvenOddDisplay                      │
            │   └── ref.watch(isEvenProvider)     │
            │                                     │
            │ CounterButtons                      │
            │   └── ref.read(counterProvider.notifier) │
            └─────────────────────────────────────┘

User Interaction Flow:

1. User taps "+" button
        │
        ▼
2. ref.read(counterProvider.notifier).increment()
        │
        ▼
3. CounterNotifier.increment() called
        │
        ▼
4. state = state + 1 (Riverpod detects state change)
        │
        ▼
5. Riverpod automatically notifies all watchers
        │
        ├──────────────────────────────────┐
        ▼                                  ▼
6. CounterDisplay rebuilds          isEvenProvider recalculates
   (watches counterProvider)        (depends on counterProvider)
        │                                  │
        ▼                                  ▼
7. Shows new count                  EvenOddDisplay rebuilds
                                   (watches isEvenProvider)
                                           │
                                           ▼
                                   8. Shows new even/odd status

Dependencies and Rebuilds:
┌─────────────────────┬─────────────────────┬─────────────────────┐
│ Widget              │ Watches             │ Rebuilds When       │
├─────────────────────┼─────────────────────┼─────────────────────┤
│ CounterDisplay      │ counterProvider     │ Counter changes     │
│ EvenOddDisplay      │ isEvenProvider      │ Even/odd changes    │
│ CounterButtons      │ Nothing (read only) │ Never               │
└─────────────────────┴─────────────────────┴─────────────────────┘

54.4 Key Riverpod Concepts

54.4.1 1. Provider Types

// StateNotifierProvider - for complex state with methods
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// Provider - for computed/derived values
final isEvenProvider = Provider<bool>((ref) {
  final count = ref.watch(counterProvider);
  return count % 2 == 0;
});

// StateProvider - for simple state (like useState)
final simpleCounterProvider = StateProvider<int>((ref) => 0);

54.4.2 2. ConsumerWidget vs StatelessWidget

// Use ConsumerWidget when you need access to providers
class CounterDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Has access to ref
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

// Regular StatelessWidget when you don't need providers
class Header extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('My App');
  }
}

54.4.3 3. ref.watch vs ref.read

// ref.watch - Subscribe to changes (rebuilds widget)
final counter = ref.watch(counterProvider);

// ref.read - One-time access (no rebuilds)
ref.read(counterProvider.notifier).increment();

54.4.4 4. Automatic Dependency Management

Riverpod automatically tracks dependencies between providers:

final isEvenProvider = Provider<bool>((ref) {
  final count = ref.watch(counterProvider); // Auto-dependency
  return count % 2 == 0;
});

When counterProvider changes, isEvenProvider automatically recalculates.

54.5 Benefits Over Provider

Compile-time Safety:

  • No runtime errors from missing providers
  • Better type inference and IDE support

No BuildContext Required:

  • Providers can be accessed anywhere (services, repositories)
  • Easier testing and separation of concerns

Better Performance:

  • Fine-grained rebuilds
  • Automatic disposal of unused providers

Simpler API:

  • No need for different widget types (Consumer, Selector, etc.)
  • Consistent ref.watch and ref.read API

Better Testing:

// Easy to override providers for testing
testWidgets('counter test', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        counterProvider.overrideWith(() => CounterNotifier()),
      ],
      child: MyApp(),
    ),
  );
});

Riverpod is like having Redux with automatic selectors and dependency injection, but with much less boilerplate. It’s particularly powerful for complex apps with interdependent state.

Would you like me to show you how to handle async operations (like API calls) with Riverpod next?