Riverpod is a reactive caching and data-binding framework that was born as a complete rewrite the Provider package. It can be used to:
- catch programming errors at compile-time rather than at runtime.
- easily fetch, cache, and update data from a remote source.
- perform reactive caching and easily update your UI.
- depend on asynchronous or computed state.
- create, use, and combine providers with minimal boilerplate code.
- dispose the state of a provider when it is no longer used.
- write testable code and keep your logic outside the widget tree.
The main drawback of the Provider package: Provider is an improvement over InheritedWidget, and thus it depends on the widget tree → lead to the common ProviderNotFoundException
.
Riverpod is compile-safe since all providers are declared globally and can be accessed anywhere → can create providers to hold your application state and business logic outside the widget tree.
Add the latest version of flutter_riverpod
as a dependency to our pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.6
To more easily add Riverpod providers in your code, install the Flutter Riverpod Snippets extension for VSCode or Android Studio / IntelliJ.
ProviderScope is a widget that stores the state of all the providers we create.
Usage: Wrap our root widget with a ProviderScope
:
void main() {
// wrap the entire app with a ProviderScope so that widgets
// will be able to read providers
runApp(ProviderScope(
child: MyApp(),
));
}
For more details about ProviderContainer
and UncontrolledProviderScope
, read: https://codewithandrea.com/articles/riverpod-initialize-listener-app-startup/
It is a provider object that encapsulates a piece of state and allows listening to that state:
- Replace design patterns such as singletons, service locators, dependency injection, and InheritedWidgets.
- Store some state and easily access it in multiple locations.
- Optimize performance by filtering widget rebuilds or caching expensive state computations.
- Make the code more testable, since each provider can be overridden to behave differently during a test.
// provider that returns a string value
final helloWorldProvider = Provider<String>((_) => 'Hello world');
- Use a
ConsumerWidget
// 1. widget class now extends [ConsumerWidget]
class HelloWorldWidget extends ConsumerWidget {
@override
// 2. build method has an extra [WidgetRef] argument
Widget build(BuildContext context, WidgetRef ref) {
// 3. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
}
}
→ If you create widgets that are small and reusable favours composition, leading to code that is concise, more performant, and easier to reason about, then you'll naturally use ConsumerWidget
most of the time.
- Use a
Consumer
lass HelloWorldWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. Add a Consumer
return Consumer(
// 2. specify the builder and obtain a WidgetRef
builder: (_, WidgetRef ref, __) {
// 3. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
},
);
}
}
→ If you have a big widget class with a complex layout, you can use Consumer
to rebuild only the widgets that depend on the provider.
- Using
ConsumerStatefulWidget
&ConsumerState
// 1. extend [ConsumerStatefulWidget]
class HelloWorldWidget extends ConsumerStatefulWidget {
@override
ConsumerState<HelloWorldWidget> createState() => _HelloWorldWidgetState();
}
// 2. extend [ConsumerState]
class _HelloWorldWidgetState extends ConsumerState<HelloWorldWidget> {
@override
void initState() {
super.initState();
// 3. if needed, we can read the provider inside initState
final helloWorld = ref.read(helloWorldProvider);
print(helloWorld); // "Hello world"
}
@override
Widget build(BuildContext context) {
// 4. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
}
}
It is an object that allows widgets to interact with providers and access any provider. There are some similarities between BuildContext
and WidgetRef:
BuildContext
lets us access ancestor widgets in the widget tree (such as Theme.of(context) and MediaQuery.of(context)).WidgetRef
lets us access any provider inside our app.
There are a total of 8 types:
-
Provider → is for accessing dependencies and objects that don't change, e.g., date formatter.
-
StateProvider (legacy, use
Notifier
related objects instead) → is for storing simple state objects that can change like enums, strings, booleans, and numbers, e.g., a counter value. -
StateNotifierProvider (legacy, use
FutureProvider
instead) → listen to and expose aStateNotifier
. ⇒StateNotifierProvider
andStateNotifier
are for managing state that may change in reaction to an event or user interaction, e.g., Clock time change event every second. -
FutureProvider → get the result from an API call that returns a
Future
and often used with theautoDispose
modifier. Here are some usage:- perform and cache asynchronous operations (such as network requests)
- handle the error and loading states of asynchronous operations
- combine multiple asynchronous values into another value
- re-fetch and refresh data (useful for pull-to-refresh operations)
-
StreamProvider → watch a
Stream
of results from a realtime API and reactively rebuild the UI. -
ChangeNotifierProvider (legacy, use
StateNotifier
instead) → is for storing some state and notify listeners when it changes. However, it makes easy to break two important rules: immutable state and unidirectional data flow. -
NotifierProvider (new in Riverpod 2.0)
-
AsyncNotifierProvider (new in Riverpod 2.0) → Riverpod 2.0 introduced new Notifier and AsyncNotifier classes, along their corresponding providers. Ref: How to use Notifier and AsyncNotifier with the new Flutter Riverpod Generator
- call
ref.watch(provider)
to observe a provider's state in thebuild
method and rebuild a widget when it changes. - call
ref.read(provider)
to read a provider's state just once (this can be useful ininitState
or otherlifecycle methods
).
The .notifier
syntax is available with StateProvider
and StateNotifierProvider
only and works as follows:
- call
ref.read(provider.notifier)
on aStateProvider<T>
to return the underlyingStateController<T>
that we can use to modify the state. - call
ref.read(provider.notifier)
on aStateNotifierProvider<T>
to return the underlyingStateNotifier<T>
so we can call methods on it.
Sometimes we want to show an alert dialog or a SnackBar
when a provider state changes.
final counterStateProvider = StateProvider<int>((_) => 0);
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// if we use a StateProvider<T>, the type of the previous and current
// values is StateController<T>
ref.listen<StateController<int>>(counterStateProvider.state, (previous, current) {
// note: this callback executes when the provider value changes,
// not when the build method is called
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Value is ${current.state}')),
);
});
// watch the provider and rebuild when the value changes
final counter = ref.watch(counterStateProvider);
return ElevatedButton(
// use the value
child: Text('Value: $counter'),
// change the state inside a button callback
onPressed: () => ref.read(counterStateProvider.notifier).state++,
);
}
}
When working with FutureProvider
or StreamProvider
, we'll want to dispose of any listeners when our provider is no longer in use to ensure that the stream connection is closed as soon as we leave the page where we're watching the provider.
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
// get FirebaseAuth from another provider
final firebaseAuth = ref.watch(firebaseAuthProvider);
// call method that returns a Stream<User?>
return firebaseAuth.authStateChanges();
});
If desired, we can call ref.keepAlive()
to preserve the state so that the request won't fire again if the user leaves and re-enters the same screen.
Example usage: a KeepAliveLink to implement a timeout-based caching strategy to dispose the provider's state after a given duration.
family
is a modifier that we can use to pass an argument to a provider by adding a second type annotation and an additional parameter that we can use inside the provider body:
final movieProvider = FutureProvider.autoDispose
// additional movieId argument of type int
.family<TMDBMovieBasic, int>((ref, movieId) async {
// get the repository
final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
// call method that returns a Future<TMDBMovieBasic>, passing the movieId as an argument
return moviesRepo.movie(movieId: movieId, cancelToken: cancelToken);
});
class MovieDetailsScreen extends ConsumerWidget {
const MovieDetailsScreen({super.key, required this.movieId});
// pass this as a property
final int movieId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// fetch the movie data for the given movieId
final movieAsync = ref.watch(movieProvider(movieId));
// map to the UI using pattern matching
return movieAsync.when(
data: (movie) => MovieWidget(movie: movie),
loading: (_) => Center(child: CircularProgressIndicator()),
error: (e, __) => Center(child: Text(e.toString())),
);
}
}
Riverpod does not support this, you can pass any custom object that implements hashCode
and the equality operator (objects that use equatable).
Sometimes we want to create a Provider to store a value or object that is not immediately available.
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
// asynchronous initialization can be performed in the main method
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(ProviderScope(
overrides: [
// override the previous value with the new object
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
child: MyApp(),
));
}
Providers can depend on other providers, e.g., SettingsRepository
class that takes an explicit SharedPreferences
.
class SettingsRepository {
const SettingsRepository(this.sharedPreferences);
final SharedPreferences sharedPreferences;
// synchronous read
bool onboardingComplete() {
return sharedPreferences.getBool('onboardingComplete') ?? false;
}
// asynchronous write
Future<void> setOnboardingComplete(bool complete) {
return sharedPreferences.setBool('onboardingComplete', complete);
}
}
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
// watch another provider to obtain a dependency. Using ref.watch() ensures that the provider is updated when the provider we depend on changes. As a result, any dependent widgets and providers will rebuild too.
final sharedPreferences = ref.watch(sharedPreferencesProvider);
// pass it as an argument to the object we need to return
return SettingsRepository(sharedPreferences);
});
As an alternative, we can pass Ref
as an argument when creating the SettingsRepository
:
class SettingsRepository {
const SettingsRepository(this.ref);
final Ref ref;
// synchronous read
bool onboardingComplete() {
final sharedPreferences = ref.read(sharedPreferencesProvider);
return sharedPreferences.getBool('onboardingComplete') ?? false;
}
// asynchronous write
Future<void> setOnboardingComplete(bool complete) {
final sharedPreferences = ref.read(sharedPreferencesProvider);
return sharedPreferences.setBool('onboardingComplete', complete);
}
}
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
return SettingsRepository(ref);
});
→ The sharedPreferencesProvider
becomes an implicit dependency, and we can access it with a call to ref.read()
.
For a ListView
, we can override the provider value inside a nested ProviderScope
:
// 1. Declare a Provider
final currentProductIndex = Provider<int>((_) => throw UnimplementedError());
class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: (context, index) {
// 2. Add a parent ProviderScope
return ProviderScope(
overrides: [
// 3. Add a dependency override on the index
currentProductIndex.overrideWithValue(index),
],
// 4. return a **const** ProductItem with no constructor arguments
child: const ProductItem(),
);
});
}
}
class ProductItem extends ConsumerWidget {
const ProductItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 5. Access the index via WidgetRef
final index = ref.watch(currentProductIndex);
// do something with the index
}
}
This is better for performance because we can create ProductItem
as a const widget in the ListView.builder
. So even if the ListView
rebuilds, our ProductItem
will not rebuild unless its index has changed.
Sometimes you have a model class with multiple properties, and you want to rebuild a widget only when a specific property changes:
class Connection {
Connection({this.bytesSent = 0, this.bytesReceived = 0});
final int bytesSent;
final int bytesReceived;
}
// Using [StateProvider] for simplicity.
// This would be a [FutureProvider] or [StreamProvider] in real-world usage.
final connectionProvider = StateProvider<Connection>((ref) {
return Connection();
});
class BytesReceivedText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// rebuild when bytesSent OR bytesReceived changes
final counter = ref.watch(connectionProvider).state;
return Text('${counter.bytesReceived}');
}
}
Calling ref.watch(connectionProvider)
, our widget will (incorrectly) rebuild when the bytesSent value changes → Use select()
instead:
class BytesReceivedText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// only rebuild when bytesReceived changes
final bytesReceived = ref.watch(connectionProvider.select(
(connection) => connection.state.bytesReceived
));
return Text('$bytesReceived');
}
}
Separate widget tests will never share any state, so there is no need for setUp
and tearDown
methods.
Multiple tests don't share any state because each test has a different ProviderScope
:
void main() {
testWidgets('incrementing the state updates the UI', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));
// The default value is `0`, as declared in our provider
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Increment the state and re-render
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// The state have properly incremented
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});
testWidgets('the counter state is not shared between tests', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));
// The state is `0` once again, with no tearDown/setUp needed
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
}
To mock and override dependencies in tests, use dependency overrides to change the behaviour of a provider by replacing it with a different implementation, for example:
// 1. Create a MockMoviesRepository
class MockMoviesRepository implements MoviesRepository {
@override
Future<List<Movie>> favouriteMovies() {
return Future.value([
Movie(id: 1, title: 'Rick & Morty', posterUrl: 'https://nnbd.me/1.png'),
Movie(id: 2, title: 'Seinfeld', posterUrl: 'https://nnbd.me/2.png'),
]);
}
}
// 2. And in our widget tests, we can override the repository provider
void main() {
testWidgets('Override moviesRepositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
moviesRepositoryProvider
.overrideWithValue(MockMoviesRepository())
],
child: MoviesApp(),
),
);
});
}
References:
A ProviderObserver class can be sub-classed to implement a Logger
that can be used for the entire app:
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('[${provider.name ?? provider.runtimeType}] value: $newValue');
}
}
void main() {
runApp(
ProviderScope(observers: [Logger()], child: MyApp()),
);
}
Add a name to our providers to improve logging:
final counterStateProvider = StateProvider<int>((ref) {
return 0;
}, name: 'counter');
ProviderObserver
is similar to the BlocObserver widget from the flutter_bloc
package.
Ref: Flutter App Architecture with Riverpod: An Introduction
- Official Riverpod example apps: https://github.com/rrousselGit/riverpod/tree/master/examples
- Flutter App Architecture with Riverpod: An Introduction: https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/