Skip to content

与 Provider 互动的小助手:Ref

一句话总结

Ref 是我们与 Provider 沟通和互动的主要工具。


Ref 是我们与 Provider 沟通和互动的主要工具。

你可以把 Ref 想象成 Flutter 中 BuildContext 的“亲戚”,但它服务于 Provider,而不是 Widget。通过 Ref,我们可以做很多事,比如:

  • 读取或监听一个 Provider 的状态。
  • 检查一个 Provider 当前是否还在加载中。
  • 重置一个 Provider 的状态。

除此之外,Ref 还赋予了 Provider 一种“自我感知”的能力,让它能监听到自己的生命周期事件。这就像是给 Provider 加上了 initStatedispose 方法。这些方法包括:

  • onDispose (当 Provider 被销毁时)
  • onCancel (当 Provider 不再被监听时)
  • 等等...

如何拿到一个 Ref 对象?

在哪儿能拿到 Ref,取决于你当前在代码的哪个位置。

Provider “天生”就能拿到一个 Ref。你可以在创建 Provider 的函数参数里找到它,或者在 Notifier 类里通过 this.ref 访问它。

dart
@riverpod
int myProvider(Ref ref) {
  // 在这里,ref 是现成的
  // ...
}

@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    // 在 Notifier 内部的任何地方,都可以用 this.ref
    ref.watch(someProvider);
    // ...
  }
}

要在 Widget 里拿到 Ref,你需要使用一个叫做 Consumer 的特殊 Widget。

dart
Consumer(
  builder: (context, ref, _) {
    // 在这里,ref 也是现成的
    final value = ref.watch(myProvider);
    return Text('$value');
  },
);

“如果我既不在 Widget 里,也不在 Provider 里,那该怎么拿到 Ref 呢?”

如果你遇到了这种情况,别担心,你写的代码很可能还是和某个 Widget 或 Provider 有着千丝万缕的联系。

这时候,你只需要把从 Widget 或 Provider 那里拿到的 ref 对象,当作参数传递给你自己的函数或对象就行了:

dart
void myFunction(WidgetRef ref) {
  // 你可以把 ref 传来传去!
}

// ...

Consumer(
  builder: (context, ref, _) {
    return ElevatedButton(
      // 把 ref 传给你的函数
      onPressed: () => myFunction(ref), 
      child: Text('Click me'),
    );
  },
);

使用 Ref 与 Provider 互动

和 Provider 的互动,通常可以分为两大类:

  • 监听 Provider 的状态变化。
  • 修改 Provider 的状态(比如重置它、更新它等等)。

监听 Provider 的状态

Riverpod 提供了两种方式来监听一个 Provider:

  • ref.watch - 这是一种“声明式”的监听方式。

    这是最常用、也是我们首选的方式。它能让你的界面或其他的 Provider 自动响应状态变化。

  • ref.listen - 这是一种“手动”的监听方式。

    它的风格更像是传统的 addListener。功能强大,但用起来也更复杂一些。

为了演示,我们先创建一个每秒更新一次的 Provider:

dart
@riverpod
class Tick extends _$Tick {
  @override
  int build() {
    final timer = Timer.periodic(Duration(seconds: 1), (_) => state++);
    ref.onDispose(timer.cancel);
    return 0;
  }
}

ref.watch

ref.watch 是 Riverpod 的标志性功能。它能让你轻松地将多个 Provider 组合在一起,并且当数据变化时,界面能自动刷新。

使用 ref.watch 的感觉和在 Flutter 里用 Theme.of(context) 很像。当你调用 Theme.of(context) 时,你的 Widget 就“订阅”了主题,当主题变化时,Widget 就会重建。同样,当你调用 ref.watch(myProvider) 时,你的 Widget 或 Provider 就“订阅”了 myProvider,当它的状态变化时,就会自动重建。

下面的代码展示了一个 Consumer Widget,它会自动跟着 tickProvider 的更新而刷新:

dart
Consumer(
  builder: (context, ref, _) {
    final tick = ref.watch(tickProvider);
    return Text('Tick: $tick');
  },
);

ref.watch 最酷的地方在于,Provider 自己也能用它!

比如,我们可以创建一个新的 Provider,用来判断当前的 tick 值是否能被 4 整除:

dart
@riverpod
bool isDivisibleBy4(Ref ref) {
  // 在这里“监听” tickProvider
  final tick = ref.watch(tickProvider);
  return tick % 4 == 0;
}

然后,我们可以在界面上转而监听这个新的 Provider:

dart
Consumer(
  builder: (context, ref, _) {
    final isDivisibleBy4 = ref.watch(isDivisibleBy4Provider);
    return Text('Tick 能被 4 整除吗? ${isDivisibleBy4}');
  },
);

现在,我们的界面不再是每秒都刷新了,而只会在这个布尔值(truefalse)发生变化时才刷新。

ref.listen

ref.listen 是一种更手动的监听方式。它类似于 ChangeNotifieraddListener 方法,或者 Streamlisten 方法。

当你希望在 Provider 状态变化时执行一些“副作用”操作时,这个方法就特别有用,比如:

  • 弹出一个对话框
  • 跳转到一个新页面
  • 打印一条日志
  • 等等...
dart
@riverpod
int example(Ref ref) {
  // 在 Provider 内部监听另一个 Provider
  ref.listen(tickProvider, (previous, next) {
    // 每当 tickProvider 变化时,这里就会被调用
    print('Tick 从 $previous 变成了 $next');
  });

  return 0;
}
dart
Consumer(
  builder: (context, ref, _) {
    // 在 Widget 内部监听
    ref.listen(tickProvider, (previous, next) {
      // 每当 tickProvider 变化时,这里就会被调用
      print('Tick 从 $previous 变成了 $next');
    });
  
    return Text('正在监听 Tick 的变化');
  },
);

注意

在 Widget 的 build 方法里使用 ref.listen 是完全安全的,它就是为此设计的。

如果你想在 build 方法之外(比如 State.initState)监听 Provider,应该使用 ref.listenManual

重置 Provider 的状态

通过 ref.invalidate,你可以轻松地重置一个 Provider 的状态。

这会告诉 Riverpod 丢弃当前的状态,并在下次读取时重新计算这个 Provider。

下面的例子会把 tick 的值重置回 0

dart
Consumer(
  builder: (context, ref, _) {
    return ElevatedButton(
      onPressed: () {
        // 让 tickProvider 失效
        // 这会让 tick 从 0 重新开始
        ref.invalidate(tickProvider);
      },
      child: Text('重置 Tick'),
    );
  },
);

小技巧

如果你需要在重置后立刻获取新的状态,可以紧接着调用 ref.read

dart
ref.invalidate(tickProvider);
final newTick = ref.read(tickProvider);

或者,你也可以用 ref.refresh 来一步到位,它能同时完成重置和读取新值的操作:

dart
final newTick = ref.refresh(tickProvider);

这两种写法效果完全一样,ref.refresh 只是 ref.invalidate + ref.read 的一个语法糖。

在用户交互中与 Provider 互动

A last use-case is to interact with a provider's state inside button clicks. In this scenario, we do not want to "listen" to the state. For this case, Ref.read exists.

You can safely call Ref.read button clicks to perform work. The following example will print the current tick value when the button is clicked:

dart
Consumer(
  builder: (context, ref, _) {
    return ElevatedButton(
      onPressed: () {
        // Read the current tick value
        final tick = ref.read(tickProvider);
        print('Current tick: $tick');
      },
      child: Text('Print Tick'),
    );
  },
);

危险

不要为了所谓的“优化”而用 ref.read 来代替 ref.watch。这会让你的代码变得很脆弱,因为当 Provider 的行为改变时,你的 UI 可能无法与真实状态保持同步。

要么就继续使用 ref.watch(这点性能差异几乎可以忽略不计),要么就使用 select 来精确监听你关心的那部分状态:

dart
Consumer(
  builder: (context, ref, _) {
    // ❌ 不要用 "read" 来逃避监听
    final tick = ref.read(tickProvider);

    // ✅ 直接用 "watch" 来监听变化。
    // 这不应该成为你 App 的性能瓶颈,不要过度优化。
    final tick = ref.watch(tickProvider);

    // ✅ 使用 "select" 只监听你关心的那部分状态
    final isEven = ref.watch(
      tickProvider.select((tick) => tick.isEven),
    );

    // ...
  },
);

监听生命周期事件

Ref 对象还有一个 Provider 专属的强大功能:监听生命周期事件。这些事件类似于 Flutter Widget 中的 initStatedispose 等方法。

生命周期监听器通过一系列以 on 开头的方法来注册,比如 onDisposeonCancel

dart
@riverpod
int counter(Ref ref) {
  ref.onDispose(() {
    // 当这个 provider 被销毁时,这里会被调用
    print('Counter provider 正在被销毁');
  });
  
  return 0;
}

小技巧

你不需要手动“注销”这些监听器。

当 Provider 被重置时,Riverpod 会自动帮你清理它们。

当然,如果你非要手动注销,也可以通过监听方法返回的函数来实现。

dart
final unregister = ref.onDispose(() {
  print('这行代码永远不会被调用');
});

// 这会注销掉上面注册的 "onDispose" 监听器
unregister();