08 · 状态管理 Riverpod
⚠️ 版本警告:本项目是 Riverpod 2.6.1,不是 3.x。 网上大量教程是 3.x 写法(如
Notifier的初始化方式、Ref泛型参数), 用 3.x 语法会直接编译失败。本文严格按 2.6.1 撰写。
开篇:前端概念对照
这是全系列最重要的一张表:
| 前端(React / Vue) | Riverpod 2.x | 说明 |
|---|---|---|
useState | ref.watch(provider) | 读状态,变化时重建 |
useEffect(() => fetch(), []) | build() 方法体 | provider 首次被监听时执行 |
useEffect 返回的清理函数 | ref.onDispose(() => ...) | |
dispatch(action) | ref.read(xProvider.notifier).method() | |
refetch / 手动重新请求 | ref.invalidateSelf() | |
useSelector / computed | ref.watch(p.select((s) => s.field)) | 精准订阅 |
useEffect(() => {...}, [dep]) | ref.listen(p, (prev, next) => ...) | 状态变化触发副作用 |
| Vuex / Pinia store | @riverpod class / provider | |
| Vuex getter | ref.watch(provider) | |
| 组件卸载清理 | provider 自动 dispose(无监听者时) |
一、Riverpod 是什么,为什么用它
一句话
Riverpod 是 Provider 的重写版,由同一个作者开发。相比 Provider:
| Provider | Riverpod | |
|---|---|---|
依赖 BuildContext | ✅ 是 | ❌ 否 |
| 编译安全 | ❌ 运行时可能报错 | ✅ 编译期检查 |
| 多个同类型 provider | ❌ 麻烦 | ✅ 天然支持 |
| 测试 | 需要 mock context | ✅ 直接测 |
关键优势:不依赖 BuildContext。所有 provider 通过 ref 访问,ref 可以在任何地方传递。
前端类比
约等于「不依赖组件树的 Pinia」——store 是独立的,任何地方都能访问,不需要 useStore() 挂到组件上。
二、三种 Provider 类型
本项目用到三类,按使用频率排序:
① @riverpod class —— 可变状态的 Notifier(最常用)
@riverpod
class DeviceListViewModel extends _$DeviceListViewModel with DeviceManageMixin {
@override
DeviceListUiState build() {
// 初始化,返回初始状态
return const DeviceListUiState();
}
Future<void> refresh() async {
// 修改状态:直接赋值给 state
state = state.copyWith(loading: true);
...
}
}要点:
- 继承
_$XxxViewModel(由代码生成) build()返回初始状态- 方法里通过
state = ...修改 - 页面用
ref.watch(xxxProvider)读、ref.read(xxxProvider.notifier).method()调方法
② @riverpod 函数 —— 只读的计算值 / 依赖注入
@Riverpod(keepAlive: true)
ApiClient apiClient(Ref ref) {
final url = ref.watch(baseUrlProvider);
final interceptor = ref.watch(authInterceptorProvider);
return ApiClient(authInterceptor: interceptor, ...);
}用途:依赖注入(DI)、派生值。
前端类比:Vuex getter 或 DI 容器的 factory。
项目实例 —— lib/config/providers/i18n_providers.dart:37-41:
@Riverpod(keepAlive: true)
I18nSyncService i18nSyncService(Ref ref) {
return I18nSyncService(
remoteSource: ref.watch(i18nRemoteSourceProvider),
cache: const I18nCache(),
);
}③ @riverpod Future<T> 函数 —— 异步只读数据
@riverpod
Future<BStatistics> homeStats(Ref ref, {required String keyword}) async {
final filter = ref.watch(plantFilterNotifierProvider);
final repository = ref.watch(plantRepositoryProvider);
final result = await repository.bStatistics(...);
return switch (result) {
Ok(value: final v) => v,
ErrorResult(error: final e) => throw e,
};
}项目实例 —— lib/ui/home/view_models/agent_home_view_model.dart:121-124。
⚠️ 注意这是 2.x 的 family 写法:Future<T> name(Ref ref, {required String keyword})。 3.x 的写法不同,不要混淆。
选型速查
| 需求 | 用 |
|---|---|
| 有可变状态 + 有方法 | @riverpod class |
| 依赖注入 / 派生只读值 | @riverpod 函数 |
| 异步只读数据(带 family 参数) | @riverpod Future<T> 函数 |
| 异步状态 + 有方法 | @riverpod class 的 build() 返回 Future<T> |
三、AsyncValue 三态(核心)
当 build() 返回 Future<T> 时,Riverpod 自动把状态包装成 AsyncValue<T>:
@riverpod
class UserTabViewModel extends _$UserTabViewModel {
@override
Future<MyHome?> build() async { ... }
}页面读到的是 AsyncValue<MyHome?>,天然三态:
| 状态 | 含义 | 前端对应 |
|---|---|---|
AsyncValue.loading() | 加载中 | if (loading) |
AsyncValue.data(value) | 成功 | if (data) |
AsyncValue.error(e, st) | 失败 | if (error) |
消费方式一:when
ref.watch(userTabViewModelProvider).when(
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('出错了:$e'),
data: (data) => Text('${data?.name}'),
);消费方式二:LoadingStateHandler(本项目标准做法)
LoadingStateHandler<MyData>(
state: ref.watch(myViewModelProvider),
loadingBuilder: (context) => const HeListSkeleton(),
successBuilder: (data) => _buildContent(data),
onErrorRetry: () => ref.read(myViewModelProvider.notifier).refresh(),
)定义于 lib/ui/core/widgets/loading_state_handler.dart。
为什么用它而不是 when:
- 统一的骨架屏 / 空态 / 错误重试 UI
- 支持
skipLoadingOnRefresh(下拉刷新时不闪骨架屏,见下) - 团队视觉统一
copyWithPrevious:刷新时不闪骨架屏
state = AsyncValue<PagedState<T>>.loading()
.copyWithPrevious(state, isRefresh: true);含义:进入 loading 态,但保留上一次的数据。UI 判断 isRefresh 后继续展示旧列表,只显示刷新指示器。
项目实例 —— lib/ui/core/notifiers/paged_notifier_mixin.dart:87-92:
Future<void> refresh() async {
_page = PageDefaults.defaultPage;
state = AsyncValue<PagedState<TOrder>>.loading()
.copyWithPrevious(state, isRefresh: true);
state = await AsyncValue.guard(() => _fetchPage(_page));
}前端类比:SWR / React Query 的 keepPreviousData。
AsyncValue.guard:把异常转成 error 态
state = await AsyncValue.guard(() => _fetchPage(_page));等价于:
try {
state = AsyncValue.data(await _fetchPage(_page));
} catch (e, st) {
state = AsyncValue.error(e, st);
}这是本项目处理异步错误的标准方式——配合 Repository 抛出的异常(见 09 章)。
常用属性
final state = ref.watch(myProvider);
state.isLoading; // 是否加载中
state.isRefreshing; // 是否刷新中(保留旧数据)
state.hasError; // 是否有错误
state.hasValue; // 是否有值
state.value; // 取值(有错误时抛异常)
state.valueOrNull; // 安全取值(推荐)
state.error; // 错误对象⚠️ 优先用 valueOrNull 而不是 value —— 后者在 error 态会抛异常。
自检
- [ ] 知道
AsyncValue有 loading / data / error 三态 - [ ] 会用
AsyncValue.guard包装异步 - [ ] 知道
copyWithPrevious(state, isRefresh: true)用于刷新不闪屏 - [ ] 优先用
valueOrNull而非value
四、ref 的三个方法:watch / read / listen
这是 Riverpod 最容易混淆的地方。
| 方法 | 用途 | 放在哪 | 前端类比 |
|---|---|---|---|
ref.watch | 订阅:变化时重建 Widget | build() 里 | useSelector |
ref.read | 读取一次:不订阅 | 回调、事件处理里 | store.getState() |
ref.listen | 监听变化做副作用 | build() 里 | useEffect(() => {...}, [dep]) |
ref.watch:订阅(只在 build 里用)
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(myViewModelProvider); // ✅ 状态变化时重建
return Text('${state.count}');
}⚠️ 不要在回调里用 watch:
onPressed: () {
ref.watch(myProvider); // ❌ 错误!会导致不可预期的重建
}ref.read:读一次(在回调里用)
onPressed: () {
ref.read(myViewModelProvider.notifier).refresh(); // ✅
}⚠️ 调用方法必须加 .notifier:
ref.read(myViewModelProvider).refresh(); // ❌ 报错(provider 本身没这方法)
ref.read(myViewModelProvider.notifier).refresh(); // ✅这是新手第一高频错误。
ref.listen:副作用
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<MyState>(myViewModelProvider, (previous, next) {
if (next.hasError && !next.isLoading) {
HeLoading.showError(msg); // 弹 toast
}
});
...
}用途:弹 toast、导航、触发一次性动作。不用于渲染。
项目实例 —— lib/ui/home/views/agent_home_page.dart:69-75:
ref.listen<int>(homeShowTickProvider, (previous, next) {
if (ref.read(detailClickProvider)) {
ref.read(detailClickProvider.notifier).state = false;
} else {
_onPlantDataChanged();
}
});错误 toast 的标准写法
只在首次进入错误态时弹(避免每次重建都弹):
ref.listen<AsyncValue<MyData>>(myViewModelProvider, (previous, next) {
if (next.hasError && !next.isLoading &&
(previous == null || previous.isLoading || !previous.hasError)) {
final msg = resolveApiErrorMessage(next.error, l10n);
if (msg != null) HeLoading.showError(msg);
}
});本项目把它收口成 showAsyncErrorToast(lib/ui/core/utils/api_error_message.dart),分页 body 通过 setupErrorListener 覆盖调用:
// lib/ui/core/widgets/paged_list_body.dart:59-63
void setupErrorListener(WidgetRef ref, RuntimeI18n l10n) {}自检
- [ ]
build里用watch,回调里用read - [ ] 调用 Notifier 方法必须
.notifier - [ ]
listen只用于副作用(toast / 导航) - [ ] 知道错误 toast 要判断「首次进入错误态」
五、select:精准订阅(性能优化)
问题
// ❌ 只要任何字段变化就重建,即使我只用 totalCount
final state = ref.watch(myProvider);
final count = state.totalCount;解法
// ✅ 只在 totalCount 变化时重建
final count = ref.watch(myProvider.select((s) => s.totalCount));前端类比:Vuex 的 mapState + 精准 getter,或 Redux useSelector(s => s.field)。
项目实例
lib/ui/home/views/agent_home_page.dart:60 是反面教材但合理的写法:
final canAddPlant = ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');这里 watch 了整个 authViewModelProvider,权限变化才影响 UI——但因为 AuthState 变化不频繁,可接受。
如果这是高频变化的状态,应该改成:
final canAddPlant = ref.watch(
authViewModelProvider.select((s) => s.hasPermission('app:b:addPlant')),
);⚠️ select 的约束:返回值必须实现 ==(基本类型、freezed 类都满足)。返回新建的 List/Map 会导致每次都重建(失去优化意义)。
六、生命周期:keepAlive 与 onDispose
默认行为:自动销毁
默认情况下,provider 在没有监听者时自动销毁。这是 Riverpod 的重要特性。
页面 A 监听 provider → provider 创建
页面 A 退出 → 无监听者 → provider 自动 dispose好处:不用手动清理,天然避免内存泄漏。
副作用:返回页面时状态丢失,需要重新请求。
keepAlive: true:长期存活
@Riverpod(keepAlive: true)
class ThemeModeNotifier extends _$ThemeModeNotifier { ... }用途:全局共享的状态——主题、语言、鉴权、网络客户端等。
项目实例(lib/config/providers/ 下大量使用):
// lib/config/providers/theme_mode_provider.dart:35-36
@Riverpod(keepAlive: true)
class ThemeModeNotifier extends _$ThemeModeNotifier { ... }
// lib/config/providers/locale_provider.dart:90-91
@Riverpod(keepAlive: true)
class LocaleNotifier extends _$LocaleNotifier { ... }
// lib/config/providers/settings_providers.dart:19-20
@Riverpod(keepAlive: true)
class UseMockApi extends _$UseMockApi { ... }
// lib/config/providers/network_providers.dart:135-136
@Riverpod(keepAlive: true)
ApiClient apiClient(Ref ref) { ... }判断标准:
| 场景 | keepAlive |
|---|---|
| 页面专属数据(列表、详情) | ❌ 默认(自动销毁) |
| 全局设置(主题、语言) | ✅ true |
| 全局单例(ApiClient、SSE) | ✅ true |
| 鉴权状态 | ✅ true |
ref.onDispose:清理副作用
项目实例 —— lib/ui/user/view_models/user_tab_view_model.dart:26-29:
@override
Future<MyHome?> build() async {
_cancelToken?.cancel();
_cancelToken = CancelToken();
ref.onDispose(() => _cancelToken?.cancel()); // ✅ 页面销毁时取消请求
...
}这是 CancelToken 的标准用法:页面退出时自动取消进行中的请求,避免「已销毁的 provider 还在回调」。
更多清理场景 —— lib/config/providers/network_providers.dart:174-178:
final unregister = bridge.registerOnLogout(client.disconnectAll);
ref.onDispose(() {
unregister();
client.disconnectAll();
});
return client;lib/config/providers/network_providers.dart:206-209(Stream 清理):
ref.onDispose(() {
subscription.cancel();
controller.close();
});前端类比:useEffect 返回的清理函数。
ref.invalidateSelf:主动重新执行 build
Future<void> refresh() async => ref.invalidateSelf();作用:让 provider 重新执行 build()(等于重新请求)。
前端类比:React Query 的 refetch()。
相关:
ref.invalidate(provider); // 让别的 provider 失效
ref.invalidateSelf(); // 让自己失效自检
- [ ] 知道默认 provider 无监听者时自动销毁
- [ ] 知道全局状态用
keepAlive: true - [ ] 会在
build()里用ref.onDispose注册清理(尤其 CancelToken) - [ ] 知道
ref.invalidateSelf()等于重新请求
七、family:带参数的 provider
2.6.1 的写法
@riverpod
Future<PlantDetail> plantDetail(Ref ref, {required int plantId}) async {
final repo = ref.watch(plantRepositoryProvider);
...
}使用:
final detail = ref.watch(plantDetailProvider(plantId: 42));⚠️ 这是 2.x 语法。函数第一个参数是 Ref ref,后面跟命名参数。
项目实例
lib/ui/home/view_models/agent_home_view_model.dart:121-124:
@riverpod
Future<BStatistics> homeStats(Ref ref, {required String keyword}) async {
final filter = ref.watch(plantFilterNotifierProvider);
final repository = ref.watch(plantRepositoryProvider);
...
}调用:
ref.watch(homeStatsProvider(keyword: _keyword));family 的 key 相等性
homeStatsProvider(keyword: 'a') 和 homeStatsProvider(keyword: 'a') 是同一个 provider(参数相等)。
参数不同的话,各自独立缓存。
⚠️ 参数必须能正确比较相等(基本类型、freezed 类 OK;普通 class 要自己实现 ==)。
八、完整案例:PagedNotifierMixin + ViewModel
把前面的知识串起来,看一个真实的完整实现。
Mixin 定义(lib/ui/core/notifiers/paged_notifier_mixin.dart:28-51)
mixin PagedNotifierMixin<TOrder, TDto>
on AutoDisposeAsyncNotifier<PagedState<TOrder>> {
CancelToken? _cancelToken;
int _page = PageDefaults.defaultPage;
int _totalPages = 0;
bool get hasMore => _page < _totalPages;
/// 子类提供:拉取一页 DTO
Future<Result<PageDTO<TDto>>> fetchPage(
int page, {String? keyword, String? status, CancelToken? cancelToken});
/// 子类提供:DTO → UI Model
TOrder mapDto(TDto dto);
}使用方(简化示意)
@riverpod
class PlantSelectListViewModel extends _$PlantSelectListViewModel
with PagedNotifierMixin<PlantDto, PlantDto> {
@override
Future<PagedState<PlantDto>> build() => initialLoad();
@override
Future<Result<PageDTO<PlantDto>>> fetchPage(
int page, {String? keyword, String? status, CancelToken? cancelToken}) async {
return ref.read(plantRepositoryProvider).getPlantPage(
PlantPageRequest(pageNum: page, pageSize: PageDefaults.defaultSize, keyword: keyword),
cancelToken: cancelToken,
);
}
@override
PlantDto mapDto(PlantDto dto) => dto;
}只需实现两个方法,分页的搜索/刷新/加载更多/判终全部由 mixin 提供。
数据流总结
页面 PagedListBody
├─ watchState(ref) → ref.watch(plantSelectListViewModelProvider)
├─ refresh(ref) → ref.read(...notifier).refresh()
└─ loadMore(ref) → ref.read(...notifier).loadMore()详见 12-分页与列表。
九、本章速查
定义
| 需求 | 写法 |
|---|---|
| 可变状态 Notifier | @riverpod class X extends _$X { S build() => ...; } |
| 依赖注入 / 派生值 | @riverpod T x(Ref ref) => ... |
| 异步只读(带参) | @riverpod Future<T> x(Ref ref, {required A a}) async |
| 长期存活 | @Riverpod(keepAlive: true) |
使用
| 需求 | 写法 |
|---|---|
| 订阅(build 里) | ref.watch(provider) |
| 精准订阅 | ref.watch(p.select((s) => s.field)) |
| 读一次(回调里) | ref.read(provider) |
| 调 Notifier 方法 | ref.read(provider.notifier).method() |
| 监听变化做副作用 | ref.listen(p, (prev, next) => ...) |
| 清理 | ref.onDispose(() => ...) |
| 重新请求 | ref.invalidateSelf() |
| 让其他 provider 失效 | ref.invalidate(otherProvider) |
AsyncValue
| 需求 | 写法 |
|---|---|
| 三态渲染 | .when(loading:, error:, data:) |
| 项目标准三态 | LoadingStateHandler<T> |
| 包装异步异常 | AsyncValue.guard(() => future) |
| 刷新保留旧数据 | .copyWithPrevious(state, isRefresh: true) |
| 安全取值 | state.valueOrNull |
| 判断是否刷新中 | state.isRefreshing |