Skip to content

让 Provider 学会“断舍离”:自动销毁

一句话总结

Riverpod 允许你告诉框架,在某个 Provider 不再被使用时,自动销毁它所关联的资源,做到“人走茶凉”,不留垃圾。


在 Riverpod 中,我们可以让框架在某个 Provider 不再被使用时,自动销毁它所关联的资源。

如何开启/关闭自动销毁?

如果你正在使用代码生成,那么这个功能是默认开启的。你可以通过在注解里配置来关闭它:

dart
// 关闭自动销毁功能
@Riverpod(keepAlive: true)
String helloWorld(Ref ref) => 'Hello world!';

如果你没有使用代码生成,你可以在创建 Provider 时通过 isAutoDispose: true 来手动开启它:

dart
final helloWorldProvider = Provider<String>(
  // 手动选择开启自动销毁
  isAutoDispose: true,
  (ref) => 'Hello world!',
);

注意

开启或关闭自动销毁,并不会影响 Provider 在被“重新计算”时其状态是否被销毁。

只要 Provider 被重新计算,旧的状态就总是会被销毁。

注意

当你的 Provider 需要接收参数时(也就是使用 Family 时),我们强烈建议开启自动销毁。否则,每一种参数组合都会创建一个新的状态并一直保留在内存中,这很容易导致内存泄漏。

自动销毁是何时触发的?

当自动销毁功能开启后,Riverpod 会默默地“监视”着这个 Provider 是否还有“听众”。它通过追踪 ref.watch / ref.listen 等调用来统计听众的数量。

当听众数量变为零时,这个 Provider 就被认为是“不再被使用”了,此时 ref.onCancel 会被触发。紧接着,Riverpod 会等待一帧的时间(你可以想象成 await null)。如果在这一帧之后,这个 Provider 仍然没有任何新的听众,那么它就会被彻底销毁,并且 ref.onDispose 会被触发。

如何响应状态的销毁?

在 Riverpod 中,有几种内置的方式会导致一个状态被销毁:

  • Provider 不再被使用,并且它处于“自动销毁”模式。在这种情况下,与该 Provider 关联的所有状态都会被销毁。
  • Provider 被重新计算,比如通过 ref.watch 监听的另一个 Provider 发生了变化。在这种情况下,旧的状态会被销毁,然后创建一个新的状态。

在这两种情况下,你可能都希望在状态被销毁时执行一些清理工作。

这可以通过 ref.onDispose 来实现。这个方法允许你注册一个当状态被销毁时触发的回调。

例如,你可能会用它来关闭一个活跃的 StreamController

dart
@riverpod
Stream<int> example(Ref ref) {
  final controller = StreamController<int>();

  // 当状态被销毁时,我们关闭这个 StreamController。
  ref.onDispose(controller.close);

  // 待办:向 StreamController 推送一些值
  return controller.stream;
}

注意

ref.onDispose 的回调函数不应该触发任何副作用。在 onDispose 内部修改其他 Provider 可能会导致意想不到的行为。

小贴士

还有一些其他有用的生命周期方法,比如:

  • ref.onCancel:当 Provider 的最后一个听众被移除时调用。
  • ref.onResume:在 onCancel 被调用后,当一个新的听众被添加时调用。

小贴士

你可以根据需要多次调用 ref.onDispose。对于你 Provider 中每一个需要被清理的对象,都为它调用一次 ref.onDispose。这种做法可以让我们更容易地发现是否忘记了清理某些资源。

使用 ref.invalidate 手动强制销毁一个 Provider

有时候,你可能想主动强制销毁一个 Provider。这可以通过 ref.invalidate 来实现,你可以在另一个 Provider 或 Widget 中调用它。

使用 ref.invalidate 会销毁当前的 Provider 状态。之后可能会发生两种情况:

  • 如果这个 Provider 正在被监听,那么一个新的状态会被创建。
  • 如果这个 Provider 没有被监听,那么它将被彻底销毁。
dart
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // 点击时,销毁这个 provider。
        ref.invalidate(someProvider);
      },
      child: const Text('销毁一个 provider'),
    );
  }
}

小贴士

Provider 也可以通过 ref.invalidateSelf 来销毁自己。不过在这种情况下,总会立即创建一个新的状态。

小贴士

当你想要销毁一个带参数的 Provider (Family) 时,你可以选择只销毁一个特定的参数组合,或者一次性销毁所有参数组合:

dart
@riverpod
String label(Ref ref, String userName) {
  return 'Hello $userName';
}

// ...

void onTap() {
  // 销毁这个 provider 的所有可能的参数组合。
  ref.invalidate(labelProvider);
  // 只销毁一个特定的组合
  ref.invalidate(labelProvider('John'));
}

使用 ref.keepAlive 进行精细化的销毁控制

如前所述,当自动销毁开启时,如果一个 Provider 在一整帧的时间里都没有听众,它的状态就会被销毁。

但有时你可能想对这个行为有更精细的控制。例如,你可能想保留那些成功获取到的网络请求结果,但对于失败的请求则不进行缓存。

这可以通过 ref.keepAlive 来实现。使用它,你可以决定何时停止自动销毁。

dart
@riverpod
Future<String> example(Ref ref) async {
  final response = await http.get(Uri.parse('https://example.com'));
  // 我们只在请求成功完成后才保持 provider 的存活。
  // 如果请求失败(并抛出异常),那么当 provider 停止被监听时,
  // 它的状态就会被销毁。
  ref.keepAlive();

  // 我们可以使用返回的 `link` 对象来恢复自动销毁行为:
  // link.close();

  return response.body;
}

注意

如果这个 Provider 被重新计算,自动销毁功能会重新被启用。

也可以使用 ref.keepAlive 的返回值来手动恢复自动销毁。

示例:让状态在特定时间内保持存活

目前,Riverpod 没有内置的方法来让状态在特定时间内保持存活。

但利用我们目前所学的工具,实现这样一个功能既简单又可复用。

通过结合使用 Timerref.keepAlive,我们就可以让状态在特定时间内保持存活。为了让这个逻辑可复用,我们可以将它实现为一个扩展方法:

dart
extension CacheForExtension on Ref {
  /// 让 provider 存活 [duration] 时间。
  void cacheFor(Duration duration) {
    // 立即阻止状态被销毁。
    final link = keepAlive();
    // 在持续时间过后,我们重新启用自动销毁。
    final timer = Timer(duration, link.close);

    // 可选:当 provider 被重新计算时(例如通过 ref.watch),
    // 我们取消这个待处理的计时器。
    onDispose(timer.cancel);
  }
}

然后,我们就可以像这样使用它:

dart
@riverpod
Future<Object> example(Ref ref) async {
  /// 让状态存活 5 分钟
  ref.cacheFor(const Duration(minutes: 5));

  return http.get(Uri.https('example.com'));
}

这个逻辑可以根据你的需求进行调整。例如,你可以使用 ref.onCancel/ref.onResume 来实现仅当一个 Provider 在特定时间内没有被监听时才销毁其状态。