背景
Flutter 中一直有一个很有争议的话题,就是有太多的状态管理框架可以用,开发者不知道如何选择,这是一个非常大的挑战。
我们现在在代码里使用的是 Provider 框架,但是它有很多限制和设计上的缺陷,这很难被移除。
Riverpod 就是一个 rearranged 的 Provider,就如字面意思,它的所有字母就是通过 Provider 重新排列的。
它不仅仅是一个重新排列的 Provider,它还是一个改进版本。
Provider 的缺陷
众所周知,Provider 依赖于 inherited widgets,但随着代码量的增加,Provider 的缺陷越来越暴露出来。
Provider 高度依赖 widget tree 和 build contexts,这本身不是大问题但是它还是带来一些麻烦。
多个 Provider 一起使用是个噩梦,有很多样板代码,把简单的事情变得复杂。
让我们看下面这段代码:
一个 Credentials
类,以及一个依赖它的 Authenticate
类。
我们需要使用 ProxyProvider
来实现,这里有太多的样板代码。
我们经常遇到运行时找不到 Provider 的问题,虽然很容易解决,但是随着代码量越来越多,变得越来越困难。
[图片上传失败...(image-82897a-1647511422989)]
Providers 会从 widget tree 上去找第一个 instance,但是如果有多个相同类型的 provider 时,就会出现预料之外的结果。
上面这些是 Provider 的缺陷。
Riverpod
从设计之初就是想摆脱对 Flutter 的依赖,所以它也可以被用于其他 UI 框架比如 angular_dart。
因为它把 UI 和业务逻辑剥离,所以更容易进行测试。
Riverpod 有三种依赖方式
- riverpod
- flutter_riverpod
- hooks_riverpod
通常我们使用 flutter_riverpod。
ProviderScope
因为 riverpod 是不依赖 flutter 的,那么就需要一个实际的类来和 widget 及 build context 关联,这个类就是 flutter_riverpod 提供的 ProviderScope,通常我们把它包在 App 最外层,这样我们在 App 里只需要顶层这一个,当然你也可以包在任何地方。
Provider
然后我们就可以创建一个最简单的 Provider,这里的概念和我们之前使用的 Provider
框架完全是两个东西,就理解为致敬吧。
Provider<T> 会暴露一个只读的 value。
因为 greetingsProvider 是一个具体的实例,而不是一个类,这样就确保了它的 compile safe。
这样我们就能有多个相同类型的实例而不出现冲突。
声明一个全局变量的方式真的好吗?当然,因为 provider 里是不保存状态的,状态保存在 Scope 里,也就是我们包在顶部的 ProviderScope。
WidgetRef
那么我们如何访问它呢?和 Provider
框架一样,我们通过一个叫 WidgetRef 的东西来访问。
context.read() -> ref.read()
context.watch() -> ref.watch()
context.listen() -> ref.listen()
Consumer
这个 ref 是哪里来的呢?通过一个叫 Consumer 的东西(这个 Consumer 和 Provider
框架里的也不是一个东西)
或者直接通过集成 ConsumerWidget,它是继承自 StatelessWidget 的。
当然也有 ConsumerStatefulWidget。
我们现在知道如何注入一个 provider,但是如果要监听改变,就需要用到这些继承自 AlwaysAliveProviderBase 的 Providers。
我们先从 StateProvider 开始。
假如我们有一个文本按钮,上面显示点击次数。
我们可以这样写:
这看上去和我们之前使用 Provider
框架时用 context 的扩展方法没什么两样,简直可以无缝迁移。
一般来讲,状态总是一个 model/class,这时我们可以和 ChangeNotifier 结合来用了。
StateNotifier
ChangeNotifier 里面还要 notifyListeners,等于还是有样板代码,我们更进一步,使用 StateNotifier 来做:
[图片上传失败...(image-365893-1647511422988)]
更重要的是使用 StateNotifier 是不可变的,也更容易测试。
这里有个稍微不太一样的地方:
ref.read(clicksChangeProvider.notifier).incrementClicks();
这是为了让我们能够使用它暴露出来的方法。
Ref method
接下来讲一下这几个东西的区别:
- Read - ref.read() 表示只读。
- Watch - ref.watch() 表示监听,同时会 rebuild。
- Listen - ref.listen() 表示监听,但不会 rebuild,比如导航这里可能有用。
- Select - provider.select() 表示监听部分,减少 rebuild 次数。
还有一些比较有用的 Provider。
FutureProvider
它其实就是 FutureBuilder 和 Provider 的结合。
可以看到,这里已经把 loading 和 error 状态都自动做了,非常简便。
这里的 tokenValue 是 AsyncValue<T> 类型。
StreamProvider
它的用法和 FutureProvider 差不多。
之前我们一直说 ref,实际上 ref 有两种:
WidgetRef - Consumer 里的 ref。
ProviderReference - Provider 创建时的 ref。
第二种的用法实际上和 Provider
框架里的 ProxyProvider 差不多。
我们用 ProviderReference 来获取其他 provider,简单直接。
Provider.family
Provider 构造还有不少方式,其中一个就是使用 family。
它通常是用来创建带参数的 provider 的。
这里有个 typo,应该是 revokeAuthenticationProvider
如果我们要创建多个值,只要后面多加泛型就可以了,也可以使用 turple 插件。
Provider.autoDispose
有些情况下,在 FutureProvider 里要做一些 dispose 清理工作。
这里通过 autoDispose 构造方法,配合 ref.onDispose() 来处理。
Test
任何中型到大型的应用,对应用程序的测试环节都非常关键。
要达成测试目的,我们通常需要做到以下几点:
- 在
test/testWidgets
里面没有任何的状态保存。 - 能够通过 mock 或者手动操作让 provider 达到一种特定的状态。
测试组件间无状态保存
我们知道 provider 通常是定义成全局的,全局变量会让测试变得很困难。
因为我们需要一些诸如 setUp/tearDown
之类的方法。
事实上,虽然 provider 是全局的,但是 provider 的状态确不是。
实际上,状态保存在一个叫 ProviderContainer
的类中,这个类是被 ProviderScope
隐式创建的。
具体例子:
// A Counter implemented and tested using Flutter
// We declared a provider globally, and we will use it in two tests, to see
// if the state correctly resets to `0` between tests.
final counterProvider = StateProvider((ref) => 0);
// Renders the current state and a button that allows incrementing the state
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Consumer(builder: (context, ref, _) {
final counter = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('$counter'),
);
}),
);
}
}
void main() {
testWidgets('update the UI when incrementing the state', (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);
});
}
可以看到两个测试方法是完全隔离的,没有任何状态上的耦合。
覆写 provider 行为
final todoListProvider = FutureProvider((ref) async => <Todo>[]);
// ...
ProviderScope(
overrides: [
/// Allows overriding a FutureProvider to return a fixed value
todoListProvider.overrideWithValue(
AsyncValue.data([Todo(id: '42', label: 'Hello', completed: true)]),
),
],
child: const MyApp(),
);
参考文档
Riverpod: A deep dive “on the surface”