pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.5
之前翻译过 RiverPod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
初心
本篇深度好文旨在说明为什么 RiverPod 很不一般。
尤其是本篇会回答以下问题:
- 既然 Provider 受到广泛欢迎,为什么需要迁移到 RiverPod ?
- 能获得哪些具体的好处?
- 如何迁移到 RiverPod ?
- 能渐进式迁移吗?
- 等
到了本篇的最后,你会信服相比 Provider 自己会更喜欢 RiverPod 。
相比 Provider ,RiverPod 的确是更现代更值得推荐更可靠的方式。
RiverPod 提供了更好的状态管理能力,更好的缓存策略和更简化的响应模型。
而 Provider 现在在很多缺失的方面没有进步。
Provider 的局限性
受限于 InheritedWidget API ,Provider 有很多原生问题。
Provider 本质上是"简化的 InheritedWidget
"; Provider 只不过是 InheritedWidget 的封装,所以也会受限于此。
下面是已知的 Provider 的问题列表。
Provider 无法保持两个(或更多)同样类型的 provider
声明两个 Provider<Item>
会导致不稳定的行为:InheritedWidget
的 API 只会从 二者之一 中获取:最近的 Provider<Item>
祖先。
变通方案 在 Provider 文档中有说明,RiverPod 则没有这个问题。
可自由地将业务逻辑细分为多个部分以消除该局限性,如下:
dart
@riverpod
List<Item> items(ItemsRef ref) {
return []; // ...
}
@riverpod
List<Item> evenItems(EvenItemsRef ref) {
final items = ref.watch(itemsProvider);
return [...items.whereIndexed((index, element) => index.isEven)];
}
按理说 Provider 一次只产出一个值
当读取外部的 RESTful API 时,通常是展示最后读取的值,新的调用会加载下一次的值。
RiverPod 可通过 AsyncValue
的 API 使该行为一次产出两个值(也就是,上次的数据值,和将要加载的新值)。
dart
@riverpod
Future<List<Item>> itemsApi(ItemsApiRef ref) async {
final client = Dio();
final result = await client.get<List<dynamic>>('your-favorite-api');
final parsed = [...result.data!.map((e) => Item.fromJson(e as Json))];
return parsed;
}
@riverpod
List<Item> evenItems(EvenItemsRef ref) {
final asyncValue = ref.watch(itemsApiProvider);
if (asyncValue.isReloading) return [];
if (asyncValue.hasError) return const [Item(id: -1)];
final items = asyncValue.requireValue;
return [...items.whereIndexed((index, element) => index.isEven)];
}
在前面的代码片段中,监听 evenItemsProvider
会产生以下效果:
- 开始时创建了请求。会获得一个空列表;
- 然后,会告知发生错误。这样会获取
[Item(id: -1)]
; - 然后尝试使用拉取刷新逻辑再次请求(例如,通过
ref.invalidate
)。 - 重新加载最初的 provider 时,第二个 provider 还是会暴露
[Item(id: -1)]
的值; - 这一次正确接收到了一些解析后的数据:正确返回的 Even 项目 。
使用 Provider ,上面的特性很难实现,甚至也没有方便的变通做法。
绑定 provider 比较困难且容易出错
使用 Provider 可能会尝试在 provider 的 create
中使用 context.watch
。 这也是不可靠的,因为即使没有依赖有变化也可能会触发 didChangeDependencies
(例如,在组件树中引入了 GlobalKey 的情况)。
尽管如此,Provider 提供了名为 ProxyProvider
的特别方案,但是这种方式很冗长且容易出错。
绑定状态是 RivderPod 的核心机制,因为零成本响应式地绑定和缓存值只需要已有的强大工具方法,如 ref.watch 和 ref.listen:
dart
@riverpod
int number(NumberRef ref) {
return Random().nextInt(10);
}
@riverpod
int doubled(DoubledRef ref) {
final number = ref.watch(numberProvider);
return number * 2;
}
使用 Riverpod绑定值感觉很自然:依赖是可读的并且 API 也保持不变。
安全性不足
使用 Provider,在重构和(或)大规模修改时,通常会导致 ProviderNotFoundException
。
实际上,该运行时异常是最初想要创建 Riverpod 的主要原因之一。
尽管(Riverpod)提供了比该方式更多的工具,Riverpod 也不会抛出该异常。
状态清理比较困难
InheritedWidget
无法在消费者停止监听时作出响应。
这就使得 Provider 无法在组件的状态不再使用时自动清理。
使用 Provider ,就必须 依靠作用域 provider 在其不再被使用清理状态。
但这种做法并不容易,因为页面间共享状态时会比较迷糊。
Riverpod 用了一些简单易懂的 API 解决这个问题,如 autodispose 和 keepAlive。
dart
// 使用代码生成器,默认就是 .autoDispose 的。
@riverpod
int diceRoll(DiceRollRef ref) {
// 由于该 provider 是 .autoDispose 的,不再监听时会清理当前暴露的状态。
// 然后,无论何时只要该 provider 又被监听,就会产生新的状态并再次暴露出来。
final dice = Random().nextInt(10);
return dice;
}
@riverpod
int cachedDiceRoll(CachedDiceRollRef ref) {
final coin = Random().nextInt(10);
if (coin > 5) throw Exception('Way too large.');
// 上面的情况可能会失败;
// 如果没有失败的话,下面的指令会告诉 Provider 即使不再被监听也要保持缓存的状态。
ref.keepAlive();
return coin;
}
很不幸,使用原生 InheritedWidget
无法实现该方式,更别说用 Provider 了。
欠缺可靠的参数化机制
Riverpod 允许用户使用 .family 修饰符 声明 "参数化的" provider。
实际上,.family
是 Riverpod 最强大的特性之一,也是(Riverpod)创新的核心。
例如,它能极大地简化业务逻辑。
如果想用 Provider 实现同样的处理,就得放弃易用性和此类变量的类型安全。
此外,使用 Provider 无法实现类似 .autoDispose
的机制也从根本上使得 .family
无法实现, 因为两个特性是相互共存的。
最后,正如上面所示,组件无法 停止监听 InheritedWidget
是无法改变的。
所以如果 provider 的状态是动态加载 的,则意味着严重的内存泄漏。也就是说,使用参数构建一个 Provider ,正是 .family
所做的。
因此,对于 Provier ,获得 .family
的处理方式现在从根本上就是不可能的。
测试比较冗长
要编写测试,必须在每个测试中重新定义 provider。
使用 Riverpod ,provider 默认就能使用内部测试。 此外,Riverpod 暴露了一些 mock provider 时便于使用的覆写工具方法集。
测试上面绑定状态的代码片段会为如下这么简单:
dart
void main() {
test('it doubles the value correctly', () async {
final container = ProviderContainer(
overrides: [numberProvider.overrideWith((ref) => 9)],
);
final doubled = container.read(doubledProvider);
expect(doubled, 9 * 2);
});
}
要了解更多关于测试的内容,查看Testing。
触发的副作用不是很简单明了
因为 InheritedWidget
没有 onChange
回调,所以 Provider 也不可能有回调。
这对于导航很成问题,例如 snack bar ,模态窗口等。
Riverpod 只提供了方便的 ref.listen
,就能和 Flutter 完美集成了。
dart
class DiceRollWidget extends ConsumerWidget {
const DiceRollWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(diceRollProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Dice roll! We got: $next')),
);
});
return TextButton.icon(
onPressed: () => ref.invalidate(diceRollProvider),
icon: const Icon(Icons.casino),
label: const Text('Roll a dice'),
);
}
}
转向 Riverpod
从概念上,Riverpod 和 Provider 非常相似。两个包都扮演相似的角色。两个包都尝试实现:
- 缓存和消除一些有状态对象;
- 测试时提供 Mock 这些对象的方法;
- 为组件简化监听这些对象的方式提供一种做法。
可以将 Riverpod 看作是持续发展多年后成熟的 Provider 。
为什么是单独的包?
起初,Provider 的一个主版本要启动,该版本就是为了解决前面提到的问题。
但是之后决定放弃了,因为新的 ConsumerWidget
API 会很有破坏性 ,甚至会导致很大争议。
因为 Provider 仍是最常用的 Flutter 包之一,所以就决定创建单独的包,因此 Riverpod 就被创建出来了。
创建独立的包可以做到:
- 对于任何想迁移的人来说都很容易,因为两种方式可以临时共存;
- 如果原则上不喜欢 Riverpod 或还不认为 Rivderpod 很可靠,人们也能继续坚持使用 Provider ;
- 作为试验,使用 Riverpod 寻求用于生产环境、解决 Provider 各种技术局限性的方案。
实际上,Riverpod 是设计作为 Provider 的精神继承者。因此名为 "Riverpod"("Provider" 的变形单词(字母换了位置))。
破坏性改动
Riverpod 唯一可以称作退步的地方是使用时需要修改组件的类型:
- 原来是继承
StatelessWidget
,使用 Riverpod 应该继承ConsumerWidget
。 - 原来是继承
Statefu地方idget
,使用 Riverpod 应该继承ConsumerStatefulWidget
。
但该不便之处只是九牛一毛。因为这种修改可能只需一天就能解决掉了。
选择正确的库
你很可能会问自己:所以,作为 Provider 用户,我应该使用 Provider 还是 Riverpod ?
我们想非常清楚地回答这个问题:
很可能应该使用 Riverpod
Riverpod 是整体上优化设计的,能大幅简化业务逻辑。