跳到主要内容

从 `StateNotifier` 迁移

Riverpod 2.0 一起引入了新类: Notifier/AsyncNotifer
现在不鼓励使用 StateNotifier,转而使用这些新 API。

本页展示如何从已弃用的 StateNotifier 迁移到新的 API。

AsyncNotifier 带来的主要好处是更好的 async 支持;事实上, AsyncNotifier 可以被认为是 FutureProvider ,并且具备从 UI 修改的公开方法。

此外,新的 (Async)Notifier:

  • 在其类中公开 Ref 对象
  • 在代码生成和非代码生成方法之间提供类似的语法
  • 在同步和异步版本之间提供类似的语法
  • 将逻辑从提供者程序中移开,并将其集中到通知者程序本身中

让我们看看如何定义 Notifier、它与 StateNotifier 的比较以及如何迁移新的 AsyncNotifier 以获得异步状态。

新语法比较​

在进行比较之前,请务必了解如何定义 Notifier。 请参阅执行副作用

让我们使用旧的 StateNotifier 语法编写一个示例:

class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);

void increment() => state++;
void decrement() => state++;
}

final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});

这是使用新的 Notifier API 构建的相同示例,大致可翻译为:


class CounterNotifier extends _$CounterNotifier {

int build() => 0;

void increment() => state++;
void decrement() => state++;
}

比较 NotifierStateNotifier,可以观察到以下主要区别:

  • StateNotifier 的反应式依赖项在其提供者程序中声明,而 Notifier 将此逻辑集中在其 build 方法中
  • StateNotifier 的整个初始化过程分为其提供者程序和构造函数, 而 Notifier 保留一个位置来放置此类逻辑
  • 请注意,与 StateNotifier 不同,没有任何逻辑被写入 Notifier 的构造函数中

使用 AsyncNotiferNotifier 的异步等效项可以得出类似的结论。

迁移异步 StateNotifier

新 API 语法的主要吸引力在于改进了异步数据的开发体验。
举个例子:

class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
AsyncTodosNotifier() : super(const AsyncLoading()) {
_postInit();
}

Future<void> _postInit() async {
state = await AsyncValue.guard(() async {
final json = await http.get('api/todos');

return [...json.map(Todo.fromJson)];
});
}

// ...
}

下面是用新的 AsyncNotifier API 重写的上面的示例:


class AsyncTodosNotifier extends _$AsyncTodosNotifier {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');

return [...json.map(Todo.fromJson)];
}

// ...
}

AsyncNotiferNotifier 一样,带来了更简单、更统一的 API。 在这里,很容易将 AsyncNotifer 视为带有方法的 FutureProvider

AsyncNotifer 附带了一组 StateNotifier 没有的实用程序和 getter,例如 futureupdate。 这使我们能够在处理异步突变和副作用时编写更简单的逻辑。 另请参阅执行副作用

提示

StateNotifier<AsyncValue<T>> 迁移到 AsyncNotifer<T> 归结为:

  • 将初始化逻辑放入 build
  • 删除初始化或副作用方法中的任何 catch/try
  • build 中删除任何 AsyncValue.guard ,因为它将 Future 转换为 AsyncValue

优点​

在这几个示例之后,现在让我们重点介绍 NotifierAsyncNotifer 的主要优点:

  • 新语法应该感觉更简单、更具可读性,特别是对于异步状态
  • 一般来说,新 API 的样板代码可能会更少
  • 无论您正在编写哪种类型的提供者程序,语法现在都是统一的,从而支持代码生成 (请参阅关于代码生成

让我们进一步深入并强调更多的差异和相似之处。

显式 .family.autoDispose 修改​

另一个重要的区别是新 API 处理系列和自动处置的方式。

Notifier,有其自己的 .family.autoDispose 对应项, 例如 FamilyNotifierAutoDisposeNotifier
与往常一样,此类修改可以组合使用(又名 AutoDisposeFamilyNotifier)。
AsyncNotifer 也有其异步等效项(例如 AutoDisposeFamilyAsyncNotifier)。

修改在类中明确说明;所有参数都直接注入 build 方法中,以便初始化逻辑可以使用它们。
这应该会带来更好的可读性、更简洁、总体上更少的错误。

以下面的示例为例,其中定义了 StateNotifierProvider.family

class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
BugsEncounteredNotifier({
required this.ref,
required this.featureId,
}) : super(const AsyncData(99));
final String featureId;
final Ref ref;

Future<void> fix(int amount) async {
state = await AsyncValue.guard(() async {
final old = state.requireValue;
final result = await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
return max(old - result, 0);
});
}
}

final bugsEncounteredNotifierProvider =
StateNotifierProvider.family.autoDispose<BugsEncounteredNotifier, int, String>((ref, id) {
return BugsEncounteredNotifier(ref: ref, featureId: id);
});

BugsEncounteredNotifier 感觉...沉重/难以阅读。
让我们看一下它的迁移后的 AsyncNotifier 对应部分:


class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {

FutureOr<int> build(String featureId) {
return 99;
}

Future<void> fix(int amount) async {
final old = await future;
final result = await ref.read(taskTrackerProvider).fix(id: this.featureId, fixed: amount);
state = AsyncData(max(old - result, 0));
}
}

其迁移后的版本应该是一本轻松的读物。

信息

(Async)Notifier.family 参数可通过 this.arg 获取(或使用代码生成时的 this.paramName

生命周期有不同的行为​

Notifier/AsyncNotifierStateNotifier 之间的生命周期有很大不同。

这个例子再次展示了旧 API 如何具有稀疏逻辑:

class MyNotifier extends StateNotifier<int> {
MyNotifier(this.ref, this.period) : super(0) {
// 1 初始化逻辑
_timer = Timer.periodic(period, (t) => update()); // 2 初始化副作用
}
final Duration period;
final Ref ref;
late final Timer _timer;

Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1); // 3 发生突变
if (mounted) state++; // 4 检测挂载属性
}


void dispose() {
_timer.cancel(); // 5 自定义处置逻辑
super.dispose();
}
}

final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
// 6 提供者程序定义
final period = ref.watch(durationProvider); // 7 反应式依赖逻辑
return MyNotifier(ref, period); // 8 传递 `ref`
});

在这里,如果 durationProvider 更新,MyNotifier 会进行处置: 然后重新实例化其实例,然后重新初始化其内部状态。
此外,与其他提供者程序不同的是,dispose 回调将在类中单独定义。
最后,仍然可以在其 provider 中编写 ref.onDispose, 再次显示此 API 的逻辑是多么稀疏;潜在地,开发人员可能必须研究八 (8!) 个不同的地方才能理解此通知者程序行为!

这些歧义可以通过 Riverpod 2.0 解决。

disposeref.onDispose

StateNotifierdispose 方法指的是通知者程序本身的 dispose 事件, 也就是在自行处置之前调用的回调。

(Async)Notifier 没有此属性,因为它们在重建时不会被处置;只有他们的内部状态是。
在新的通知者程序中,处置生命周期仅在一个地方处理,通过 ref.onDispose (和其他),就像任何其他提供者程序一样。 这简化了 API,希望也提高了开发体验,这样只需查看一个地方 即可了解生命周期的副作用:它的 build 方法。

简而言之:要注册在内部状态重建之前触发的回调, 我们可以像其他提供者程序一样使用 ref.onDispose

您可以像这样迁移上面的代码片段:


class MyNotifier extends _$MyNotifier {

int build() {
// 只需在此处读取/写入代码,一目了然
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);

return 0;
}

Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1);
// `mounted` 已不复存在!
state++; // 这可能会抛出。
}
}

在最后一个片段中,肯定有一些简化,但仍然存在一个未解决的问题: 我们现在无法了解我们的通知者程序在执行 update 时是否仍然存在。
这可能会出现不需要的 StateError

不再 mounted

发生这种情况是因为 (Async)Notifier 缺少 mounted 属性, 而该属性在 StateNotifier 上可用。
考虑到它们生命周期的差异,这是完全有道理的;尽管只是可能,mounted 属性可能会误导新通知者程序:mounted 几乎总是 true

虽然可以制定自定义解决方法, 但建议通过取消异步操作来解决此问题。

可以使用自定义完成器 或任何自定义派生程序来取消操作。

例如,如果您使用 Dio 执行网络请求,请考虑使用取消令牌 (另请参阅清除缓存并对状态处置做出反应)。

因此,上面的示例迁移到以下内容:


class MyNotifier extends _$MyNotifier {

int build() {
// 只需在此处读取/写入代码,一目了然
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);

return 0;
}

Future<void> update() async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
// 调用 `cancelToken.cancel` 时,会抛出一个自定义异常
state++;
}
}

突变 API 与之前相同​

到目前为止,我们已经展示了 StateNotifier 和新 API 之间的差异。
相反, NotifierAsyncNotiferStateNotifier 共享的一件事是 如何使用和改变它们的状态。

消费者程序可以使用相同的语法从这三个提供者程序获取数据, 这在您从 StateNotifier 迁移时非常有用;这也适用于通知者程序方法。

class SomeConsumer extends ConsumerWidget {
const SomeConsumer({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterNotifierProvider);
return Column(
children: [
Text("You've counted up until $counter, good job!"),
TextButton(
onPressed: ref.read(counterNotifierProvider.notifier).increment,
child: const Text('Count even more!'),
)
],
);
}
}

其他迁移​

让我们探讨一下 StateNotifierNotifier(或 AsyncNotifier)之间影响较小的差异

.addListener.stream 迁移​

StateNotifier.addListener.stream 可用于监听状态更改。 这两个 API 现在被认为已经过时了。

这是有意为之,因为我们希望与 NotifierAsyncNotifier 和其他提供者程序实现完全的 API 统一。
事实上,使用 NotifierAsyncNotifier 应该与任何其他提供者程序没有任何不同。

因此:

class MyNotifier extends StateNotifier<int> {
MyNotifier() : super(0);

void add() => state++;
}

final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
final notifier = MyNotifier();

final cleanup = notifier.addListener((state) => debugPrint('$state'));
ref.onDispose(cleanup);

// 或者,等效为:
// final listener = notifier.stream.listen((event) => debugPrint('$event'));
// ref.onDispose(listener.cancel);

return notifier;
});

就变成这样了:


class MyNotifier extends _$MyNotifier {

int build() {
ref.listenSelf((_, next) => debugPrint('$next'));
return 0;
}

void add() => state++;
}

简而言之:如果你想监听 Notifier/AsyncNotifer,只需使用 ref.listen。 请参阅组合请求

从测试中的 .debugState 迁移​

StateNotifier 公开 .debugState:此属性供 pkg:state_notifier 用户在开发模式下启用从类外部进行状态访问,以用于测试目的。

如果您在测试中使用 .debugState 访问状态,则您很可能需要放弃这种方法。

Notifier/AsyncNotifer 没有 .debugState;相反,它们直接公开 .state, 即 @visibleForTesting

危险

避免!从测试中访问 .state;如果必须的话,当且仅当您已经正确实例化了 Notifier/AsyncNotifer 时才执行此操作; 然后,您可以在测试中自由访问 .state

事实上,Notifier/AsyncNotifier 不应该手动实例化;相反, 它们应该通过使用其提供者程序进行交互:如果不这样做将会破坏通知者程序, 因为 ref 和 family 参数没有被初始化。

没有 Notifier 实例?
没问题,您可以使用 ref.read 获取一个,就像您读取其暴露状态一样:

void main(List<String> args) {
test('my test', () {
final container = ProviderContainer();
addTearDown(container.dispose);

// 获取通知者程序
final AutoDisposeNotifier<int> notifier =
container.read(myNotifierProvider.notifier);

// 获取其暴露状态
final int state = container.read(myNotifierProvider);

// TODO 编写您的测试
});
}

在其专用指南中了解有关测试的更多信息。请参阅测试你的提供者程序

StateProvider 迁移​

StateProvider 自发布以来就被 Riverpod 暴露出来, 它是为了节省一些代码行数(LoC)来简化 StateNotifierProvider 的版本。
由于 StateNotifierProvider 已被弃用,因此 StateProvider 也应避免使用。
此外,到目前为止,新 API 还没有等效的 StateProvider

尽管如此,从 StateProvider 迁移到 Notifier 很简单。

这样:

final counterProvider = StateProvider<int>((ref) {
return 0;
});

变成:


class CounterNotifier extends _$CounterNotifier {

int build() => 0;


set state(int newState) => super.state = newState;
int update(int Function(int state) cb) => state = cb(state);
}

尽管它花费了我们更多的代码行数(LoC),但从 StateProvider 迁移使我们能够明确地归档 StateNotifier