cubegao

Riverpod 源码解析与设计思想

2024-05-19

一、从一个盲区开始

上一篇文章对比了 Provider、Bloc、Riverpod、GetX 之后,我们最终选择了 Bloc + Provider 混合方案。但在源码评估阶段,Riverpod 的设计给我留下了最深的印象。

它解决了 Provider 的几乎所有已知问题——BuildContext 强依赖、运行时类型查找的崩溃风险、Provider 不能相互组合的局限——同时又保持了 Provider 简洁的声明式风格。

本文不打算推荐你使用 Riverpod(上一篇文章已经说明了我们的选择),而是从源码层面剖析它的核心设计思想。理解 Riverpod 的设计,能帮助你更深刻地理解状态管理框架的本质权衡,不管最后选哪个方案。

二、核心设计问题:如何摆脱 BuildContext

Provider 最大的痛点是 BuildContext 依赖。context.read<T>() 的本质是在 Widget 树上查找最近的 InheritedWidget,这带来了两个无法回避的问题:

  1. 非 Widget 代码中拿不到 context,没法访问状态。
  2. 如果 Provider 不在当前 Widget 的祖先树上,运行时会抛 ProviderNotFoundException,而不是编译时错误。

Riverpod 是如何解决这个问题的?答案核心在于 ProviderContainerRef 的设计。

三、ProviderContainer:脱离 Widget 树的状态容器

3.1 架构总览

Riverpod 的核心模块分为三层:

1
2
3
4
5
6
7
┌──────────────────────────────┐
│ ProviderScope (Flutter) │ ← Widget 树的接入点
├──────────────────────────────┤
│ ProviderContainer (Core) │ ← 核心:独立于 Flutter 的状态存储与生命周期管理
├──────────────────────────────┤
│ Provider / Ref (Core) │ ← Provider 定义和依赖访问接口
└──────────────────────────────┘

ProviderContainer 是整个架构的核心。它是一个独立于 Widget 树的状态容器,内部维护了所有 Provider 的状态和它们的依赖关系图。因为它是纯 Dart 对象,不是 Widget,所以可以在任何地方使用——包括 WebSocket 回调、数据库操作回调、甚至命令行 Dart 脚本。

3.2 容器如何存储状态

ProviderContainer 内部用一个 Map 存储所有 Provider 的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化后的核心结构
class ProviderContainer {
// 存储每个 Provider 的状态
final Map<ProviderBase, _State> _states = {};

T read<T>(ProviderBase<T> provider) {
final state = _states[provider];
if (state == null) {
return _createAndStore(provider);
}
return state.value as T;
}
}

关键点在于 Map 的 key 是 ProviderBase 对象本身,而不是类型(Provider 用的是 Type 作为 key)。这带来了两个好处:

  1. 同一个类型可以存在多个独立的 Provider。在 Provider 中,Provider<Message> 只能有一个实例,因为它是通过类型查找的。Riverpod 中你可以创建 messageProvider1messageProvider2 两个独立的 Provider<Message>,它们是不同的对象实例,互不冲突。
  2. 编译时安全ProviderBase 是强类型的,不存在运行时类型转换失败的风险。

3.3 如何在 Widget 树中使用

虽然 ProviderContainer 本身不依赖 Widget 树,但 Flutter 端需要一个 Widget 来持有和传递它,这就是 ProviderScope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ProviderScope extends StatefulWidget {
final Widget child;

@override
State<ProviderScope> createState() => _ProviderScopeState();
}

class _ProviderScopeState extends State<ProviderScope> {
late final ProviderContainer _container = ProviderContainer();

@override
Widget build(BuildContext context) {
return _InheritedProviderScope(
container: _container,
child: widget.child,
);
}
}

ProviderScope 通过一个私有 InheritedWidget (_InheritedProviderScope) 将 ProviderContainer 注入 Widget 树。ConsumerWidgetbuild 方法接收一个 WidgetRef ref 参数——实际上 ref 就是一个持有 ProviderContainer 引用的对象,封装了对 container.read()container.listen() 的访问。

设计上的巧妙之处:InheritedWidget 承载的是 ProviderContainer 的引用,而不是状态本身。状态存储在 ProviderContainer 的 Map 中,与 Widget 树完全解耦。InheritedWidget 只是状态容器的「快递员」,不是「仓库」。

四、Ref:依赖管理与自动销毁

4.1 Ref 是什么

Ref 是 Riverpod 中最重要的抽象接口。它封装了对 ProviderContainer 的访问,同时对 Provider 的使用者隐藏了容器的实现细节:

1
2
3
4
5
6
7
8
9
10
abstract class Ref {
// 读取另一个 Provider 的当前值(不监听变化)
T read<T>(ProviderListenable<T> provider);

// 监听另一个 Provider 的变化(值变化时当前 Provider 也会重新计算)
T watch<T>(AlwaysAliveProviderListenable<T> provider);

// 注册销毁回调
void onDispose(void Function() listener);
}

Ref 的设计目的:

  1. 依赖追踪:通过 watch 方法,Riverpod 自动构建 Provider 之间的依赖图。当被依赖的 Provider 状态变化时,依赖它的 Provider 自动重新计算。
  2. 生命周期管理:通过 onDispose 方法,Provider 可以在被销毁时释放资源(关闭 WebSocket、取消 Timer 等)。
  3. 代码生成支持:Ref 是代码生成的入口。riverpod_generator 包可以在编译期分析 Provider 的依赖关系,生成优化后的代码。

4.2 watch 的实现:依赖追踪

watch 是 Riverpod 中最核心的方法,它的实现原理涉及依赖追踪:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化逻辑
class ProviderElement {
final Set<ProviderElement> _dependencies = {};
final Set<ProviderElement> _dependents = {};

T watch<T>(ProviderListenable<T> provider) {
final targetElement = container.readElement(provider);
// 建立双向依赖关系
_dependencies.add(targetElement);
targetElement._dependents.add(this);
return targetElement.value;
}
}

当一个 Provider 的 watch 了另一个 Provider,它们之间就会建立双向链接。当被依赖的 Provider 状态更新时,它会遍历 _dependents,通知所有依赖它的 Provider 重新计算。这个机制类似于电子表格中的公式依赖:改了 A1 单元格,所有引用 A1 的单元格自动重算。

4.3 autoDispose:自动资源管理

Riverpod 的 autoDispose 机制是其设计的另一个亮点:

1
2
3
4
5
6
7
final messageProvider = FutureProvider.autoDispose.family<Message, String>((ref, id) {
ref.onDispose(() {
// 当 Provider 不再被任何 Widget 监听时,自动调用
print('清理消息 $id 的资源');
});
return fetchMessage(id);
});

实现原理:每个 ProviderElement 维护一个引用计数。当 ConsumerWidget 通过 ref.watch 开始监听时,引用计数 +1;当 Widget 被 disposed 时,引用计数 -1。引用计数归零后,ProviderElement 被标记为待清理,在下一个微任务中执行 onDispose 回调并释放自身。

在企业 IM 的聊天页面场景中,这个特性特别有价值:进入聊天页面时创建 messageProvider,加载消息;退出聊天页面时,Provider 自动销毁,释放消息列表占用的内存。不需要手动管理生命周期。

五、Provider 组合:函数式依赖注入

5.1 组合 vs 继承

Riverpod 提倡通过组合而非继承来构建复杂的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 基础 Provider
final messagesProvider = FutureProvider<List<Message>>((ref) async {
return ref.read(messageRepositoryProvider).getMessages();
});

// 过滤后的 Provider(依赖 messagesProvider)
final filteredMessagesProvider = Provider<List<Message>>((ref) {
final messages = ref.watch(messagesProvider).value ?? [];
final keyword = ref.watch(searchKeywordProvider);
return messages.where((m) => m.content.contains(keyword)).toList();
});

// 未读计数 Provider(依赖 filteredMessagesProvider)
final unreadCountProvider = Provider<int>((ref) {
final filtered = ref.watch(filteredMessagesProvider);
return filtered.where((m) => !m.isRead).length;
});

messagesProvider 的数据变化时,filteredMessagesProvider 自动重新计算;filteredMessagesProvider 变化时,unreadCountProvider 自动重新计算。整个依赖链由 Riverpod 自动管理,开发者只需要声明「我依赖什么」。

这种设计在聊天列表的实时更新场景中非常高效:新消息到达 → messagesProvider 更新 → 过滤结果更新 → 未读计数更新,整个过程不需要手动通知,不会出现「这条消息在列表里但未读数没变」的数据不一致问题。

5.2 与 Bloc 的对比

Bloc 的多 Bloc 协作需要通过 BlocListener 手动桥接:

1
2
3
4
5
6
BlocListener<MessageBloc, MessageState>(
listener: (context, messageState) {
context.read<UnreadBloc>().add(UpdateUnread(messageState.newMessages));
},
child: ...,
);

这种方式的问题是:跨 Bloc 通信是隐式的,依赖关系散落在 Widget 层的 BlocListener 中,容易遗漏。Riverpod 的 Provider 组合将依赖关系显式声明在 Provider 定义中,不会遗漏,也更容易测试。

六、总结

Riverpod 的设计思想可以归纳为四个核心原则:

  1. 状态容器与 Widget 树解耦:ProviderContainer 是纯 Dart 对象,不依赖 Flutter 框架。这是解决 BuildContext 依赖问题的根本方案,也让状态管理可以脱离 UI 进行测试。

  2. 自动依赖追踪:通过 ref.watch 构建的依赖图,让状态变化自动传播,省去了手动通知的模板代码。这在复杂的数据流场景中大大降低了出错概率。

  3. 编译时安全:Provider 对象作为 Map 的 key 而非运行时类型查找,消除了 ProviderNotFoundException 这类运行时崩溃。

  4. 声明式资源管理:autoDispose 的引用计数机制,让开发者不需要手动管理 Provider 的生命周期,减少内存泄漏风险。

Riverpod 不是「更好的 Provider」,它是解决「依赖注入 + 状态管理 + 生命周期管理」三类问题的一体化方案。理解它的设计,能帮你更清晰地看到各种状态管理方案在架构层面的本质差异。

扫描二维码,分享此文章