Riverpod Basics
Introducing Riverpod — a modern, robust, and testable state management library for Flutter.
Riverpod
is a state management
library that takes the Provider pattern and makes it more robust, testable, and flexible.
It's built on top of the
Provider
package but offers several improvements:
✅ Why Riverpod?
Riverpod was created because the original Provider package had some limitations:
it relied heavily on
BuildContext
, made testing difficult, and didn't offer
great compile‑time safety. Riverpod solves these problems and adds powerful features
like
StateNotifier
,
FutureProvider
, and
StreamProvider
.
It's now the recommended approach by the Flutter team for new projects.
Riverpod is built around a few key concepts. Understanding these is essential to using it effectively.
1. Providers
A provider is a piece of logic that holds a value and can be listened to by widgets. Providers are defined globally and can be accessed from anywhere in your app.
2. Ref
Ref (short for "reference") is the object that lets you read, watch, or listen to providers. It's passed to the provider's creation function and is the primary way to interact with providers.
3. ProviderScope
ProviderScope is the root widget that makes Riverpod work. It stores the state of all your providers and must be placed at the top of your widget tree.
Let's start with a classic counter example using Riverpod. This will show you the basic structure of a Riverpod app.
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.0
# Use the latest version
import
'package:flutter/material.dart'
;
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
// 1. Define a provider that holds an integer
final
counterProvider = StateProvider<int>((ref) =>
0
);
void
main
() {
runApp(
// 2. Wrap the app with ProviderScope
ProviderScope(
child: MyApp(),
),
);
}
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
home: CounterScreen(),
);
}
}
// 3. Use ConsumerWidget instead of StatelessWidget
class
CounterScreen
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
// 4. Watch the provider to get its current value and rebuild on changes
final
count = ref.watch(counterProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'Riverpod Counter'
)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $count'
),
ElevatedButton(
onPressed: () {
// 5. Use ref.read to get the provider's notifier and update the state
ref.read(counterProvider.notifier).state++;
},
child: Text(
'Add'
),
),
],
),
),
);
}
}
counterProvider
is a
StateProvider
that holds
an integer. The arrow function
(ref) => 0
initializes it to 0. The
ref
parameter
is the reference object we'll use to interact with other providers.
StatelessWidget
, we use
ConsumerWidget
.
This gives us access to the
ref
parameter in the
build()
method.
notifier
and update its
state
.
ref.read()
is typically used
inside event handlers or callbacks.
Riverpod provides three main methods for interacting with providers:
👀 ref.watch()
Use when: You want to read a provider and rebuild the widget when it changes.
- Causes the widget to rebuild on changes
-
Always use inside the
build()method - Provides the current value
final
count = ref.watch(counterProvider);
📖 ref.read()
Use when: You want to read a provider without rebuilding the widget.
- Does NOT cause the widget to rebuild
- Used in event handlers, callbacks, and side effects
- Good for updating state from user actions
ref.read(counterProvider.notifier).state++;
🔔 ref.listen()
Use when: You want to run side effects (like showing a snackbar) when a provider changes, without rebuilding the widget.
- Does NOT cause the widget to rebuild
- Runs a callback when the provider's state changes
- Perfect for navigation, showing dialogs, or logging
ref.listen<int>(counterProvider, (prev, next) {
print('Count changed from $prev to $next');
});
⚠️ Important: When to Use Each
-
Use
watchinbuild()to react to changes. -
Use
readin event handlers to perform actions without rebuilding. -
Use
listenfor side effects that don't affect the UI directly.
Riverpod offers several types of providers for different use cases. Here's a quick overview:
StateProvider
The simplest provider. Holds a mutable piece of state.
final
provider = StateProvider<int>((ref) => 0);
FutureProvider
Holds a
Future
. Handles loading and error states.
final
provider = FutureProvider<User>((ref) async {
return
await fetchUser();
});
StreamProvider
Holds a
Stream
. Perfect for real‑time data.
final
provider = StreamProvider<Message>((ref) {
return
chatService.messagesStream;
});
StateNotifierProvider
The most powerful provider. Manages complex state with methods.
final
provider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
return
TodoNotifier();
});
We'll cover each of these provider types in detail in the upcoming topics.
If you forget to wrap your app with
ProviderScope
, Riverpod will throw an error.
Always ensure
ProviderScope
is at the root of your widget tree.
runApp(ProviderScope(child: MyApp()));
is the standard way to initialize Riverpod.
ref.watch()
can only be called during the
build()
method or in the
Provider
creation function. Using it in event handlers will cause errors.
For event handlers, use
ref.read(provider.notifier).state = newValue
to update state.
With
StateProvider
, you must update the state through the
notifier
.
Directly assigning to
state
without the notifier won't trigger rebuilds.
Use
ref.read(provider.notifier).state = newValue
to properly update state.
🎯 Key Takeaway
Riverpod is a modern, robust state management library that makes it easy to manage global state in Flutter apps. It introduces the concept of providers that hold state, and ref to interact with them. With its compile‑time safety, testability, and performance, Riverpod is an excellent choice for intermediate Flutter developers. Start with StateProvider for simple state, and explore other provider types as needed.