跳到主要内容

FAQ 常见问题

以下是社区中的一些常见问题:

ref.refreshref.invalidate 之间有什么不同?

您可能已经注意到 ref 有两种方法可以强制提供者程序重新计算,并且想知道它们有何不同。

它比你想象的要简单: ref.refresh 只是 invalidate + read 的语法糖:

T refresh<T>(provider) {
invalidate(provider);
return read(provider);
}

如果您在重新计算后不关心提供者程序的新值, 那么 invalidate 就是正确的选择。
如果这样做,请改用 refresh

信息

该逻辑通过 lint 规则自动执行。 如果您尝试使用 ref.refresh 而不使用返回值,您将收到警告。

行为上的主要区别在于,通过在使提供者程序失效后, 提供者程序会立即重新计算。
然而,如果我们调用 invalidate 但没有立即读取它, 那么更新将稍后触发。

“稍后”更新通常是在下一帧开始时。 然而,如果当前被未监听的提供者程序失效, 则它在再次被监听之前都不会被更新。

为什么 Ref 和 WidgetRef 之间没有共享接口?​

Riverpod 自愿分离 RefWidgetRef
这样做的目的是为了避免编写有条件依赖其中之一的代码。

一个问题是 RefWidgetRef 虽然看起来相似,但存在细微的差异。
依赖于两者的代码将变得不可靠,并且难以发现。

同时,依赖 WidgetRef 就相当于依赖 BuildContext。 它实际上将您的逻辑放在 UI 层中,但不建议这样做。


此类代码应重构为始终使用 Ref

此问题的解决方案通常是将您的逻辑移至 Notifier 中 (请参阅 执行副作用), 然后让您的逻辑成为该 Notifier 的方法。

这样,当您的小部件想要调用此逻辑时,它们可以编写如下内容:

ref.read(yourNotifierProvider.notifier).yourMethod();

yourMethod 将使用 NotifierRef 与其他提供者程序交互。

为什么我们需要扩展 ConsumerWidget 而不是使用原始的 StatelessWidget?

这是由于 InheritedWidget API 中的一个不幸的限制造成的。

有几个问题:

  • 无法使用 InheritedWidget 实现监听器的“当更改时”。 这意味着诸如 ref.listen 之类的内容不能与 BuildContext 一起使用。

    State.didChangeDependencies 是最接近它的东西,但它并不可靠。 一个问题是,即使没有改变依赖关系,生命周期也可能被触发, 特别是如果你的 widget 树使用 GlobalKeys(并且一些 Flutter widget 已经在内部这样做了)。

  • 监听 InheritedWidget 的小部件永远不会停止监听它。 这通常适用于纯元数据,例如 "theme" 或 "media query"。

    对于业务逻辑来说,这是一个问题。 假设您使用提供者程序来表示分页 API。 当页面偏移量发生变化时,您不希望小部件继续监听先前可见的页面。

  • InheritedWidget 无法跟踪小部件何时停止监听它们。 Riverpod 有时依赖于跟踪提供者程序是否被监听。

此功能对于自动处置机制和将参数传递给提供者程序的能力至关重要。
这些功能使 Riverpod 如此强大。

也许在遥远的将来,这些问题将会得到解决。在这种情况下, Riverpod 将迁移到使用 BuildContext 而不是 Ref。 这将允许使用 StatelessWidget 而不是 ConsumerWidget
但那是以后再说了!

为什么 hooks_riverpod 不导出 flutter_hooks?

这是为了尊重良好的版本控制实践。

虽然您不能在没有 flutter_hooks 的情况下使用 hooks_riverpod, 但这两个包都是独立版本控制的。 当其中一个可能会发生重大变化时,不会影响另一个。

为什么 Riverpod 在某些情况下使用 identical 而不是 == 来过滤更新?​

通知者程序使用 identical 而不是 == 来过滤更新。

这是因为 Riverpod 用户为了实现 copyWith 而使用 Freezed/built_value 等代码生成器是很常见的。 这些包重写 == 以深入比较对象。深度对象比较的成本相当高。 “业务逻辑”模型往往具有很多属性。更糟糕的是,他们还有列表、地图等集合。

同时,当使用复杂的“业务”对象时,大多数 state = newState 调用 总是会产生通知(否则调用 setter 没有意义)。一般来说, 当当前状态和新状态相等时,我们调用 state = newState 的主要情况 是对于原始对象(整数、枚举、字符串,但不是列表/类/...)。 这些对象“默认被规范化”。如果这些对象是相等的, 那么它们通常也是“相同的(identical)”。

因此,Riverpod 使用 identical 来过滤更新是一个两全其美的默认值尝试。 对于复杂对象,我们并不真正关心过滤对象, 并且由于代码生成器默认生成 == 覆盖,因此 == 可能会很昂贵, 使用 identical 提供了一种通知监听器的有效方式。 同时,对于简单对象,identical 确实正确过滤了冗余通知。

最后且同样重要的一点是,您可以通过重写通知者程序上的方法 updateShouldNotify 来更改此行为。

有没有办法一次性重置所有提供者程序

不,没有办法立即重置所有提供者程序。

这是故意的,因为它被认为是反模式。 立即重置所有提供者程序通常会重置您不打算重置的提供者程序。

当用户注销时想要重置应用程序状态的用户通常会询问此问题。
如果这就是您所希望的,那么您应该将所有内容都 通过 ref.watch 依赖于 "user" 提供者程序的用户状态。

然后,当用户注销时,依赖于它的所有提供者程序将自动重置,但其他所有内容都将保持不变。

我收到错误“在处理小部件后无法使用‘ref’”,这是怎么回事?​

您可能还会看到 "Bad state: No ProviderScope found",这是同一问题的较旧错误消息。

当您尝试在不再安装的小部件中使用 ref 时,会发生此错误。这通常发生在 await 之后:

ElevatedButton(
onPressed: () async {
await future;
ref.read(...); // 可能抛出 "Cannot use "ref" after the widget was disposed"
}
)

解决方案是,与 BuildContext 一样,在使用 ref 之前检查 mounted

ElevatedButton(
onPressed: () async {
await future;
if (!context.mounted) return;
ref.read(...); // 不再抛出
}
)