Provider
Understanding the foundation of Riverpod — providers are the building blocks that hold and expose state.
In Riverpod, a provider is a piece of logic that holds a value and can be listened to by widgets. Providers are the fundamental building blocks of Riverpod. They are:
- Global — they can be accessed from anywhere in your app
- Type-safe — they hold a specific type of value
- Reactive — widgets can listen to them and rebuild when the value changes
- Composable — providers can depend on other providers
✅ The Big Idea
Think of a provider as a container for state that lives outside your widget tree. You define providers globally, and then your widgets can watch them to get the current value and rebuild when it changes.
A provider is defined using a simple function that returns a value. Here's the basic structure:
final
myProvider = Provider<String>((ref) {
// This function is called once when the provider is first read
return
'Hello, World!'
;
});
1.
final myProvider
The provider is declared as a top-level final variable. This makes it globally accessible.
2.
Provider<String>
The type parameter tells Riverpod what type of value this provider holds.
3.
(ref) => ...
The creation function. It receives a
ref
object that can be used to read other providers.
4.
return 'Hello, World!';
The value that the provider exposes. This can be any Dart object.
There are three main ways to interact with providers in your widgets:
ref.watch()
Listens to a provider and rebuilds the widget when the provider's value changes.
Use in build() method
final
value = ref.watch(myProvider);
ref.read()
Reads a provider once without listening for changes. The widget does not rebuild.
Use in callbacks, event handlers
final
value = ref.read(myProvider);
ref.listen()
Runs a callback when the provider's value changes. The widget does not rebuild.
Use for side effects (navigation, logging)
ref.listen<String>(myProvider, (prev, next) {
print('Value changed to: $next');
});
ref.read().notifier
Gets the notifier of a provider (for StateProvider, StateNotifierProvider) to update state.
Use to update state from callbacks
ref.read(myProvider.notifier).state = newValue;
Riverpod offers several provider types, each designed for a specific use case:
Provider
The simplest provider. Holds a value that never changes after initialization. Good for dependencies, configuration, and services.
final
apiProvider = Provider<ApiService>((ref) => ApiService());
StateProvider
Holds a mutable piece of state. Use for simple state like counters, booleans, or strings.
final
counterProvider = StateProvider<int>((ref) => 0);
FutureProvider
Holds a
Future
. Perfect for asynchronous data fetching like API calls.
Handles loading, data, and error states.
final
userProvider = FutureProvider<User>((ref) async {
return
await fetchUser();
});
StreamProvider
Holds a
Stream
. Ideal for real-time data like Firebase Firestore streams.
final
messagesProvider = StreamProvider<Message>((ref) {
return
chatService.messagesStream;
});
StateNotifierProvider
The most powerful provider. Holds a
StateNotifier
that manages complex state
with methods. Best for forms, lists, and complex business logic.
final
todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
return
TodoNotifier();
});
A common use case for the basic
Provider
is
dependency injection
.
You can use providers to make services, repositories, or configuration objects available throughout your app.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
// A simple service class
class
ApiService
{
Future
<String>
fetchData
() async {
// Simulate network request
await Future.delayed(Duration(seconds: 1));
return
'Fetched data from API'
;
}
}
// 1. Define a provider that holds the ApiService instance
final
apiServiceProvider = Provider<ApiService>((ref) {
return
ApiService();
// The service is created once and reused
});
// 2. Another provider that depends on the first one
final
greetingProvider = Provider<String>((ref) {
// Watch the apiServiceProvider to get the service
final
api = ref.watch(apiServiceProvider);
return
'Using API service: ${api.runtimeType}'
;
});
import
'package:flutter/material.dart'
;
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
import
'providers.dart'
;
void
main
() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
home: HomeScreen(),
);
}
}
class
HomeScreen
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
// 3. Watch the greeting provider to get the value
final
greeting = ref.watch(greetingProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'Dependency Injection'
)),
body: Center(
child: Text(greeting),
),
);
}
}
Provider
that creates and holds an
ApiService
instance.
This ensures the service is created once and reused throughout the app.
apiServiceProvider
.
It uses
ref.watch()
to get the service and creates a greeting string.
StatelessWidget
, we use
ConsumerWidget
to get access to
ref
. We watch
greetingProvider
and display its value.
One of the most powerful features of Riverpod is that providers can depend on other providers. This is called provider composition .
🔗 How Provider Composition Works
When a provider depends on another provider, it uses
ref.watch()
to read the value.
If the depended-upon provider changes, the dependent provider also updates.
This creates a
reactive dependency graph
.
// Provider 1: Holds a counter value
final
counterProvider = StateProvider<int>((ref) => 0);
// Provider 2: Depends on counterProvider
final
doubledCounterProvider = Provider<int>((ref) {
final
count = ref.watch(counterProvider);
// 👈 Watch the dependency
return
count * 2;
// 👈 Compute derived state
});
// Provider 3: Depends on doubledCounterProvider
final
messageProvider = Provider<String>((ref) {
final
doubled = ref.watch(doubledCounterProvider);
return
'The doubled value is: $doubled'
;
});
✅ Why This Is Powerful
-
Reactive updates
– When
counterProviderchanges, bothdoubledCounterProviderandmessageProviderupdate automatically. - Clean separation – Each provider has a single responsibility.
- Testability – Each provider can be tested in isolation.
- Performance – Only the widgets that watch specific providers rebuild.
ref.watch()
is only valid inside the
build()
method of a
ConsumerWidget
or inside a provider's creation function. Using it in callbacks will throw an error.
For buttons, taps, and other callbacks, use
ref.read()
to read the current value
without listening for changes.
Providers should be declared as top-level final variables , not inside widgets. Declaring them inside widgets breaks the global accessibility and can lead to unexpected behavior.
final myProvider = Provider<String>((ref) => 'value');
should be at the top of your file,
outside any class.
Without
ProviderScope
, Riverpod cannot store provider states. This will cause runtime errors.
runApp(ProviderScope(child: MyApp()));
is the standard way to initialize Riverpod.
🎯 Key Takeaway
Providers are the building blocks of Riverpod. They hold state, services, or dependencies and make them available throughout your app. Understanding the different types of providers and when to use each is essential for effective state management. Start with Provider for dependencies and services, and use StateProvider for simple state that changes.