前端转 Flutter 笔记 (Day 7):状态管理深度剖析 (Riverpod vs Pinia) 🧠
⏱️ 阅读时长:约 15 分钟
摘要:Day 7,今天暂停功能开发,专门做一次"技术复盘"。深入分析了项目中的 Riverpod 状态管理架构,并和熟悉的 Vue3 + Pinia 做了全方位对比。从父子传参到多层级共享,从
ref.watch到useXxxStore,一文打通两个框架的状态管理思维。
1. 为什么要专门学状态管理?🤔
写到 Day 6,项目里已经有了 3 种不同的 Provider 写法,我自己都有点晕了:
diary_provider.dart用的是@riverpod注解theme_provider.dart用的是StateNotifierProviderauth_view_model.dart用的是@riverpod+ 自定义 State 类
今天花时间梳理一下,搞清楚什么场景用什么写法。
2. Flutter 状态管理方案演进:InheritedWidget → Provider → Riverpod
在深入 Riverpod 之前,先了解一下 Flutter 状态管理的"前世今生":
方案对比表
| 特性 | InheritedWidget | Provider 包 | Riverpod |
|---|---|---|---|
| 定位 | Flutter 内置底层 API | 社区封装(官方推荐) | 新一代独立方案 |
| 依赖 Widget 树 | ✅ 必须在特定位置 | ✅ 必须在 Widget 树中 | ❌ 完全独立 |
| 类型安全 | ⚠️ 手动类型转换 | ⚠️ 运行时检查 | ✅ 编译时完整推断 |
| 多个相同类型 | ❌ 只能获取最近的一个 | ❌ 需要用 .family 区分 | ✅ 天然支持 |
| 代码量 | 多(需要手写很多模板代码) | 中 | 少(注解自动生成) |
| 测试友好度 | 低(需要构建 Widget 树) | 中 | 高(可完全独立测试) |
| 学习曲线 | 陡峭 | 平缓 | 中等 |
代码对比
InheritedWidget(最底层,不推荐直接使用):
// 1. 定义一个 InheritedWidget
class ThemeInherited extends InheritedWidget {
final ThemeMode themeMode;
const ThemeInherited({required this.themeMode, required super.child});
static ThemeInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeInherited>()!;
}
@override
bool updateShouldNotify(ThemeInherited oldWidget) => themeMode != oldWidget.themeMode;
}
// 2. 使用(必须在 Widget 树中查找)
final themeMode = ThemeInherited.of(context).themeMode;Provider 包(社区标准,Vue 玩家熟悉的 provide/inject 升级版):
// 1. 定义 ChangeNotifier
class ThemeNotifier extends ChangeNotifier {
ThemeMode _mode = ThemeMode.system;
ThemeMode get mode => _mode;
void setMode(ThemeMode mode) {
_mode = mode;
notifyListeners(); // 手动通知
}
}
// 2. 在 Widget 树中提供
ChangeNotifierProvider(
create: (_) => ThemeNotifier(),
child: MyApp(),
)
// 3. 消费
final themeMode = context.watch<ThemeNotifier>().mode;Riverpod(本项目使用,推荐):
// 1. 定义 Provider(不在 Widget 树中!)
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, AppThemeMode>((ref) {
return ThemeModeNotifier();
});
// 2. 只需在根部包一个 ProviderScope
ProviderScope(child: MyApp())
// 3. 任意位置消费(不需要 context!)
final themeMode = ref.watch(themeModeProvider);为什么选择 Riverpod?
- 脱离 Widget 树:Provider 定义在全局,不需要纠结"放在哪个位置"
- 编译时安全:拼错 Provider 名字会直接报错,而不是运行时崩溃
- 多个同类型 Provider:可以有多个返回
User类型的 Provider,用名字区分 - 更好的测试:不需要构建 Widget 树就能测试业务逻辑
- 代码生成:
@riverpod注解自动生成模板代码,减少手写量
💡 一句话总结:
InheritedWidget是原料,Provider是半成品,Riverpod是开箱即用的成品。
3. Riverpod vs Pinia:核心概念对照表
作为前端人,先来一个"速查表"快速建立映射关系:
| 概念 | Flutter / Riverpod | Vue3 / Pinia |
|---|---|---|
| 状态容器 | ProviderScope 包裹根组件 | createPinia() 挂载到 App |
| 定义状态 | @riverpod class Xxx extends _$Xxx | defineStore('xxx', {...}) |
| 读取状态 | ref.watch(xxxProvider) | const store = useXxxStore() |
| 只读一次 | ref.read(xxxProvider) | (Pinia 自动优化) |
| 调用方法 | ref.read(xxxProvider.notifier).method() | store.method() |
| 选择性监听 | ref.watch(xxx.select((s) => s.field)) | storeToRefs(store).field |
| 刷新数据 | ref.refresh(xxxProvider) | 手动调用 action |
| 派生状态 | 另一个 Provider watch 其他 Provider | Pinia getters |
| 异步状态 | 返回 AsyncValue<T> (loading/error/data) | 手动管理 loading/error |
4. 项目中的 3 种 Provider 写法详解
写法一:@riverpod 注解 — 异步数据
// 📁 lib/providers/diary_provider.dart
@riverpod
class DiaryList extends _$DiaryList {
@override
Future<List<DiaryEntry>> build() async {
// 👇 从 Supabase 获取数据
final data = await Supabase.instance.client
.from('diary_entries')
.select()
.order('created_at', ascending: false);
return (data as List).map((e) => DiaryEntry.fromJson(e)).toList();
}
Future<void> deleteEntry(String id) async {
await Supabase.instance.client.from('diary_entries').delete().eq('id', id);
ref.invalidateSelf(); // 👈 触发重新加载
}
}使用方式:
final diaryListAsync = ref.watch(diaryListProvider);
return diaryListAsync.when(
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (entries) => ListView(...),
);Vue3 对应:
export const useDiaryStore = defineStore('diary', {
state: () => ({ entries: [], loading: false, error: null }),
actions: {
async fetchEntries() {
this.loading = true;
try {
const { data } = await supabase.from('diary_entries').select();
this.entries = data;
} finally {
this.loading = false;
}
}
}
});关键差异:Riverpod 的
AsyncValue自动帮你管理了 loading/error 状态,不用手动声明!
写法二:StateNotifierProvider — 同步状态 + 持久化
// 📁 lib/providers/theme_provider.dart
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, AppThemeMode>((ref) {
return ThemeModeNotifier();
});
class ThemeModeNotifier extends StateNotifier<AppThemeMode> {
ThemeModeNotifier() : super(AppThemeMode.system) {
_loadThemeMode(); // 👈 初始化时从本地读取
}
Future<void> setThemeMode(AppThemeMode mode) async {
state = mode; // ⚠️ state 是关键字!赋值即触发 UI 更新
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme_mode', mode.index); // 👈 持久化
}
}⚠️ 关键概念:
state是 StateNotifier 的内置属性
- 它不是你自己定义的变量,而是父类
StateNotifier<T>提供的- 当你给
state赋新值时,Riverpod 会自动通知所有ref.watch()的组件重建- 类似 Vue 中
this.xxx = newValue会触发响应式更新
使用方式:
// 读取
final themeMode = ref.watch(themeModeProvider);
// 修改
ref.read(themeModeProvider.notifier).setThemeMode(AppThemeMode.dark);Vue3 对应:
export const useThemeStore = defineStore('theme', {
state: () => ({ mode: 'system' }),
actions: {
async setMode(mode) {
this.mode = mode;
localStorage.setItem('theme_mode', mode);
}
}
});适用场景:简单的同步状态,需要持久化到本地存储。
写法三:@riverpod + 自定义 State — 复杂业务
// 📁 lib/view_models/auth_view_model.dart
// 1. 定义复合状态
class AuthState {
final User? user;
final bool isLoading;
final String? errorMessage;
// ... copyWith 方法
}
// 2. 定义 ViewModel
@riverpod
class AuthViewModel extends _$AuthViewModel {
@override
AuthState build() {
// 监听 Supabase 认证变化
_supabase.auth.onAuthStateChange.listen((data) {
state = state.copyWith(user: data.session?.user); // ⚠️ state 赋值 → UI 更新
});
return AuthState(user: _supabase.auth.currentUser);
}
Future<bool> signInWithPassword({required String email, required String password}) async {
state = state.copyWith(isLoading: true, errorMessage: null); // ⚠️ state 赋值
try {
final response = await _supabase.auth.signInWithPassword(...);
state = state.copyWith(isLoading: false); // ⚠️ state 赋值
return response.user != null;
} on AuthException catch (e) {
state = state.copyWith(isLoading: false, errorMessage: e.message);
return false;
}
}
}⚠️
state在@riverpod中同样是关键内置属性
- 继承
_$AuthViewModel后,state属性类型自动变成AuthState- 每次
state = ...都会触发依赖该 Provider 的组件重建- 使用
state.copyWith()是更新复杂对象的惯用模式(类似 Redux 的不可变更新)
使用方式(精准订阅):
// 只监听登录状态,避免用户信息变化导致重建
final isLoggedIn = ref.watch(authViewModelProvider.select((s) => s.isLoggedIn));Vue3 对应:
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null, isLoading: false, errorMessage: null }),
getters: {
isLoggedIn: (state) => state.user !== null,
},
actions: {
async signIn(email, password) { /* ... */ }
}
});适用场景:多个相关状态需要一起管理(loading + error + data)。
5. 组件传参方式全对比
4.1 父传子 (Props)
| Flutter | Vue3 |
|---|---|
| 构造函数参数 | defineProps() |
Flutter 实现:
// 📁 lib/pages/home/widgets/diary_card.dart
class DiaryCard extends StatelessWidget {
final DiaryEntry entry; // ← 父组件传入
const DiaryCard({super.key, required this.entry});
}
// 父组件使用
DiaryCard(entry: dayEntries[index])Vue3 对应:
<script setup>
const props = defineProps<{ entry: DiaryEntry }>()
</script>
<!-- 父组件 -->
<DiaryCard :entry="entry" />4.2 子传父 (Emit / Callback)
| Flutter | Vue3 |
|---|---|
VoidCallback / Function(T) | defineEmits() |
Flutter 实现:
class MyButton extends StatelessWidget {
final VoidCallback onPressed;
final void Function(String) onTextChange;
const MyButton({required this.onPressed, required this.onTextChange});
}
// 父组件
MyButton(
onPressed: () => print('Clicked!'),
onTextChange: (text) => setState(() => _value = text),
)Vue3 对应:
<script setup>
const emit = defineEmits(['pressed', 'textChange'])
</script>
<!-- 父组件 -->
<MyButton @pressed="handlePress" @text-change="handleTextChange" />4.3 兄弟传参
| 方案 | Flutter | Vue3 |
|---|---|---|
| 状态提升 | 共同父组件管理 + 回调 | 共同父组件 + v-model |
| 全局状态 | Riverpod Provider | Pinia Store |
Flutter(Riverpod 方案):
// 任意兄弟组件中直接访问
final value = ref.watch(sharedValueProvider);
ref.read(sharedValueProvider.notifier).update(newValue);Vue3(Pinia 方案):
const store = useSharedStore();
console.log(store.value);
store.updateValue(newValue);4.4 多层级传参 (3+ 层嵌套)
| 方案 | Flutter | Vue3 |
|---|---|---|
| 依赖注入 | InheritedWidget (底层) | provide / inject |
| 全局状态 | Riverpod (推荐) | Pinia (推荐) |
Flutter(Riverpod):
// 任意深度子组件,无需逐层传递
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final user = ref.watch(authViewModelProvider.select((s) => s.user));
}Vue3(Pinia):
// 任意深度子组件
const themeStore = useThemeStore()4.5 路由传参
| 方式 | Flutter (GoRouter) | Vue Router |
|---|---|---|
| Path 参数 | /detail/:id | /detail/:id |
| Query 参数 | context.push('/page?key=value') | push({ query: {...} }) |
| 对象传参 | extra: myObject | state: myObject |
Flutter 实现(项目代码):
// 📁 app_router.dart - 路由定义
GoRoute(
path: '/detail/:id',
pageBuilder: (context, state) {
final id = state.pathParameters['id']!; // Path 参数
final entry = state.extra as DiaryEntry?; // 对象传参
return DiaryDetailPage(id: id, entry: entry);
},
),
// 📁 diary_card.dart - 跳转
context.pushNamed(
'detail',
pathParameters: {'id': entry.id},
extra: entry,
);Vue Router 对应:
router.push({
name: 'detail',
params: { id: entry.id },
state: { entry }
})6. 选型指南:什么场景用什么方式?
| 场景 | 推荐方案 |
|---|---|
| 简单父子组件 (1-2 层) | 构造函数传参 |
| 表单控件值同步 | 回调函数 (onChanged) |
| 兄弟组件共享 | Riverpod Provider |
| 深层嵌套 (3+ 层) | Riverpod Provider |
| 跨路由共享 | Riverpod Provider |
| 临时传递对象到新页面 | GoRouter extra |
| URL 可分享(刷新保留) | Path / Query 参数 |
7. Riverpod 三种写法选型
| 写法 | 适用场景 | 项目示例 |
|---|---|---|
@riverpod (异步) | 远程数据获取 | diary_provider.dart |
StateNotifierProvider | 简单本地状态 + 持久化 | theme_provider.dart |
@riverpod + State 类 | 复杂业务状态 (loading/error) | auth_view_model.dart |
8. 项目架构回顾
lib/
├── providers/ # Riverpod Providers
│ ├── diary_provider.dart # @riverpod 异步数据
│ └── theme_provider.dart # StateNotifierProvider
├── view_models/ # 复杂业务状态
│ └── auth_view_model.dart # @riverpod + AuthState
├── routers/
│ └── app_router.dart # GoRouter 路由传参
└── pages/
└── home/
└── widgets/
└── diary_card.dart # 父子传参示例对应 Vue 项目结构:
src/
├── stores/ # Pinia Stores
│ ├── diary.ts
│ ├── theme.ts
│ └── auth.ts
├── router/
│ └── index.ts
└── views/
└── home/
└── components/
└── DiaryCard.vueDay 7 总结
今天没写新功能,但收获更大:
- ✅ 理清了 3 种 Provider 写法:异步用
@riverpod,同步用StateNotifier,复杂业务用自定义 State - ✅ 掌握了 5 种传参方式:构造函数 / 回调 / Provider / 路由参数 / extra 对象
- ✅ 建立了 Vue ↔ Flutter 思维映射:
useStore()≈ref.watch(),emit≈Callback
核心心法
Flutter 没有"双向绑定",一切皆"单向数据流"。
- 数据向下传递 → 构造函数
- 事件向上传递 → 回调函数
- 跨组件共享 → Provider
明日预告: 搞清楚状态管理后,明天准备动手优化启动流程,研究一下 性能优化 和 包体积瘦身。
