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.954.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 typeHere’s what it means:
StateNotifier<int>means this notifier manages state of typeint- The
stateproperty inside the class will be of typeint - When you call
state = state + 1, you’re working with anintvalue - The initial state
super(0)must also be anint
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 objectsIn 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 paramCounterNotifier- The Notifier Type- This is the class that manages the state
- It’s the business logic container that extends
StateNotifier<int>
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.watchandref.readAPI
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?