Skip to content

08 · 状态管理 Riverpod

⚠️ 版本警告:本项目是 Riverpod 2.6.1,不是 3.x。 网上大量教程是 3.x 写法(如 Notifier 的初始化方式、Ref 泛型参数), 用 3.x 语法会直接编译失败。本文严格按 2.6.1 撰写。


开篇:前端概念对照

这是全系列最重要的一张表:

前端(React / Vue)Riverpod 2.x说明
useStateref.watch(provider)读状态,变化时重建
useEffect(() => fetch(), [])build() 方法体provider 首次被监听时执行
useEffect 返回的清理函数ref.onDispose(() => ...)
dispatch(action)ref.read(xProvider.notifier).method()
refetch / 手动重新请求ref.invalidateSelf()
useSelector / computedref.watch(p.select((s) => s.field))精准订阅
useEffect(() => {...}, [dep])ref.listen(p, (prev, next) => ...)状态变化触发副作用
Vuex / Pinia store@riverpod class / provider
Vuex getterref.watch(provider)
组件卸载清理provider 自动 dispose(无监听者时)

一、Riverpod 是什么,为什么用它

一句话

Riverpod 是 Provider 的重写版,由同一个作者开发。相比 Provider:

ProviderRiverpod
依赖 BuildContext✅ 是❌ 否
编译安全❌ 运行时可能报错✅ 编译期检查
多个同类型 provider❌ 麻烦✅ 天然支持
测试需要 mock context✅ 直接测

关键优势不依赖 BuildContext。所有 provider 通过 ref 访问,ref 可以在任何地方传递。

前端类比

约等于「不依赖组件树的 Pinia」——store 是独立的,任何地方都能访问,不需要 useStore() 挂到组件上。


二、三种 Provider 类型

本项目用到三类,按使用频率排序:

@riverpod class —— 可变状态的 Notifier(最常用)

dart
@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 函数 —— 只读的计算值 / 依赖注入

dart
@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

dart
@Riverpod(keepAlive: true)
I18nSyncService i18nSyncService(Ref ref) {
  return I18nSyncService(
    remoteSource: ref.watch(i18nRemoteSourceProvider),
    cache: const I18nCache(),
  );
}

@riverpod Future<T> 函数 —— 异步只读数据

dart
@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 classbuild() 返回 Future<T>

三、AsyncValue 三态(核心)

build() 返回 Future<T> 时,Riverpod 自动把状态包装成 AsyncValue<T>

dart
@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

dart
ref.watch(userTabViewModelProvider).when(
  loading: () => const CircularProgressIndicator(),
  error: (e, st) => Text('出错了:$e'),
  data: (data) => Text('${data?.name}'),
);

消费方式二:LoadingStateHandler本项目标准做法

dart
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

  1. 统一的骨架屏 / 空态 / 错误重试 UI
  2. 支持 skipLoadingOnRefresh(下拉刷新时不闪骨架屏,见下)
  3. 团队视觉统一

copyWithPrevious:刷新时不闪骨架屏

dart
state = AsyncValue<PagedState<T>>.loading()
    .copyWithPrevious(state, isRefresh: true);

含义:进入 loading 态,但保留上一次的数据。UI 判断 isRefresh 后继续展示旧列表,只显示刷新指示器。

项目实例 —— lib/ui/core/notifiers/paged_notifier_mixin.dart:87-92

dart
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 态

dart
state = await AsyncValue.guard(() => _fetchPage(_page));

等价于:

dart
try {
  state = AsyncValue.data(await _fetchPage(_page));
} catch (e, st) {
  state = AsyncValue.error(e, st);
}

这是本项目处理异步错误的标准方式——配合 Repository 抛出的异常(见 09 章)。

常用属性

dart
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订阅:变化时重建 Widgetbuild()useSelector
ref.read读取一次:不订阅回调、事件处理里store.getState()
ref.listen监听变化做副作用build()useEffect(() => {...}, [dep])

ref.watch:订阅(只在 build 里用)

dart
Widget build(BuildContext context, WidgetRef ref) {
  final state = ref.watch(myViewModelProvider);   // ✅ 状态变化时重建
  return Text('${state.count}');
}

⚠️ 不要在回调里用 watch

dart
onPressed: () {
  ref.watch(myProvider);      // ❌ 错误!会导致不可预期的重建
}

ref.read:读一次(在回调里用)

dart
onPressed: () {
  ref.read(myViewModelProvider.notifier).refresh();   // ✅
}

⚠️ 调用方法必须加 .notifier

dart
ref.read(myViewModelProvider).refresh();          // ❌ 报错(provider 本身没这方法)
ref.read(myViewModelProvider.notifier).refresh(); // ✅

这是新手第一高频错误

ref.listen:副作用

dart
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

dart
ref.listen<int>(homeShowTickProvider, (previous, next) {
  if (ref.read(detailClickProvider)) {
    ref.read(detailClickProvider.notifier).state = false;
  } else {
    _onPlantDataChanged();
  }
});

错误 toast 的标准写法

只在首次进入错误态时弹(避免每次重建都弹):

dart
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);
  }
});

本项目把它收口成 showAsyncErrorToastlib/ui/core/utils/api_error_message.dart),分页 body 通过 setupErrorListener 覆盖调用:

dart
// 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:精准订阅(性能优化)

问题

dart
// ❌ 只要任何字段变化就重建,即使我只用 totalCount
final state = ref.watch(myProvider);
final count = state.totalCount;

解法

dart
// ✅ 只在 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反面教材但合理的写法:

dart
final canAddPlant = ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');

这里 watch 了整个 authViewModelProvider,权限变化才影响 UI——但因为 AuthState 变化不频繁,可接受。

如果这是高频变化的状态,应该改成:

dart
final canAddPlant = ref.watch(
  authViewModelProvider.select((s) => s.hasPermission('app:b:addPlant')),
);

⚠️ select 的约束:返回值必须实现 ==(基本类型、freezed 类都满足)。返回新建的 List/Map 会导致每次都重建(失去优化意义)。


六、生命周期:keepAliveonDispose

默认行为:自动销毁

默认情况下,provider 在没有监听者时自动销毁。这是 Riverpod 的重要特性。

页面 A 监听 provider → provider 创建
页面 A 退出         → 无监听者 → provider 自动 dispose

好处:不用手动清理,天然避免内存泄漏。

副作用:返回页面时状态丢失,需要重新请求。

keepAlive: true:长期存活

dart
@Riverpod(keepAlive: true)
class ThemeModeNotifier extends _$ThemeModeNotifier { ... }

用途:全局共享的状态——主题、语言、鉴权、网络客户端等。

项目实例lib/config/providers/ 下大量使用):

dart
// 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

dart
@override
Future<MyHome?> build() async {
  _cancelToken?.cancel();
  _cancelToken = CancelToken();
  ref.onDispose(() => _cancelToken?.cancel());    // ✅ 页面销毁时取消请求
  ...
}

这是 CancelToken 的标准用法:页面退出时自动取消进行中的请求,避免「已销毁的 provider 还在回调」。

更多清理场景 —— lib/config/providers/network_providers.dart:174-178

dart
final unregister = bridge.registerOnLogout(client.disconnectAll);
ref.onDispose(() {
  unregister();
  client.disconnectAll();
});
return client;

lib/config/providers/network_providers.dart:206-209(Stream 清理):

dart
ref.onDispose(() {
  subscription.cancel();
  controller.close();
});

前端类比useEffect 返回的清理函数。

ref.invalidateSelf:主动重新执行 build

dart
Future<void> refresh() async => ref.invalidateSelf();

作用:让 provider 重新执行 build()(等于重新请求)。

前端类比:React Query 的 refetch()

相关

dart
ref.invalidate(provider);        // 让别的 provider 失效
ref.invalidateSelf();            // 让自己失效

自检

  • [ ] 知道默认 provider 无监听者时自动销毁
  • [ ] 知道全局状态用 keepAlive: true
  • [ ] 会在 build() 里用 ref.onDispose 注册清理(尤其 CancelToken)
  • [ ] 知道 ref.invalidateSelf() 等于重新请求

七、family:带参数的 provider

2.6.1 的写法

dart
@riverpod
Future<PlantDetail> plantDetail(Ref ref, {required int plantId}) async {
  final repo = ref.watch(plantRepositoryProvider);
  ...
}

使用

dart
final detail = ref.watch(plantDetailProvider(plantId: 42));

⚠️ 这是 2.x 语法。函数第一个参数是 Ref ref,后面跟命名参数。

项目实例

lib/ui/home/view_models/agent_home_view_model.dart:121-124

dart
@riverpod
Future<BStatistics> homeStats(Ref ref, {required String keyword}) async {
  final filter = ref.watch(plantFilterNotifierProvider);
  final repository = ref.watch(plantRepositoryProvider);
  ...
}

调用:

dart
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

dart
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);
}

使用方(简化示意)

dart
@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

下一步

09 · 数据层链路