Skip to content

前端转 Flutter 笔记 (Day 7):状态管理深度剖析 (Riverpod vs Pinia) 🧠

⏱️ 阅读时长:约 15 分钟

摘要:Day 7,今天暂停功能开发,专门做一次"技术复盘"。深入分析了项目中的 Riverpod 状态管理架构,并和熟悉的 Vue3 + Pinia 做了全方位对比。从父子传参到多层级共享,从 ref.watchuseXxxStore,一文打通两个框架的状态管理思维。

1. 为什么要专门学状态管理?🤔

写到 Day 6,项目里已经有了 3 种不同的 Provider 写法,我自己都有点晕了:

  • diary_provider.dart 用的是 @riverpod 注解
  • theme_provider.dart 用的是 StateNotifierProvider
  • auth_view_model.dart 用的是 @riverpod + 自定义 State 类

今天花时间梳理一下,搞清楚什么场景用什么写法

2. Flutter 状态管理方案演进:InheritedWidget → Provider → Riverpod

在深入 Riverpod 之前,先了解一下 Flutter 状态管理的"前世今生":

方案对比表

特性InheritedWidgetProvider 包Riverpod
定位Flutter 内置底层 API社区封装(官方推荐)新一代独立方案
依赖 Widget 树✅ 必须在特定位置✅ 必须在 Widget 树中❌ 完全独立
类型安全⚠️ 手动类型转换⚠️ 运行时检查✅ 编译时完整推断
多个相同类型❌ 只能获取最近的一个❌ 需要用 .family 区分✅ 天然支持
代码量多(需要手写很多模板代码)少(注解自动生成)
测试友好度低(需要构建 Widget 树)高(可完全独立测试)
学习曲线陡峭平缓中等

代码对比

InheritedWidget(最底层,不推荐直接使用):

dart
// 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 升级版):

dart
// 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(本项目使用,推荐):

dart
// 1. 定义 Provider(不在 Widget 树中!)
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, AppThemeMode>((ref) {
  return ThemeModeNotifier();
});

// 2. 只需在根部包一个 ProviderScope
ProviderScope(child: MyApp())

// 3. 任意位置消费(不需要 context!)
final themeMode = ref.watch(themeModeProvider);

为什么选择 Riverpod?

  1. 脱离 Widget 树:Provider 定义在全局,不需要纠结"放在哪个位置"
  2. 编译时安全:拼错 Provider 名字会直接报错,而不是运行时崩溃
  3. 多个同类型 Provider:可以有多个返回 User 类型的 Provider,用名字区分
  4. 更好的测试:不需要构建 Widget 树就能测试业务逻辑
  5. 代码生成@riverpod 注解自动生成模板代码,减少手写量

💡 一句话总结InheritedWidget 是原料,Provider 是半成品,Riverpod 是开箱即用的成品。


3. Riverpod vs Pinia:核心概念对照表

作为前端人,先来一个"速查表"快速建立映射关系:

概念Flutter / RiverpodVue3 / Pinia
状态容器ProviderScope 包裹根组件createPinia() 挂载到 App
定义状态@riverpod class Xxx extends _$XxxdefineStore('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 其他 ProviderPinia getters
异步状态返回 AsyncValue<T> (loading/error/data)手动管理 loading/error

4. 项目中的 3 种 Provider 写法详解

写法一:@riverpod 注解 — 异步数据

dart
// 📁 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(); // 👈 触发重新加载
  }
}

使用方式:

dart
final diaryListAsync = ref.watch(diaryListProvider);

return diaryListAsync.when(
  loading: () => CircularProgressIndicator(),
  error: (err, stack) => Text('Error: $err'),
  data: (entries) => ListView(...),
);

Vue3 对应:

js
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 — 同步状态 + 持久化

dart
// 📁 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 会触发响应式更新

使用方式:

dart
// 读取
final themeMode = ref.watch(themeModeProvider);

// 修改
ref.read(themeModeProvider.notifier).setThemeMode(AppThemeMode.dark);

Vue3 对应:

js
export const useThemeStore = defineStore('theme', {
  state: () => ({ mode: 'system' }),
  actions: {
    async setMode(mode) {
      this.mode = mode;
      localStorage.setItem('theme_mode', mode);
    }
  }
});

适用场景:简单的同步状态,需要持久化到本地存储。


写法三:@riverpod + 自定义 State — 复杂业务

dart
// 📁 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 的不可变更新)

使用方式(精准订阅):

dart
// 只监听登录状态,避免用户信息变化导致重建
final isLoggedIn = ref.watch(authViewModelProvider.select((s) => s.isLoggedIn));

Vue3 对应:

js
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)

FlutterVue3
构造函数参数defineProps()

Flutter 实现:

dart
// 📁 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 对应:

vue
<script setup>
const props = defineProps<{ entry: DiaryEntry }>()
</script>

<!-- 父组件 -->
<DiaryCard :entry="entry" />

4.2 子传父 (Emit / Callback)

FlutterVue3
VoidCallback / Function(T)defineEmits()

Flutter 实现:

dart
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 对应:

vue
<script setup>
const emit = defineEmits(['pressed', 'textChange'])
</script>

<!-- 父组件 -->
<MyButton @pressed="handlePress" @text-change="handleTextChange" />

4.3 兄弟传参

方案FlutterVue3
状态提升共同父组件管理 + 回调共同父组件 + v-model
全局状态Riverpod ProviderPinia Store

Flutter(Riverpod 方案):

dart
// 任意兄弟组件中直接访问
final value = ref.watch(sharedValueProvider);
ref.read(sharedValueProvider.notifier).update(newValue);

Vue3(Pinia 方案):

js
const store = useSharedStore();
console.log(store.value);
store.updateValue(newValue);

4.4 多层级传参 (3+ 层嵌套)

方案FlutterVue3
依赖注入InheritedWidget (底层)provide / inject
全局状态Riverpod (推荐)Pinia (推荐)

Flutter(Riverpod):

dart
// 任意深度子组件,无需逐层传递
Widget build(BuildContext context, WidgetRef ref) {
  final themeMode = ref.watch(themeModeProvider);
  final user = ref.watch(authViewModelProvider.select((s) => s.user));
}

Vue3(Pinia):

js
// 任意深度子组件
const themeStore = useThemeStore()

4.5 路由传参

方式Flutter (GoRouter)Vue Router
Path 参数/detail/:id/detail/:id
Query 参数context.push('/page?key=value')push({ query: {...} })
对象传参extra: myObjectstate: myObject

Flutter 实现(项目代码):

dart
// 📁 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 对应:

js
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.vue

Day 7 总结

今天没写新功能,但收获更大:

  1. 理清了 3 种 Provider 写法:异步用 @riverpod,同步用 StateNotifier,复杂业务用自定义 State
  2. 掌握了 5 种传参方式:构造函数 / 回调 / Provider / 路由参数 / extra 对象
  3. 建立了 Vue ↔ Flutter 思维映射useStore()ref.watch()emitCallback

核心心法

Flutter 没有"双向绑定",一切皆"单向数据流"。

  • 数据向下传递 → 构造函数
  • 事件向上传递 → 回调函数
  • 跨组件共享 → Provider

明日预告: 搞清楚状态管理后,明天准备动手优化启动流程,研究一下 性能优化包体积瘦身