What's new in Riverpod 3.0
Welcome to Riverpod 3.0!
This update includes many long-due features, bug fixes, and simplifications of the API.
This version is a transition period toward a simpler, unified Riverpod.
This version contains a few life-cycle changes. Those could break your app in subtle ways. Upgrade carefully.
For the migration guide, please refer to the migration page.
Some of the key highlights include:
- Offline persistence (experimental) - Providers can now opt-in to be persisted to a database
- Mutations (experimental) (code-generation) - A new mechanism to enable interfaces to react to side-effects
- Automatic retry - Providers now refresh when they fail, with exponential backoff
Ref.mounted
- Similar toBuildContext.mounted
, but forRef
.- Generic support (code-generation) - Generated providers can now define type parameters
- Pause/Resume support - Temporarily pause a listener when using
ref.listen
- Unification of the Public APIs - Behaviors are unified and duplicate interfaces are fused
- Provider life-cycle changes - Slight tweaks to how providers behave, to better fit modern code
- New testing utilities:
ProviderContainer.test
- A test util that creates a container and automatically disposes it after the test ends.NotifierProvider.overrideWithBuild
- A way to mock onlyNotifier.build
, without mocking the whole notifier.Future/StreamProvider.overrideWithValue
- The old utilities are back
- Statically safe scoping - New lint rules are added to detect when an override is missing
Offline persistence (experimental)
This feature is experimental and not yet stable. It is usable, but the API may change in breaking ways without a major version bump.
Offline persistence is a new feature that enables caching a provider locally on the device.
Then, when the application is closed and reopened, the provider can be restored from the cache.
Offline persistence is opt-in, and supported by all "Notifier" providers,
and regardless of if you use code generation or not.
Riverpod only includes interfaces to interact with a database. It does not include a database itself.
You can use any database you want, as long as it implements the interfaces.
An official package for SQlife is maintained: riverpod_sqflite.
As a short demo, here's how you can use offline persistence:
- riverpod_generator
- riverpod
Future<JsonSqFliteStorage> storage(Ref ref) async {
// Initialize SQFlite. We should share the Storage instance between providers.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
}
/// A serializable Todo class. We're using Freezed for simple serialization.
abstract class Todo with _$Todo {
const factory Todo({
required int id,
required String description,
required bool completed,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
()
class TodosNotifier extends _$TodosNotifier {
FutureOr<List<Todo>> build() async {
// We call persist at the start of our 'build' method.
// This will:
// - Read the DB and update the state with the persisted value the first
// time this method executes.
// - Listen to changes on this provider and write those changes to the DB.
// We "await" for persist to complete to make sure that the decoding is done
// before we return the state.
await persist(
// We pass our JsonSqFliteStorage instance. No need to "await" the Future.
// Riverpod will take care of that.
storage: ref.watch(storageProvider.future),
// By default, state is cached offline only for 2 days.
// In this example, we tell Riverpod to cache the state forever.
options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
);
// If a state is persisted, we return it. Otherwise we return an empty list.
return state.value ?? [];
}
Future<void> add(Todo todo) async {
// When modifying the state, no need for any extra logic to persist the change.
// Riverpod will automatically cache the new state and write it to the DB.
state = AsyncData([...await future, todo]);
}
}
// A example showcasing JsonSqFliteStorage without code generation.
final storageProvider = FutureProvider<JsonSqFliteStorage>((ref) async {
// Initialize SQFlite. We should share the Storage instance between providers.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
});
/// A serializable Todo class.
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
Todo.fromJson(Map<String, dynamic> json)
: id = json['id'] as int,
description = json['description'] as String,
completed = json['completed'] as bool;
final int id;
final String description;
final bool completed;
Map<String, dynamic> toJson() {
return {
'id': id,
'description': description,
'completed': completed,
};
}
}
final todosProvider =
AsyncNotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
class TodosNotifier extends AsyncNotifier<List<Todo>>
// We mix-in [Persistable] to enable the automatic persistence of the state.
// Since the object will be encoded into JSON, we pass [String]/[String]
// as key/value types.
with
Persistable<List<Todo>, String, String> {
FutureOr<List<Todo>> build() async {
// We call persist at the start of our 'build' method.
// This will:
// - Read the DB and update the state with the persisted value the first
// time this method executes.
// - Listen to changes on this provider and write those changes to the DB.
// We "await" for persist to complete to make sure that the decoding is done
// before we return the state.
await persist(
// A unique key for this state.
// No other provider should use the same key.
key: 'todos',
// We pass our JsonSqFliteStorage instance. No need to "await" the Future.
// Riverpod will take care of that.
storage: ref.watch(storageProvider.future),
// By default, state is cached offline only for 2 days.
// In this example, we tell Riverpod to cache the state forever.
options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
encode: jsonEncode,
decode: (json) {
final decoded = jsonDecode(json) as List;
return decoded
.map((e) => Todo.fromJson(e as Map<String, Object?>))
.toList();
},
);
// If a state is persisted, we return it. Otherwise we return an empty list.
return state.value ?? [];
}
Future<void> add(Todo todo) async {
// When modifying the state, no need for any extra logic to persist the change.
// Riverpod will automatically cache the new state and write it to the DB.
state = AsyncData([...await future, todo]);
}
}
Mutations (experimental) (code-generation only)
This feature is experimental and not yet stable. It is usable, but the API may change in breaking ways without a major version bump.
Mutations are currently only available using code generation.
Support for non-code generation is planned.
A new feature called "mutations" is introduced in Riverpod 3.0.
This feature is a mean to allow your UI to easily show spinners/errors for side-effects (such as form submissions).
The TL;DR is, inside a Notifier
, you can annotate a method with @mutation
:
class Todos extends _$Todos {
Future<List<Todo>> build() async {
// TODO: fetch todos from the server
return [];
}
Future<void> addTodo(String title) async {
// Post the new todo to the server
await http.post('https://example.com/todos', body: {
'title': title,
});
// Refresh the state after the post request
ref.invalidateSelf();
}
}
After that, your UI can use ref.listen
/ref.watch
to listen to the state of mutations:
class AddTodoButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Listen to the status of the "addTodo" side-effect
final addTodo = ref.watch(todosProvider.addTodo);
return switch (addTodo.state) {
// No side-effect is in progress
// Let's show a submit button
MutationIdle() => ElevatedButton(
// Trigger the side-effect on click. This is done by calling
// the "addTodo" object and passing the required parameters.
onPressed: () => addTodo('New Todo'),
child: const Text('Submit'),
),
// The side-effect is in progress. We show a spinner
MutationPending() => const CircularProgressIndicator(),
// The side-effect failed. We show a retry button
MutationErrored() => ElevatedButton(
onPressed: () => addTodo('New Todo'),
child: const Text('Retry'),
),
// The side-effect was successful. We show a success message
MutationSucceeded() => const Text('Todo added!'),
};
}
}
Automatic retry
Starting 3.0, providers that fail during initialization will automatically retry. The retry is done with an exponential backoff, and the provider will be retried until it succeeds or is disposed. This helps when an operation fails due to a temporary issue, such as a lack of network connection.
The default behavior retries any error, and starts with a 200ms delay that
doubles after each retry up to 6.4 seconds.
This can be customized for all providers on ProviderContainer/ProviderScope by passing a retry
parameter:
- ProviderScope
- ProviderContainer
void main() {
runApp(
ProviderScope(
// You can customize the retry logic, such as to skip
// specific errors or add a limit to the number of retries
// or change the delay
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
child: MyApp(),
),
);
}
void main() {
final container = ProviderContainer(
// You can customize the retry logic, such as to skip
// specific errors or add a limit to the number of retries
// or change the delay
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
);
}
Alternatively, this can be configured on a per-provider basis by passing a retry
parameter to the provider constructor:
- riverpod_generator
- riverpod
Duration retry(int retryCount, Object error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
}
(retry: retry)
class TodoList extends _$TodoList {
List<Todo> build() => [];
}
final todoListProvider = NotifierProvider<TodoList, List<Todo>>(
TodoList.new,
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
);
Ref.mounted
The long-awaited Ref.mounted
is finally here! It is similar to BuildContext.mounted
, but for Ref
.
You can use it to check if a provider is still mounted after an async operation:
- riverpod_generator
- riverpod
class TodoList extends _$TodoList {
List<Todo> build() => [];
Future<void> addTodo(String title) async {
// Post the new todo to the server
final newTodo = await api.addTodo(title);
// Check if the provider is still mounted
// after the async operation
if (!ref.mounted) return;
// If it is, update the state
state = [...state, newTodo];
}
}
class TodoList extends Notifier<List<Todo>> {
List<Todo> build() => [];
Future<void> addTodo(String title) async {
// Post the new todo to the server
final newTodo = await api.addTodo(title);
// Check if the provider is still mounted
// after the async operation
if (!ref.mounted) return;
// If it is, update the state
state = [...state, newTodo];
}
}
For this to work, quite a few life-cycle changes were necessary.
Make sure to read the life-cycle changes section.
Generic support (code-generation)
When using code generation, you can now define type parameters for your generated providers. Type parameters work like any other provider parameter, and need to be passed when watching the provider.
T multiply<T extends num>(T a, T b) {
return a * b;
}
// ...
int integer = ref.watch(multiplyProvider<int>(2, 3));
double decimal = ref.watch(multiplyProvider<double>(2.5, 3.5));
Pause/Resume support
In 2.0, Riverpod already had some form of pause/resume support, but it was fairly limited.
With 3.0, all ref.listen
listeners can be manually paused/resumed on demand:
final subscription = ref.listen(
todoListProvider,
(previous, next) {
// Do something with the new value
},
);
subscription.pause();
subscription.resume();
At the same time, Riverpod now pauses providers in various situations:
- When a provider is no-longer visible, it is paused (Based off Visibility).
- When a provider rebuilds, its subscriptions are paused until the rebuild completes.
- When a provider is paused, all of its subscriptions are paused too.
See the life-cycle changes section for more details.
Unification of the Public APIs
One goal of Riverpod 3.0 is to simplify the API. This includes:
- Highlighting what ha recommended and what is not
- Removing needless interface duplicates
- Making sure all functionalities function in a consistent way
For this sake, a few changes were made:
[StateProvider]/[StateNotifierProvider] and [ChangeNotifierProvider] are discouraged and moved to a different import
Those providers are not removed, but simply moved to a different import. Instead of:
import 'package:riverpod/riverpod.dart';
You should now use:
import 'package:riverpod/legacy.dart';
This is to highlight that those providers are not recommended anymore.
At the same time, those are preserved for backward compatibility.
AutoDispose interfaces are removed
No, the "auto-dispose" feature isn't removed. This only concerns the interfaces.
In 2.0, all providers, Refs and Notifiers were duplicated for the sake of auto-dispose (
Ref
vs AutoDisposeRef
, Notifier
vs AutoDisposeNotifier
, etc).
This was done for the sake of having a compilation error in some edge-cases, but came
at the cost of a worse API.
In 3.0, the interfaces are unified, and the previous compilation error is now implemented
as a lint rule (using riverpod_lint).
What this means concretely is that you can replace all references to
AutoDisposeNotifier
with Notifier
. The behavior of your code should not change.
final provider = NotifierProvider.autoDispose<MyNotifier, int>(
MyNotifier.new,
);
- class MyNotifier extends AutoDisposeNotifier<int> {
+ class MyNotifier extends Notifier<int> {
}
One Ref
to rule them all
In Riverpod 2.0, each provider came with its own Ref subclass (FutureProviderRef
, StreamProviderRef
, etc).
Some Ref
had state
property, some a future
, or a notifier
, etc.
Although useful, this was a lot of complexity for not much gain. One of the reasons
for that is because Notifiers already have the extra properties it had,
so the interfaces were redundant.
In 3.0, Ref
is unified. No more generic parameter such as Ref<T>
,
no more FutureProviderRef
. We only have one thing: Ref
.
What this means in practice is, the syntax for generated providers is simplified:
-Example example(ExampleRef ref) {
+Example example(Ref ref) {
return Example();
}
All updateShouldNotify
now use ==
updateShouldNotify
is a method that is used to determine if a provider should
notify its listeners when a state change occurs.
But in 2.0, the implementation of this method varied quite a bit between providers.
Some providers used ==
, some identical
, and some more complex logic.
Starting 3.0, all providers use ==
to filter notifications.
This can impact you in a few ways:
- Some of your providers may not notify their listeners anymore in certain situations.
- Some listeners may be notified more often than before.
- If you have a large data class that overrides
==
, you may see a small performance impact.
If you are impacted by those changes, you can override updateShouldNotify
to
use a custom implementation:
- riverpod_generator
- riverpod
class TodoList extends _$TodoList {
List<Todo> build() => [];
bool updateShouldNotify(List<Todo> previous, List<Todo> next) {
// Custom implementation
return true;
}
}
class TodoList extends Notifier<List<Todo>> {
List<Todo> build() => [];
bool updateShouldNotify(List<Todo> previous, List<Todo> next) {
// Custom implementation
return true;
}
}
Provider life-cycle changes
When reading a provider results in an exception, the error is now wrapped in a ProviderException
Before, if a provider threw an error, Riverpod would sometimes rethrow that error directly:
- riverpod_generator
- riverpod
Future<int> example(Ref ref) async {
throw StateError('Error');
}
// ...
ElevatedButton(
onPressed: () async {
// This will rethrow the StateError
ref.read(exampleProvider).requireValue;
// This also rethrows the StateError
await ref.read(exampleProvider.future);
},
child: Text('Click me'),
);
final exampleProvider = FutureProvider<int>((ref) async {
throw StateError('Error');
});
// ...
ElevatedButton(
onPressed: () async {
// This will rethrow the StateError
ref.read(exampleProvider).requireValue;
// This also rethrows the StateError
await ref.read(exampleProvider.future);
},
child: Text('Click me'),
);
In 3.0, this is changed. Instead, the error will be encapsulated in a ProviderException
that contains both the original error and its stack trace.
AsyncValue.error
, ref.listen(..., onError: ...)
and ProviderObservers are unaffected by this change,
and will still receive the unaltered error.
This has multiple benefits:
- Debugging is improved, as we have a much better stack trace
- It is now possible to determine if a provider failed, or if it is in error state because it depends on another provider that failed.
For example, a ProviderObserver can use this to avoid logging the same error twice:
class MyObserver extends ProviderObserver {
void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) {
if (error is ProviderException) {
// The provider didn't fail directly, but instead depends on a failed provider.
// The error was therefore already logged.
return;
}
// Log the error
print('Provider failed: $error');
}
}
This is used internally by Riverpod in its automatic retry mechanism. The default automatic retry
ignores ProviderException
s:
ProviderContainer(
// Example of the default retry behavior
retry: (retryCount, error) {
if (error is ProviderException) return null;
// ...
},
);
Listeners inside widgets that are not visible are now paused
Now that Riverpod has a way to pause listeners, Riverpod uses that to natively pauses listeners when the widget is not visible. In practice what this means is: Providers that are not used by the visible widget tree are paused.
As a concrete example, consider an application with two routes:
- A home page, listening to a websocket using a provider
- A settings page, which does not rely on that websocket
In typical applications, a user first opens the home page and then opens the settings page. This means that while the settings page is open, the homepage is also open, but not visible.
In 2.0, the homepage would actively keep listening to the websocket.
In 3.0, the websocket provider will instead be paused, possibly saving resources.
How it works:
Riverpod relies on Visibility to determine if a widget is visible or not. And when
false, all listeners of a Consumer are paused.
It also means that you can rely on Visibility yourself to manually control the pause behavior of your consumers. You can voluntarily set the value to true/false to forcibly resume/pause listeners:
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Visibility(
visible: false, // This will pause the listeners
child: Consumer(
builder: (context, ref, child) {
// This "watch" will be paused
// until Visibility is set to true
final value = ref.watch(myProvider);
return Text(value.toString());
},
),
);
}
}
If a provider is only used by paused providers, it is paused too
Riverpod 2.0 already had some form of pause/resume support. But it was limited and failed
to cover some edge-cases.
Consider:
- riverpod_generator
- riverpod
int example(Ref ref) {
ref.keepAlive();
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
return 0;
}
final exampleProvider = Provider<int>((ref) {
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
return 0;
});
In 2.0, if you were to call ref.read
once on this provider,
the state of the provider would be maintained, but 'paused' will be printed. This is because
calling ref.read
does not "listen" to the provider. And since the provider is not "listened"
to, it is paused.
This is useful to pause providers that are currently not used!
The problem is that in many cases, this optimization does not work.
For example, your provider could be used indirectly through another provider.
- riverpod_generator
- riverpod
int another(Ref ref) {
ref.keepAlive();
return ref.watch(exampleProvider);
}
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Button(
onPressed: () {
ref.read(anotherProvider);
},
child: Text('Click me'),
);
}
}
final exampleProvider = Provider<int>((ref) {
return ref.watch(exampleProvider);
});
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Button(
onPressed: () {
ref.read(anotherProvider);
},
child: Text('Click me'),
);
}
}
In this scenario, if we click on the button once,
then anotherProvider
will start listening to our exampleProvider
. But anotherProvider
is no-longer used and will be paused. Yet exampleProvider
will not be paused,
because it thinks that it is still being used.
As such, clicking on the button will not print 'paused' anymore.
In 3.0, this is fixed. If a provider is only used by paused providers, it is paused too.
When a provider rebuilds, its previous subscriptions now are kept until the rebuild completes
In 2.0, there was a known inconvenience when using asynchronous providers combined with 'auto-dispose'.
Specifically, when an asynchronous provider watches an auto-dispose provider
after an await
, the "auto dispose" could be triggered unexpectedly.
Consider:
- riverpod_generator
- riverpod
Stream<int> autoDispose(Ref ref) {
ref.onDispose(() => print('disposed'));
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
// A stream that emits a value every second
return Stream.periodic(Duration(seconds: 1), (i) => i);
}
Future<int> asynchronousExample(Ref ref) async {
print('Before async gap');
// An async gap inside a provider ; typically an API call.
// This will dispose the "autoDispose" provider
// before the async operation is completed
await null;
print('after async gap');
// We listen to our auto-dispose provider
// after the async operation
return ref.watch(autoDisposeProvider.future);
}
void main() {
final container = ProviderContainer();
// This will print 'disposed' every second,
// and will constantly print 0
container.listen(asynchronousExampleProvider, (_, value) {
if (value is AsyncData) print('${value.value}\n----');
});
}
final autoDisposeProvider = StreamProvider.autoDispose<int>((ref) {
ref.onDispose(() => print('disposed'));
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
// A stream that emits a value every second
return Stream.periodic(Duration(seconds: 1), (i) => i);
});
final asynchronousExampleProvider = FutureProvider<int>((ref) async {
print('Before async gap');
// An async gap inside a provider ; typically an API call.
// This will dispose the "autoDispose" provider
// before the async operation is completed
await null;
print('after async gap');
// We listen to our auto-dispose provider
// after the async operation
return ref.watch(autoDisposeProvider.future);
});
void main() {
final container = ProviderContainer();
// This will print 'disposed' every second,
// and will constantly print 0
container.listen(asynchronousExampleProvider, (_, value) {
if (value is AsyncData) print('${value.value}\n----');
});
}
In you run this on Dartpad, you will see that its prints:
// First print
Before async gap
after async gap
0
---- // Second and after prints
paused
Before async gap
disposed // The 'autoDispose' provider was disposed during the async gap!
after async gap
0
----
paused
Before async gap
disposed
after async gap
0
----
... // And so on every second
As you can see, this consistently prints 0
every second,
because the autoDispose
provider repeatedly gets disposed during the async gap.
A workaround was to move the ref.watch
call before the await
statement.
But this is error prone, not very intuitive, and not always possible.
In 3.0, this is fixed by delaying the disposal of listeners.
When a provider rebuilds, instead of immediately removing all of its listeners,
it pauses them.
The exact same code will now instead print:
// First print
Before async gap
after async gap
0
----
paused
Before async gap
after async gap
resumed
1
----
paused
Before async gap
after async gap
resumed
2
----
... // And so on every second
When a Notifier rebuilds, a new instance of the Notifier class is created
In 2.0, Notifier
s instances were created once, then reused for the lifetime of the provider.
This enabled storing custom state in the Notifier
class. Unfortunately, this also meant
caused some possible race conditions ; especially when we consider the new "offline persistence" feature.
Offline persistence works by differentiating the first build of a provider
from the subsequent builds. This is done by checking Ref.isFirstBuild
.
The problem is, if Notifier
instances are reused and a rebuild occurs
while Notifier.build
is still running, this could confuse offline persistence.
Although unfortunate, for 3.0 the decision was made to break the old behavior
and create a new instance of the Notifier
class every time the provider rebuilds.
On the upside, it means that you can now safely rely on late final
variables initialized during build
:
- riverpod_generator
- riverpod
class MyNotifier extends _$MyNotifier {
late final int _cached;
int build() {
// This is now safe, as a new Notifier instance is created upon rebuild
_cached = ref.watch(myProvider);
return 0;
}
void increment() {
print(_cached);
}
}
class MyNotifier extends Notifier<int> {
late final int _cached;
int build() {
// This is now safe, as a new Notifier instance is created upon rebuild
_cached = ref.watch(myProvider);
return 0;
}
void increment() {
print(_cached);
}
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
New testing utilities
ProviderContainer.test
In 2.0, typical testing code would rely on a custom-made utility called createContainer
.
In 3.0, this utility is now part of Riverpod, and is called ProviderContainer.test
.
It creates a new container, and automatically disposes it after the test ends.
void main() {
test('My test', () {
final container = ProviderContainer.test();
// Use the container
// ...
// The container is automatically disposed after the test ends
});
}
You can safely do a global search-and-replace for createContainer
to ProviderContainer.test
.
NotifierProvider.overrideWithBuild
It is now possible to mock only the Notifier.build
method, without mocking the whole notifier.
This is useful when you want to initialize your notifier with a specific state, but still want to
use the original implementation of the notifier.
- riverpod_generator
- riverpod
class MyNotifier extends _$MyNotifier {
int build() => 0;
void increment() {
state++;
}
}
void main() {
final container = ProviderContainer.test(
overrides: [
myNotifierProvider.overrideWithBuild((ref) {
// Mock the build method to start at 42.
// The "increment" method is unaffected.
return 42;
}),
],
);
}
class MyNotifier extends Notifier<int> {
int build() => 0;
void increment() {
state++;
}
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
void main() {
final container = ProviderContainer.test(
overrides: [
myNotifierProvider.overrideWithBuild((ref) {
// Mock the build method to start at 42.
// The "increment" method is unaffected.
return 42;
}),
],
);
}
Future/StreamProvider.overrideWithValue
A while back, FutureProvider.overrideWithValue
and StreamProvider.overrideWithValue
were removed "temporarily" from Riverpod.
They are finally back!
- riverpod_generator
- riverpod
Future<int> myFutureProvider() async {
return 42;
}
void main() {
final container = ProviderContainer.test(
overrides: [
// Initializes the provider with a value.
// Changing the override will update the value.
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
);
}
Future<int> myFutureProvider() async {
return 42;
}
void main() {
final container = ProviderContainer.test(
overrides: [
// Initializes the provider with a value.
// Changing the override will update the value.
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
);
}
Statically safe scoping (code-generation only)
Through riverpod_lint, Riverpod now includes a way to detect when scoping is used incorrectly. This lints detects when an override is missing, to avoid runtime errors.
Consider:
// A typical "scoped provider"
(dependencies: [])
Future<int> myFutureProvider() => throw UnimplementedError();
To use this provider, you have two options.
If neither of the following options are used, the provider will throw an error at runtime.
- Override the provider using
ProviderScope
before using it:class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
// A consumer is necessary to access the overridden provider
child: Consumer(
builder: (context, ref, child) {
// Use the provider
final value = ref.watch(myFutureProvider);
return Text(value.toString());
},
),
);
}
} - Specify
@Dependencies
on whatever uses the scoped provider to indicate that it depends on it.After specifying([myFuture])
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Use the provider
final value = ref.watch(myFutureProvider);
return Text(value.toString());
}
}@Dependencies
, all usages ofMyWidget
will require the same two options as above:- Either override the provider using
ProviderScope
before usingMyWidget
void main() {
runApp(
ProviderScope(
overrides: [
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
child: MyWidget(),
),
);
} - Or specify
@Dependencies
on whatever usesMyWidget
to indicate that it depends on it.([myFuture])
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// MyApp indirectly uses scoped providers through MyWidget
return MyWidget();
}
}
- Either override the provider using
Other changes changes
AsyncValue
AsyncValue received various changes.
- It is now "sealed". This enables exhaustive pattern matching:
AsyncValue<int> value;
switch (value) {
case AsyncData():
print('data');
case AsyncError():
print('error');
case AsyncLoading():
print('loading');
// No default case needed
} valueOrNull
has been renamed tovalue
. The oldvalue
is removed, as its behavior related to errors was odd. To migrate, do a global search-and-replace ofvalueOrNull
->value
.AsyncValue.isFromCache
has been added.
This flag is set when a value is obtained through offline persistence. It enables your UI to differentiate state coming from the database and state from the server.- An optional
progress
property is available onAsyncLoading
. This enables your providers to define the current progress for a request:- riverpod_generator
- riverpod
class MyNotifier extends _$MyNotifier {
Future<User> build() async {
// You can optionally pass a "progress" to AsyncLoading
state = AsyncLoading(progress: .0);
await fetchSomething();
state = AsyncLoading(progress: 0.5);
return User();
}
}class MyNotifier extends AsyncNotifier<User> {
Future<User> build() async {
// You can optionally pass a "progress" to AsyncLoading
state = AsyncLoading(progress: .0);
await fetchSomething();
state = AsyncLoading(progress: 0.5);
return User();
}
}
All Ref listeners now return a way to remove the listener
It is now possible to "unsubscribe" to the various life-cycles listeners:
- riverpod_generator
- riverpod
Future<int> example(Ref ref) {
// onDispose and other life-cycle listeners return a function
// to remove the listener.
final removeListener = ref.onDispose(() => print('dispose));
// Simply call the function to remove the listener:
removeListener();
// ...
}
final exampleProvider = FutureProvider<int>((ref) {
// onDispose and other life-cycle listeners return a function
// to remove the listener.
final removeListener = ref.onDispose(() => print('dispose));
// Simply call the function to remove the listener:
removeListener();
// ...
});
Weak listeners - listen to a provider without preventing auto-dispose.
When using Ref.listen
, you can optionally specify weak: true
:
- riverpod_generator
- riverpod
Future<int> example(Ref ref) {
ref.listen(
anotherProvider,
// Specify the flag
weak: true,
(previous, next) {},
);
// ...
}
final exampleProvider = FutureProvider<int>((ref) {
ref.listen(
anotherProvider,
// Specify the flag
weak: true,
(previous, next) {},
);
// ...
});
Specifying this flag will tell Riverpod that it can still dispose the listened provider if it stops being used.
This flag is an advanced feature to help with some niche use-cases regarding combining multiple "sources of truth" in a single provider.