Flutter Riverpod Basics
Riverpod is a state management library created by the author of Provider. It fixes common Provider pain points: it needs no BuildContext to read data, it catches mistakes at compile time, and it runs the same way in tests. Think of Riverpod as a well-organized warehouse where every item has a label, and any worker can fetch an item by its label.
Installing Riverpod
flutter pub add flutter_riverpod
Check the package page on pub.dev for the newest syntax, because the library receives regular updates.
The Big Picture
ProviderScope (the warehouse)
│
├── counterProvider → holds an int
├── nameProvider → holds a String
└── todosProvider → holds a List
▲
│ ref.watch / ref.read
│
Your widgets
A provider is a small object that creates and stores one piece of state. A ref is the handle a widget uses to reach providers.
Step 1: Wrap the App
Place ProviderScope at the top of the widget tree. It stores all provider state.
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
Step 2: Create a Simple Provider
A plain Provider exposes a value that never changes, such as a settings object or a greeting.
final greetingProvider = Provider<String>((ref) {
return 'Welcome to Flutter';
});
Declare providers as global final variables. The variable itself holds no state; it acts only as a key.
Step 3: Read It in a Widget
Extend ConsumerWidget instead of StatelessWidget. The build method receives an extra WidgetRef parameter.
class GreetingText extends ConsumerWidget {
const GreetingText({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final message = ref.watch(greetingProvider);
return Text(message);
}
}
The ref.watch call rebuilds the widget whenever the provider value changes.
Changing State with NotifierProvider
A counter needs state that changes. Write a Notifier class that holds the value in a property named state.
class CounterNotifier extends Notifier<int> {
@override
int build() {
return 0; // starting value
}
void increment() {
state = state + 1;
}
void reset() {
state = 0;
}
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
Assigning a new value to state notifies every widget that watches the provider.
Using the Counter
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('Count: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: Icon(Icons.add),
),
);
}
}
Tap button
│
▼
ref.read(counterProvider.notifier).increment()
│
▼
state changes 0 → 1
│
▼
ref.watch listeners rebuild → Text shows "Count: 1"
watch and read
| Method | Where to Use | Behavior |
|---|---|---|
| ref.watch | Inside build | Rebuilds when the value changes |
| ref.read | Inside button handlers | Reads once with no rebuild |
Never call ref.read inside build to get displayed data. The screen would miss every update.
FutureProvider for Loading Data
A FutureProvider handles network calls and exposes three states: loading, data, and error.
final quoteProvider = FutureProvider<String>((ref) async {
await Future.delayed(Duration(seconds: 1));
return 'Practice makes progress';
});
class QuoteView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final quote = ref.watch(quoteProvider);
return quote.when(
data: (text) => Text(text),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Something went wrong'),
);
}
}
The when method forces you to handle all three states, which prevents blank screens.
Providers That Depend on Providers
One provider reads another through its own ref. Riverpod recalculates the dependent provider automatically.
final doubleCountProvider = Provider<int>((ref) {
final count = ref.watch(counterProvider);
return count * 2;
});
Why Developers Choose Riverpod
- Providers work outside the widget tree, in plain Dart code.
- The compiler catches missing providers before the app runs.
- Tests replace any provider with fake data in one line.
- Async data has built-in loading and error handling.
Choosing a Provider Type
| Need | Provider |
|---|---|
| Fixed value or computed value | Provider |
| Value with methods that change it | NotifierProvider |
| One async result, such as a network call | FutureProvider |
| Live data from a stream | StreamProvider |
Practice Ideas
Build a to-do list with a NotifierProvider holding a list of strings. Add methods named add and remove. Show the count of tasks in the app bar through a second provider that reads the first. Watch both widgets update from one change.
Testing with Riverpod
Testing a screen that reads a provider takes one extra step: wrap the widget in a ProviderScope and override the provider with fake data. A weather screen that normally calls a server shows a fixed temperature during the test. No network, no waiting, and no random failures. This ease of testing is a major reason teams adopt the library for projects that grow beyond a few screens.
