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> {
: super(0); // Initial state is 0
CounterNotifier()
void increment() {
= state + 1; // Riverpod automatically notifies listeners
state }
void decrement() {
= state - 1;
state }
void reset() {
= 0;
state }
}
// 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 typeint
- The
state
property inside the class will be of typeint
- When you call
state = state + 1
, you’re working with anint
value - The initial state
super(0)
must also be anint
Visual Breakdown:
<StateType>
StateNotifier^^^^^^^^^
This defines what type of data the state holds
// Examples:
<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 StateNotifier
In Your Counter Example:
class CounterNotifier extends StateNotifier<int> {
// state is of type int
// state starts as 0 (an int)
void increment() {
= state + 1; // state is int, so we can do math
state }
}
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:
<NotifierType, StateType>
StateNotifierProvider^^^^^^^^^^^^ ^^^^^^^^^
First param Second param
CounterNotifier
- 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:
<CounterNotifier, int>((ref) {
StateNotifierProvider// ^^^
// 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(
: someConfig,
config: 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
, WidgetRef ref) {
Widget build(BuildContext context// This returns int (the state type)
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
class CounterButtons extends ConsumerWidget {
@override
, WidgetRef ref) {
Widget build(BuildContext contextreturn ElevatedButton(
// This returns CounterNotifier (the notifier type)
: () => ref.read(counterProvider.notifier).increment(),
onPressed: Text('+'),
child
);}
}
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(// Riverpod's equivalent to ChangeNotifierProvider
ProviderScope( : MyApp(),
child,
)
);}
class MyApp extends StatelessWidget {
@override
{
Widget build(BuildContext context) return MaterialApp(
: HomePage(),
home
);}
}
class HomePage extends StatelessWidget {
@override
{
Widget build(BuildContext context) // Clean - no props needed!
return Scaffold(
: AppBar(title: Text('Riverpod Counter')),
appBar: MainContent(),
body
);}
}
class MainContent extends StatelessWidget {
@override
{
Widget build(BuildContext context) // Clean - no props needed!
return Center(
: CounterSection(),
child
);}
}
class CounterSection extends StatelessWidget {
@override
{
Widget build(BuildContext context) return Column(
: MainAxisAlignment.center,
mainAxisAlignment: [
children,
CounterDisplay(): 10),
SizedBox(height,
EvenOddDisplay(): 20),
SizedBox(height,
CounterButtons(),
]
);}
}
class CounterDisplay extends ConsumerWidget {
@override
, WidgetRef ref) {
Widget build(BuildContext context// Watch the counter - rebuilds when it changes
final counter = ref.watch(counterProvider);
return Text(
'Count: $counter',
: Theme.of(context).textTheme.headlineMedium,
style
);}
}
class EvenOddDisplay extends ConsumerWidget {
@override
, WidgetRef ref) {
Widget build(BuildContext context// Watch derived state
final isEven = ref.watch(isEvenProvider);
return Text(
? 'Even' : 'Odd',
isEven : TextStyle(
style: 18,
fontSize: isEven ? Colors.green : Colors.orange,
color,
)
);}
}
class CounterButtons extends ConsumerWidget {
@override
, WidgetRef ref) {
Widget build(BuildContext contextreturn Row(
: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: [
children
ElevatedButton(// Read the notifier to call methods
: () => ref.read(counterProvider.notifier).decrement(),
onPressed: Text('-'),
child,
)
ElevatedButton(: () => ref.read(counterProvider.notifier).increment(),
onPressed: Text('+'),
child,
)
ElevatedButton(: () => ref.read(counterProvider.notifier).reset(),
onPressed: Text('Reset'),
child,
),
]
);}
}
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
, WidgetRef ref) {
Widget build(BuildContext context// 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)
.read(counterProvider.notifier).increment(); ref
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
andref.read
API
Better Testing:
// Easy to override providers for testing
'counter test', (tester) async {
testWidgets(await tester.pumpWidget(
ProviderScope(: [
overrides.overrideWith(() => CounterNotifier()),
counterProvider,
]: MyApp(),
child,
)
);});
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?