从 `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--;
}
比较 Notifier
与 StateNotifier
,可以观察到以下主要区别:
StateNotifier
的反应式依赖项在其提供者程序中声明,而Notifier
将此逻辑集中在其build
方法中StateNotifier
的整个初始化过程分为其提供者程序和构造函数, 而Notifier
保留一个位置来放置此类逻辑- 请注意,与
StateNotifier
不同,没有任何逻辑被写入Notifier
的构造函数中
使用 AsyncNotifer
、Notifier
的异步等效项可以得出类似的结论。
迁移异步 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)];
}
// ...
}
AsyncNotifer
与 Notifier
一样,带来了更简单、更统一的 API。
在这里,很容易将 AsyncNotifer
视为带有方法的 FutureProvider
。
AsyncNotifer
附带了一组 StateNotifier
没有的实用程序和 getter,例如
future
和 update
。
这使我们能够在处理异步突变和副作用时编写更简单的逻辑。
另请参阅执行副作用。
从 StateNotifier<AsyncValue<T>>
迁移到 AsyncNotifer<T>
归结为:
- 将初始化逻辑放入
build
- 删除初始化或副作用方法中的任何
catch
/try
块 - 从
build
中删除任何AsyncValue.guard
,因为它将Future
转换为AsyncValue
优点
在这几个示例之后,现在让我们重点介绍 Notifier
和 AsyncNotifer
的主要优点:
- 新语法应该感觉更简单、更具可读性,特别是对于异步状态
- 一般来说,新 API 的样板代码可能会更少
- 无论您正在编写哪种类型的提供者程序,语法现在都是统一的,从而支持代码生成 (请参阅关于代码生成)
让我们进一步深入并强调更多的差异和相似之处。
显式 .family
和 .autoDispose
修改
另一个重要的区别是新 API 处理系列和自动处置的方式。
Notifier
,有其自己的 .family
和 .autoDispose
对应项,
例如 FamilyNotifier
和 AutoDisposeNotifier
。
与往常一样,此类修改可以组合使用(又名 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
/AsyncNotifier
和 StateNotifier
之间的生命周期有很大不同。
这个例子再次展示了旧 API 如何具有稀疏逻辑:
class MyNotifier extends StateNotifier<int> {
MyNotifier(this.ref, this.period) : super(0) {
// 1 init logic
_timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
}
final Duration period;
final Ref ref;
late final Timer _timer;
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1); // 3 mutation
if (mounted) state++; // 4 check for mounted props
}
void dispose() {
_timer.cancel(); // 5 custom dispose logic
super.dispose();
}
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
// 6 provider definition
final period = ref.watch(durationProvider); // 7 reactive dependency logic
return MyNotifier(ref, period); // 8 pipe down `ref`
});
在这里,如果 durationProvider
更新,MyNotifier
会进行处置:
然后重新实例化其实例,然后重新初始化其内部状态。
此外,与其他提供者程序不同的是,dispose
回调将在类中单独定义。
最后,仍然可以在其 provider 中编写 ref.onDispose
,
再次显示此 API 的逻辑是多么稀疏;潜在地,开发人员可能必须研究八 (8!)
个不同的地方才能理解此通知者程序行为!
这些歧义可以通过 Riverpod 2.0
解决。
旧 dispose
与 ref.onDispose
StateNotifier
的 dispose
方法指的是通知者程序本身的 dispose 事件,
也就是在自行处置之前调用的回调。
(Async)Notifier
没有此属性,因为它们在重建时不会被处置;只有他们的内部状态是。
在新的通知者程序中,处置生命周期仅在一个地方处理,通过 ref.onDispose
(和其他),就像任何其他提供者程序一样。
这简化了 API,希望也提高了开发体验,这样只需查看一个地方
即可了解生命周期的副作用:它的 build
方法。
简而言之:要注册在内部状态重建之前触发的回调,
我们可以像其他提供者程序一样使用 ref.onDispose
。
您可以像这样迁移上面的代码片段:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
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` is no more!
state++; // This might throw.
}
}
在最后一个片段中,肯定有一些简化,但仍然存在一个未解决的问题:
我们现在无法了解我们的通知者程序在执行 update
时是否仍然存在。
这可能会出现不需要的 StateError
。
不再 mounted
发生这种情况是因为 (Async)Notifier
缺少 mounted
属性,
而该属性在 StateNotifier
上可用。
考虑到它们生命周期的差异,这是完全有道理的;尽管只是可能,mounted
属性可能会误导新通知者程序:mounted
几乎总是 true
。
虽然可以制定自定义解决方法, 但建议通过取消异步操作来解决此问题。
可以使用自定义完成器 或任何自定义派生程序来取消操作。
例如,如果您使用 Dio
执行网络请求,请考虑使用取消令牌
(另请参阅清除缓存并对状态处置做出反应)。
因此,上面的示例迁移到以下内容:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
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);
// When `cancelToken.cancel` is invoked, a custom Exception is thrown
state++;
}
}
突变 API 与之前相同
到目前为止,我们已经展示了 StateNotifier
和新 API 之间的差异。
相反, Notifier
、AsyncNotifer
和 StateNotifier
共享的一件事是
如何使用和改变它们的状态。
消费者程序可以使用相同的语法从这三个提供者程序获取数据,
这在您从 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!'),
)
],
);
}
}
其他迁移
让我们探讨一下 StateNotifier
和 Notifier
(或 AsyncNotifier
)之间影响较小的差异
从 .addListener
和 .stream
迁移
StateNotifier
的 .addListener
和 .stream
可用于监听状态更改。
这两个 API 现在被认为已经过时了。
这是有意为之,因为我们希望与 Notifier
、AsyncNotifier
和其他提供者程序实现完全的 API 统一。
事实上,使用 Notifier
或 AsyncNotifier
应该与任何其他提供者程序没有任何不同。
因此:
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);
// Or, equivalently:
// final listener = notifier.stream.listen((event) => debugPrint('$event'));
// ref.onDispose(listener.cancel);
return notifier;
});
就变成这样了:
class MyNotifier extends _$MyNotifier {
int build() {
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);
// Obtaining a notifier
final AutoDisposeNotifier<int> notifier = container.read(myNotifierProvider.notifier);
// Obtaining its exposed state
final int state = container.read(myNotifierProvider);
// TODO write your tests
});
}
在其专用指南中了解有关测试的更多信息。请参阅测试你的提供者程序。
从 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
。