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() methodfinal value = ref.watch(myProvider);
ref.read()
Reads a provider once without listening for changes. The widget does not rebuild.
Use in callbacks, event handlersfinal 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.