跳到主要内容

Migrating from 2.0 to 3.0

For the list of changes, please refer to the What's new in Riverpod 3.0 page.

Riverpod 3.0 introduces a number of breaking changes that may require you to update your code. They should in general be relatively minor, but we recommend you to read this page carefully.

信息

This migration is supposed to be smooth.
If there is anything that is unclear, or if you encountered a scenario that is difficult to migrate, please open an issue).

It is important to us that the migration is as smooth as possible, so we will do our best to help you ; improve the migration guide ; or even include helpers to make the migration easier.

Automatic retry

Riverpod 3.0 now automatically retries failing providers by default. This means that if a provider fails to compute its value, it will automatically retry until it succeeds.

In general, this is a good thing as it makes your app more resilient to transient errors. However, you may want to disable/customize this behavior in some cases.

To disable automatic retry globally, you can do so on ProviderContainer/ProviderScope:

void main() {
runApp(
ProviderScope(
// Never retry any provider
retry: (retryCount, error) => null,
child: MyApp(),
),
);
}

Alternatively, you can disable automatic retry on a per-provider basis by using the retry parameter of the provider:

// Never retry this specific provider
Duration? retry(int retryCount, Object error) => null;

(retry: retry)
class TodoList extends _$TodoList {

List<Todo> build() => [];
}

Out of view providers are paused

In Riverpod 3.0, out of view providers are paused by default.

There is currently no way to disable this behavior globally, but you can control the pause behavior at the consumer level by using the Visibility widget.

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());
},
),
);
}
}

StateProvider, StateNotifierProvider, and ChangeNotifierProvider are moved to a new import

In Riverpod 3.0, StateProvider, StateNotifierProvider, and ChangeNotifierProvider are considered "legacy".
They are not removed, but are no-longer part of the main API. This is to discourage their use in favor of the new Notifier API.

To keep using them, you need to change your imports to one of the following:

import 'package:hooks_riverpod/legacy.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:riverpod/legacy.dart';

Providers now all use == to filter updates

Before, Riverpod was inconsistent in how it filtered updates to providers. Some providers used == to filter updates, while others used identical. In Riverpod 3.0, all providers now use == to filter updates.

This generally shouldn't change anything in your code. But if you need to, you can override Notifier.updateShouldNotify to customize the behavior.


class TodoList extends _$TodoList {

List<Todo> build() => [];


bool updateShouldNotify(List<Todo> previous, List<Todo> next) {
// Custom implementation
return true;
}
}

In the scenario where you didn't use a Notifier, you can refactor your provider in its notifier equivalent (Such as converting StreamProvider to StreamNotifierProvider).

ProviderObserver has its interface slightly changed

For the sake of mutations, the ProviderObserver interface has changed slightly.

Instead of two separate parameters for ProviderContainer and ProviderBase, a single ProviderObserverContext object is passed. This object contains both the container, provider, and extra information (such as the associated mutation).

To migrate, you need to update all methods of your observers like so:

class MyObserver extends ProviderObserver {
@override
- void didAddProvider(ProviderBase provider, Object? value, ProviderContainer container) {
+ void didAddProvider(ProviderObserverContext context, Object? value) {
// ...
}
}

Simplified Ref and removed Ref subclasses

For the sake of simplification, Ref lost its type parameter. And all properties/methods that were using the type parameter have been moved to Notifiers.

Specifically, ProviderRef.state, Ref.listenSelf and FutureProviderRef.future should be replaced by Notifier.state, Notifier.listenSelf and FutureProviderRef.future respectively.

// Before:

Future<int> value(ValueRef ref) async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});

ref.listenSelf((previous, next) {
print('Log: $previous -> $next');
});

ref.future.then((value) {
print('Future: $value');
});

return 0;
}

// After

class Value extends _$Value {

Future<int> build() async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});

listenSelf((previous, next) {
print('Log: $previous -> $next');
});

future.then((value) {
print('Future: $value');
});

return 0;
}
}

Similarly, all Ref subclasses are removed (such as but not limited to ProviderRef, FutureProviderRef, etc).

This primarily affects code-generation. Instead of MyProviderRef, you can now use Ref directly:

@riverpod
-int example(ExampleRef ref) {
+int example(Ref ref) {
// ...
}

AutoDispose interfaces are moved

The auto-dispose feature is simplified. Instead of relying on a clone of all interfaces, interfaces are unified.

In short, instead of AutoDisposeProvider, AutoDisposeNotifier, etc, you now have Provider, Notifier, etc. The behavior is the same, but the API is simplified.

To easily migrate, you can do a case-sensitive replace of AutoDispose to (empty string).

Provider failures are now rethrown as ProviderExceptions

In Riverpod 3.0, all provider failures are rethrown as ProviderExceptions. This means that if a provider fails to compute its value, reading it will throw a ProviderException instead of the original error.

This can impact you if you were relying on the original error type to handle specific errors. To migrate, you can catch the ProviderException and extract the original error from it:

try {
await ref.read(myProvider.future);
-} on NotFoundException {
- // Handle NotFoundException
+} on ProviderException catch (e) {
+ if (e.exception is NotFoundException) {
+ // Handle NotFoundException
+ }
}
信息

This is only necessary if you were explicitly relying on try/catch to handle such error.

If you are using [AsyncValue] to check for errors, you don't need to change anything:

AsyncValue<int> value = ref.watch(myProvider);
if (value.error is NotFoundException) {
// Handle NotFoundException
// This still works today
}

Notifiers are now recreated when the provider rebuilds

In 2.0, Notifier instances were reused for the lifetime of the provider. In 3.0, a new Notifier is created whenever the provider rebuilds.

This prevents storing state inside the Notifier class itself, as any state outside of the state property will be lost when the provider rebuilds.
To fix this, you will need to move any custom state inside Notifier.state.

// Before:

class TodoList extends _$TodoList {
int _count = 0;


List<Todo> build() => [];
}

// After
class TodosState {
TodosState._(this.todos, this._count);
final List<Todo> todos;
int _count;
}

class TodoList extends _$TodoList {

TodosState build() => TodosState._([], 0);
}