Riverpod 的核心:到底什么是“提供者” (Provider)?
一句话总结
“提供者” (Provider) 是 Riverpod 的灵魂。可以说,我们用 Riverpod,就是冲着它的 Provider 来的。
“提供者” (Provider) 是 Riverpod 的灵魂。可以说,我们用 Riverpod,就是冲着它的 Provider 来的。
那么,Provider 到底是个啥?
简单来说,Provider 就是一个 “带了点额外功能的、有记忆的函数”。
这是什么意思呢?意思是,Provider 本质上是个函数,但它很聪明,当你用相同的参数去调用它时,它不会每次都重新计算,而是直接给你上次算好的、缓存起来的结果。
最常见的应用场景就是网络请求。
想象一下,我们有一个从服务器获取用户信息的函数:
Future<User> fetchUser() async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}这个普通函数有个问题:如果我们在好几个 Widget 里都想用它,我们就得自己手动管理请求回来的数据,比如自己搞个缓存,还得想办法把这个数据共享给所有需要它的 Widget。太麻烦了!
这时候,Provider 就闪亮登场了。Provider 就像是给普通函数套上了一个“超能外套”,它会自动帮你缓存函数的结果,并让多个 Widget 轻松共享同一个数据:
// 这就是 fetchUser 函数的“超能版”
// 它会自动缓存结果,多次调用也只会请求一次
@riverpod
Future<User> user(Ref ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}除了基本的缓存功能,Provider 还附带了一堆让它更强大的“超能力”:
聪明的缓存管理
特别是
ref.watch这个功能,能让你把好几个缓存组合起来,当其中一个变化时,它能自动更新其他依赖它的缓存。自动“垃圾回收”
当一个 Provider 不再被任何地方使用时,它会自动清理自己占用的资源,非常省心。
无缝的数据绑定
有了 Provider,你再也不需要写那些丑陋的
FutureBuilder或StreamBuilder了。贴心的错误处理
Provider 会自动帮你捕获函数里的错误,并把错误状态暴露给界面,方便你展示错误提示。
强大的模拟功能
在写测试或者其他场景下,你可以轻松地“模拟”任何一个 Provider 的返回值。
离线数据持久化 (实验性功能)
你可以把 Provider 的结果存到手机磁盘上,下次打开 App 时能自动加载,实现离线可用。
“变更”操作支持 (实验性功能)
比如提交表单这种操作,Provider 提供了一套内置方案,让你能方便地在界面上展示“加载中”或“提交失败”等状态。
Provider 家族一共有 6 位成员:
| 同步 | 异步 (Future) | 流 (Stream) | |
|---|---|---|---|
| 只读 | Provider | FutureProvider | StreamProvider |
| 可读可写 | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
这么多,是不是有点眼花缭乱?别急,我们来拆解一下。
同步 vs 异步 vs 流:
这三列其实就对应了 Dart 语言里三种最基本的函数类型。
// 返回一个普通的值
int synchronous() => 0;
// 异步返回一个值
Future<int> future() async => 0;
// 返回一个持续不断的值流
Stream<int> stream() => Stream.value(0);只读 vs 可读可写:
默认情况下,Provider 的值是“只读”的,界面不能直接修改它。而名字里带 “Notifier” 的变体,则允许从外部修改它的值。
这有点像一个变量是私有的还是公开的:
// “只读”的 Provider 就像这样,只能在内部修改
var _state = 0;
int get state => _state;对比一下:
// “可读可写”的 Provider 就像这样,谁都能改
var state = 0;小贴士
你也可以把“只读”和“可读可写”的 Provider,粗略地理解成 Flutter 里的
StatelessWidget和StatefulWidget。当然这个比喻不完全准确,因为 Provider 不是 Widget,而且它们都存着“状态”。但核心思想是类似的:“一个对象,状态不可变” vs “两个对象,状态可变”。
如何创建一个 Provider
我们应该把 Provider 定义在代码的“顶层”,也就是说,不要把它放在任何类或者函数里面。
创建 Provider 的语法,取决于你想创建的是“只读”的还是“可读可写”的,也就是上面表格里的分类。
这里我们以推荐的代码生成方式为例 创建“只读”的,用@riverpod注解函数
@riverpod
Result myFunction(Ref ref) {
// <你的逻辑代码放这里>
}创建可读可写的,用@riverpod注解类并拓展 _$类名,以便在你的内部写方法 中访问ref:
@riverpod
class MyNotifier extends _$MyNotifier {
@override
Result build() {
<your logic here>
}
<your methods here> //你的内部写方法
}| 元素 | 说明 |
|---|---|
| 注解 | 所有用代码生成的 Provider 都必须用 @riverpod 或 @Riverpod() 来标记。这个注解可以放在全局函数或类上。通过它,我们可以对 Provider 进行一些配置,比如 @Riverpod(keepAlive: true) 就可以让它不被自动销毁。 |
| 被注解的函数 | 函数的名字很重要,它决定了你将来怎么使用这个 Provider。比如函数名叫 myFunction,生成后你就可以用 myFunctionProvider 来访问它。这个函数 必须 把 Ref 作为它的第一个参数。除此之外,你可以加任意多的参数,也可以用泛型。函数体里可以写你的业务逻辑,比如网络请求。这个函数只会在 Provider 第一次被读取时执行一次,之后再读取就会直接返回缓存的结果。 |
| Ref | 这是一个用来和其他 Provider 互动的小助手。每个 Provider 都有一个 ref 对象,要么是作为函数的参数传入,要么是作为 Notifier 的一个属性。 |
小贴士
你可以随心所欲地创建任意多个 Provider。和
package:provider不同,Riverpod 允许你创建好几个类型相同的 Provider,完全没问题:dart@riverpod String city(Ref ref) => '伦敦'; @riverpod String country(Ref ref) => '英格兰';这两个 Provider 都返回
String类型,但它们不会互相冲突。
如何使用 Provider
Provider 自己本身什么也不做,就像 Widget 如果不被 runApp,也只是个普通的类一样。
你可以把 Provider 理解为对“状态”的一种描述,就像 Widget 是对“UI”的一种描述。
要真正使用 Provider,你需要一个叫做 ProviderContainer 的东西。简单来说,就是在你的 App 最外层套上一个 ProviderScope:
void main() {
runApp(
// 把你的 App 包裹起来
ProviderScope(
child: MyApp(),
),
);
}搞定这一步之后,你就可以通过一个叫 Ref 的小助手来和你的 Provider 们互动了。
简单来说,有两种方式可以拿到 Ref:
在 Provider 内部:
就像我们前面看到的,
ref会作为参数传给 Provider 函数,或者作为 Notifier 的一个属性。这让 Provider 之间可以互相通信。在 Widget 树中:
你需要使用一个特殊的 Widget,叫做
Consumer。它就像一座桥梁,连接了 Widget 世界和 Provider 世界,并递给你一个WidgetRef对象。
举个例子,假设我们有个 helloWorldProvider,它只返回一个字符串 "Hello world"。在 Widget 里可以这样用它:
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 使用 Consumer Widget
return Consumer(
builder: (context, ref, child) {
// 通过 ref.watch 来获取 provider 的值
final helloWorld = ref.watch(helloWorldProvider);
// 然后在 UI 中使用这个值
return Text(helloWorld);
},
);
}
}