Riverpod 实战指南

Riverpod 实战指南

本文不堆概念,用 9 个递进的完整场景 把 Riverpod 讲透。

每个场景给出 完整可运行代码 + 运行效果 + 踩坑点

建议边读边敲,不要只看。

基于 flutter_riverpod: ^2.6.x(Riverpod 2),文末附 Riverpod 3 迁移要点。


准备工作

yaml 复制代码
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1
  http: ^1.2.0
  freezed_annotation: ^2.4.1

dev_dependencies:
  riverpod_generator: ^2.6.2
  build_runner: ^2.4.0
  freezed: ^2.5.2
  json_serializable: ^6.7.1
  flutter_test:
    sdk: flutter
bash 复制代码
# 生成代码(一直跑着)
dart run build_runner watch -d

入口固定写法(后面每个场景都假设已有这段):

dart 复制代码
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Demo',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
      home: const HomePage(), // 替换成各场景页面
    );
  }
}

场景 1:主题切换 ------ 最小的「跨组件共享状态」

需求:页面顶部一个开关切换深色/浅色模式,底部文字实时响应。

完整代码

dart 复制代码
// theme_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateProvider 适合"一个布尔值、一个枚举"级别的极简状态
final isDarkModeProvider = StateProvider<bool>((ref) => false);
dart 复制代码
// theme_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'theme_provider.dart';

class ThemePage extends ConsumerWidget {
  const ThemePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ① watch:build 里订阅,值变 → 自动重建
    // 🔄 刷新范围:isDark 变化 → 整个 ThemePage.build() 重跑
    //    → Scaffold、Switch、Text 全部重建(因为 watch 写在最顶层)
    final isDark = ref.watch(isDarkModeProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('场景1:主题切换')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Switch(
              value: isDark,
              onChanged: (v) {
                // ② read:事件回调里只"读一次"并修改,不建立订阅
                ref.read(isDarkModeProvider.notifier).state = v;
              },
            ),
            const SizedBox(height: 20),
            Text(
              isDark ? '当前是深色模式 🌙' : '当前是浅色模式 ☀️',
              style: const TextStyle(fontSize: 24),
            ),
          ],
        ),
      ),
    );
  }
}

学到什么

要点 说明
ref.watch 写在 build 里,状态变了界面自动刷新
ref.read 写在 onChanged / onPressed 里,只读一次去改值
StateProvider 一个值、没有业务方法时够用

反面教材

dart 复制代码
// ❌ 在 build 里用 read → 界面永远不会因为 isDarkMode 变化而刷新
final isDark = ref.read(isDarkModeProvider);

场景 2:待办清单 ------ 有业务方法的状态用 Notifier

需求:添加待办、标记完成、删除。比一个布尔值复杂,需要方法。

完整代码

dart 复制代码
// todo_model.dart
class Todo {
  final String id;
  final String title;
  final bool completed;

  Todo({required this.id, required this.title, this.completed = false});

  Todo copyWith({String? title, bool? completed}) {
    return Todo(
      id: id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }
}
dart 复制代码
// todo_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_model.dart';

class TodoListNotifier extends Notifier<List<Todo>> {
  @override
  List<Todo> build() => []; // 初始状态:空列表

  void add(String title) {
    // 不可变更新:创建新 list
    state = [
      ...state,
      Todo(id: DateTime.now().millisecondsSinceEpoch.toString(), title: title),
    ];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(completed: !todo.completed) else todo,
    ];
  }

  void remove(String id) {
    state = state.where((t) => t.id != id).toList();
  }
}

final todoListProvider =
    NotifierProvider<TodoListNotifier, List<Todo>>(TodoListNotifier.new);

// 派生 Provider:已完成数量(只读、自动跟随 todoList 变化)
final completedCountProvider = Provider<int>((ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => t.completed).length;
});
dart 复制代码
// todo_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_notifier.dart';

class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:todoListProvider 或 completedCountProvider 任一变化
    //    → 整个 TodoPage.build() 重跑
    //    → AppBar(计数文字) + ListView(所有 ListTile) 全部重建
    //    → FloatingActionButton 也重建(但 const Icon 会被 Flutter 复用)
    final todos = ref.watch(todoListProvider);
    final doneCount = ref.watch(completedCountProvider);

    return Scaffold(
      appBar: AppBar(title: Text('待办 (已完成 $doneCount/${todos.length})')),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (_, i) {
          final todo = todos[i];
          return ListTile(
            leading: Checkbox(
              value: todo.completed,
              onChanged: (_) =>
                  ref.read(todoListProvider.notifier).toggle(todo.id),
            ),
            title: Text(
              todo.title,
              style: TextStyle(
                decoration:
                    todo.completed ? TextDecoration.lineThrough : null,
              ),
            ),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () =>
                  ref.read(todoListProvider.notifier).remove(todo.id),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context, WidgetRef ref) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('添加待办'),
        content: TextField(controller: controller, autofocus: true),
        actions: [
          TextButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                ref.read(todoListProvider.notifier).add(controller.text);
              }
              Navigator.pop(context);
            },
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }
}

学到什么

要点 说明
Notifier 状态有"动作"(add/toggle/remove)时用它,比 StateProvider 清晰
state = ... 必须整体赋新值(不可变更新),Riverpod 才能检测到变化
派生 Provider completedCountProvider 自动跟随 todoListProvider,不需要手动同步
.notifier 在回调里 ref.read(xxx.notifier).method() 调用业务方法

踩坑点

dart 复制代码
// ❌ 直接 mutate list → Riverpod 检测不到变化,界面不刷新
void add(String title) {
  state.add(Todo(...)); // 错!引用没变
}

// ✅ 赋新 list
void add(String title) {
  state = [...state, Todo(...)];
}

场景 3:网络请求 ------ FutureProvider + Loading/Error/Data 三态

需求:从网络拉取用户列表,显示加载中 → 数据 → 出错三种状态。

完整代码

dart 复制代码
// user_model.dart
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String,
      );
}
dart 复制代码
// user_repository.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'user_model.dart';

class UserRepository {
  Future<List<User>> fetchUsers() async {
    final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/users'),
    );
    if (response.statusCode != 200) {
      throw Exception('请求失败: ${response.statusCode}');
    }
    final list = jsonDecode(response.body) as List;
    return list.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
  }
}
dart 复制代码
// user_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'user_repository.dart';
import 'user_model.dart';

// 依赖注入:Repository 本身也是 Provider,方便测试时替换
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepository();
});

// FutureProvider:声明"这个数据怎么来",框架管 loading/error/data
final userListProvider = FutureProvider<List<User>>((ref) async {
  final repo = ref.watch(userRepositoryProvider);
  return repo.fetchUsers();
});
dart 复制代码
// user_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'user_providers.dart';

class UserListPage extends ConsumerWidget {
  const UserListPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:userListProvider 状态切换(loading → data → error)
    //    → 整个 UserListPage.build() 重跑
    //    → body 在 CircularProgressIndicator / ListView / 错误提示 之间切换
    //    → AppBar 不受数据影响(标题是 const),但仍在 build 树内会被重建
    final usersAsync = ref.watch(userListProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('场景3:网络请求'),
        actions: [
          // 下拉刷新:invalidate 让 Provider 重新执行
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => ref.invalidate(userListProvider),
          ),
        ],
      ),
      // .when 一次性处理三种状态,编译器保证你不遗漏
      body: usersAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('出错了: $err'),
              const SizedBox(height: 8),
              ElevatedButton(
                onPressed: () => ref.invalidate(userListProvider),
                child: const Text('重试'),
              ),
            ],
          ),
        ),
        data: (users) => ListView.builder(
          itemCount: users.length,
          itemBuilder: (_, i) => ListTile(
            leading: CircleAvatar(child: Text('${users[i].id}')),
            title: Text(users[i].name),
            subtitle: Text(users[i].email),
          ),
        ),
      ),
    );
  }
}

学到什么

要点 说明
FutureProvider 声明"数据怎么来",自动管理 loading/error/data 生命周期
AsyncValue.when 三态分支,编译器强制你每个都处理,不会漏
ref.invalidate 让缓存失效,Provider 重新执行(用于刷新/重试)
Repository 注入 userRepositoryProvider 让测试时可以 override 成假实现

踩坑点

dart 复制代码
// ❌ 只判断 data,忘了 loading 和 error → 白屏
body: Text(usersAsync.value?.first.name ?? ''),

// ✅ 用 .when 或 .maybeWhen 显式处理每种状态

场景 4:详情页 ------ family 按参数缓存 + autoDispose 自动释放

需求:列表点击进入用户详情页,每个用户 ID 对应独立缓存;离开页面自动释放。

完整代码

dart 复制代码
// user_detail_provider.dart
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'user_model.dart';

// .family:按 userId 参数化,每个 id 一份独立缓存
// .autoDispose:没人看这个详情页时,自动释放缓存
final userDetailProvider =
    FutureProvider.autoDispose.family<User, int>((ref, userId) async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/$userId'),
  );
  if (response.statusCode != 200) throw Exception('请求失败');
  return User.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
});
dart 复制代码
// user_detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'user_detail_provider.dart';

class UserDetailPage extends ConsumerWidget {
  final int userId;
  const UserDetailPage({super.key, required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:该 userId 对应的数据状态变化(loading → data)
    //    → 整个 UserDetailPage.build() 重跑
    //    → body 从 CircularProgressIndicator 切换到用户详情
    //    → 其他 userId 的 Provider 变化不影响这个页面
    final detailAsync = ref.watch(userDetailProvider(userId));

    return Scaffold(
      appBar: AppBar(title: Text('用户 #$userId')),
      body: detailAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, _) => Center(child: Text('$e')),
        data: (user) => Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(user.name, style: const TextStyle(fontSize: 28)),
              const SizedBox(height: 8),
              Text(user.email, style: const TextStyle(fontSize: 18)),
              const SizedBox(height: 24),
              const Text(
                '💡 返回列表后,这个 Provider 会自动释放\n'
                '   再次进入会重新请求',
              ),
            ],
          ),
        ),
      ),
    );
  }
}

从场景 3 的列表页跳转:

dart 复制代码
// 在 user_list_page.dart 的 ListTile 加 onTap
onTap: () => Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => UserDetailPage(userId: users[i].id),
  ),
),

学到什么

要点 说明
.family 一个 Provider 定义 → 按参数生成 N 个独立实例
.autoDispose 页面 pop 后无人 watch → 自动释放,不留内存
组合链 FutureProvider.autoDispose.family ------ 这三个能力可以自由组合

踩坑点

dart 复制代码
// ❌ family 参数用了复杂对象,但没实现 == 和 hashCode
//    → 每次 build 都认为是"新参数",无限重建
final p = FutureProvider.family<Data, MyFilter>((ref, filter) { ... });

// ✅ family 参数优先用基本类型(int, String, enum)
//    复杂参数务必正确实现 == / hashCode(推荐用 freezed)

场景 5:登录认证 ------ AsyncNotifier + listen 做副作用 + 依赖链

需求 :登录表单 → 提交 → 加载中 → 成功后自动跳转首页 / 失败显示错误。

这是把前面学到的东西串起来的「综合场景」。

完整代码

dart 复制代码
// auth_state.dart
class AuthState {
  final String? token;
  final String? username;

  const AuthState({this.token, this.username});

  bool get isLoggedIn => token != null;
}
dart 复制代码
// auth_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_state.dart';

class AuthNotifier extends AsyncNotifier<AuthState> {
  @override
  Future<AuthState> build() async {
    // 初始状态:未登录(实际项目这里可以读本地 token)
    return const AuthState();
  }

  Future<void> login(String username, String password) async {
    // 先切到 loading 状态
    state = const AsyncLoading();

    // AsyncValue.guard 自动把异常转为 AsyncError
    state = await AsyncValue.guard(() async {
      // 模拟网络请求
      await Future.delayed(const Duration(seconds: 2));

      if (password != '123456') {
        throw Exception('密码错误');
      }

      return AuthState(token: 'fake_token_abc', username: username);
    });
  }

  void logout() {
    state = const AsyncData(AuthState());
  }
}

final authProvider =
    AsyncNotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);

// 派生:当前是否已登录(其他地方只关心这个布尔值)
final isLoggedInProvider = Provider<bool>((ref) {
  return ref.watch(authProvider).valueOrNull?.isLoggedIn ?? false;
});
dart 复制代码
// login_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_notifier.dart';

class LoginPage extends ConsumerStatefulWidget {
  const LoginPage({super.key});
  @override
  ConsumerState<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends ConsumerState<LoginPage> {
  final _userCtrl = TextEditingController(text: 'admin');
  final _passCtrl = TextEditingController();

  @override
  void dispose() {
    _userCtrl.dispose();
    _passCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 🔄 刷新范围:authProvider 变化(未登录 → loading → 已登录/错误)
    //    → 整个 _LoginPageState.build() 重跑
    //    → ElevatedButton:loading 时变 disabled + 显示转圈
    //    → TextField 不受影响(由 TextEditingController 自己管理内容)
    final authState = ref.watch(authProvider);

    // ③ listen:监听状态变化做副作用(跳转)
    // ⚠️ listen 不触发 build 重建!它只在值变化时执行回调
    //    跳转和 SnackBar 是"副作用",不是 UI 重建
    ref.listen(authProvider, (prev, next) {
      // 登录成功 → 跳转
      if (next.valueOrNull?.isLoggedIn == true) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (_) => const HomePage()),
        );
      }
      // 登录失败 → 弹错误提示
      if (next.hasError) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${next.error}')),
        );
      }
    });

    final isLoading = authState is AsyncLoading;

    return Scaffold(
      appBar: AppBar(title: const Text('场景5:登录')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _userCtrl,
              decoration: const InputDecoration(labelText: '用户名'),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _passCtrl,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: '密码',
                hintText: '输入 123456 登录成功',
              ),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isLoading
                    ? null
                    : () => ref.read(authProvider.notifier).login(
                          _userCtrl.text,
                          _passCtrl.text,
                        ),
                child: isLoading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('登录'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class HomePage extends ConsumerWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:authProvider 变化 → 整个 HomePage.build() 重跑
    //    → AppBar title 更新用户名
    //    → body 是 const,Flutter 会复用,但仍在 build 产出树内
    final auth = ref.watch(authProvider).valueOrNull;
    return Scaffold(
      appBar: AppBar(
        title: Text('欢迎, ${auth?.username ?? ""}'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () {
              ref.read(authProvider.notifier).logout();
              Navigator.pushReplacement(
                context,
                MaterialPageRoute(builder: (_) => const LoginPage()),
              );
            },
          ),
        ],
      ),
      body: const Center(child: Text('登录成功!', style: TextStyle(fontSize: 24))),
    );
  }
}

学到什么

要点 说明
AsyncNotifier 有异步方法(login)的状态用它,自带 loading/error/data 生命周期
AsyncValue.guard 一行代码把 try/catch 变成 AsyncData 或 AsyncError
ref.listen 副作用 (跳转、SnackBar)放这里,不是 放在 build 返回的 Widget 树里
派生 Provider isLoggedInProvider 只暴露布尔值,其他页面不需要知道 token 细节
ConsumerStatefulWidget 需要 TextEditingController 等有生命周期的东西时用它

watch / read / listen 完整对照

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│                      build() 方法体                          │
│                                                              │
│   ref.watch(authProvider)  ← 订阅,值变了 build 重跑         │
│   ref.listen(authProvider, callback)  ← 订阅,值变了跑回调   │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│                   onPressed / 事件回调                        │
│                                                              │
│   ref.read(authProvider.notifier).login(...)  ← 读一次,调方法│
│                                                              │
└──────────────────────────────────────────────────────────────┘

场景 6:搜索 + 防抖 ------ 多 Provider 协作的真实模式

需求 :搜索框输入关键词 → 防抖 500ms → 发请求 → 显示结果。

展示多个 Provider 组合成链的典型做法。

完整代码

dart 复制代码
// search_providers.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;

// ① 搜索关键词(UI 写入,下游 watch)
final searchQueryProvider = StateProvider<String>((ref) => '');

// ② 防抖后的关键词
final debouncedQueryProvider = FutureProvider.autoDispose<String>((ref) async {
  final query = ref.watch(searchQueryProvider);

  // 关键:等 500ms;如果 500ms 内 searchQueryProvider 又变了,
  // 这个 Provider 会被 dispose 重建 → 旧的 Future 被丢弃 → 天然防抖
  await Future.delayed(const Duration(milliseconds: 500));

  // 如果这里 Provider 已被 dispose(用户又输入了),不会继续往下
  return query;
});

// ③ 搜索结果(依赖防抖后的关键词)
final searchResultsProvider =
    FutureProvider.autoDispose<List<String>>((ref) async {
  // watch 防抖后的值;它是 AsyncValue,用 .value 取实际值
  final query = await ref.watch(debouncedQueryProvider.future);

  if (query.isEmpty) return [];

  // 用 JSONPlaceholder 模拟搜索
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users?username=$query'),
  );
  final list = jsonDecode(response.body) as List;
  return list.map((e) => '${e["name"]} (${e["email"]})').toList();
});
dart 复制代码
// search_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'search_providers.dart';

class SearchPage extends ConsumerWidget {
  const SearchPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:searchResultsProvider 变化(防抖结束 → 请求中 → 拿到结果)
    //    → 整个 SearchPage.build() 重跑
    //    → Expanded 区域在 loading 转圈 / 结果列表 / "无结果" 之间切换
    //    ⚠️ TextField 不受影响:它通过 ref.read 写入 searchQueryProvider,
    //       自己不 watch 任何 Provider,所以搜索结果变化不会导致输入框重建或丢失焦点
    final resultsAsync = ref.watch(searchResultsProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('场景6:搜索防抖')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: TextField(
              decoration: const InputDecoration(
                hintText: '试试输入 Bret 或 Delphine',
                prefixIcon: Icon(Icons.search),
              ),
              onChanged: (value) {
                // 写入搜索关键词 → 触发整条依赖链
                ref.read(searchQueryProvider.notifier).state = value;
              },
            ),
          ),
          Expanded(
            child: resultsAsync.when(
              loading: () => const Center(child: CircularProgressIndicator()),
              error: (e, _) => Center(child: Text('搜索出错: $e')),
              data: (results) => results.isEmpty
                  ? const Center(child: Text('无结果'))
                  : ListView.builder(
                      itemCount: results.length,
                      itemBuilder: (_, i) => ListTile(
                        leading: const Icon(Icons.person),
                        title: Text(results[i]),
                      ),
                    ),
            ),
          ),
        ],
      ),
    );
  }
}

依赖链图

scss 复制代码
用户输入
   ↓
searchQueryProvider (StateProvider<String>)
   ↓  watch
debouncedQueryProvider (FutureProvider, 500ms 延迟)
   ↓  watch
searchResultsProvider (FutureProvider, 发请求)
   ↓  watch
SearchPage UI (显示结果)

学到什么

要点 说明
Provider 链 复杂逻辑拆成多个 Provider,每个只做一件事
autoDispose 实现防抖 上游变化 → 旧 Provider dispose → 新 Provider 重新等 500ms
数据流可追溯 出 bug 时沿着链条一个一个检查,而不是在一坨代码里找

场景 7:实时数据 ------ StreamProvider 监听持续变化

需求:页面实时接收服务器推送事件(类似 WebSocket / Firebase / SSE),展示连接状态 + 累积的消息历史。离开页面后自动断开连接。

这是 FutureProvider(一次性拉取)覆盖不了的场景------数据是连续推过来的

完整代码

dart 复制代码
// live_event.dart
class LiveEvent {
  final int id;
  final String content;
  final DateTime time;

  LiveEvent({required this.id, required this.content, required this.time});
}
dart 复制代码
// live_event_provider.dart
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'live_event.dart';

// 模拟服务器推送(实际项目替换为 WebSocket / Firebase / SSE 连接)
Stream<LiveEvent> _connectToServer() async* {
  const contents = [
    '新订单 #1024',
    '用户A发来消息',
    '库存预警:商品B不足10件',
    '支付成功:¥299.00',
    '系统维护提醒',
  ];
  for (var i = 0;; i++) {
    await Future.delayed(const Duration(seconds: 2));
    yield LiveEvent(
      id: i,
      content: contents[i % contents.length],
      time: DateTime.now(),
    );
  }
}

// StreamProvider:声明"数据流怎么来",框架自动管理订阅和取消
// autoDispose:离开页面 → 无人 watch → 自动取消 stream 订阅
final liveEventProvider = StreamProvider.autoDispose<LiveEvent>((ref) {
  ref.onDispose(() {
    // 实际项目:关闭 WebSocket 连接、释放资源
    print('实时连接已关闭');
  });
  return _connectToServer();
});
dart 复制代码
// live_event_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'live_event_provider.dart';
import 'live_event.dart';

class LiveEventPage extends ConsumerStatefulWidget {
  const LiveEventPage({super.key});
  @override
  ConsumerState<LiveEventPage> createState() => _LiveEventPageState();
}

class _LiveEventPageState extends ConsumerState<LiveEventPage> {
  // StreamProvider 只持有"最新一条",历史记录用局部 state 累积
  final List<LiveEvent> _history = [];

  @override
  Widget build(BuildContext context) {
    // 🔄 刷新范围:每次 stream 推送新事件 → 整个 _LiveEventPageState.build() 重跑
    //    → 顶部 Container(连接状态颜色+文字)更新
    //    → AppBar title(计数)更新
    //    → ListView(历史记录)更新
    final latestAsync = ref.watch(liveEventProvider);

    // listen:每来一条新事件,追加到历史列表
    // ⚠️ listen 本身不触发 build;但它内部的 setState(() => _history.insert(...))
    //    会触发 StatefulWidget 自己的重建。两次重建(watch + setState)
    //    在同一帧内被 Flutter 合并,实际只执行一次 build
    ref.listen(liveEventProvider, (prev, next) {
      final event = next.valueOrNull;
      if (event != null && !_history.any((e) => e.id == event.id)) {
        setState(() => _history.insert(0, event));
      }
    });

    return Scaffold(
      appBar: AppBar(title: Text('实时事件(${_history.length} 条)')),
      body: Column(
        children: [
          // 顶部:连接状态指示
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            color: latestAsync.when(
              loading: () => Colors.orange.shade100,
              error: (_, __) => Colors.red.shade100,
              data: (_) => Colors.green.shade100,
            ),
            child: latestAsync.when(
              loading: () => const Row(
                children: [
                  SizedBox(
                    width: 16,
                    height: 16,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  ),
                  SizedBox(width: 8),
                  Text('正在连接服务器...'),
                ],
              ),
              error: (e, _) => Text('连接断开: $e'),
              data: (event) => Text(
                '最新: ${event.content}',
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
          ),
          // 下方:历史记录
          Expanded(
            child: _history.isEmpty
                ? const Center(child: Text('等待事件推送...'))
                : ListView.builder(
                    itemCount: _history.length,
                    itemBuilder: (_, i) {
                      final e = _history[i];
                      final t = e.time;
                      return ListTile(
                        leading: CircleAvatar(child: Text('${e.id}')),
                        title: Text(e.content),
                        subtitle: Text(
                          '${t.hour.toString().padLeft(2, '0')}:'
                          '${t.minute.toString().padLeft(2, '0')}:'
                          '${t.second.toString().padLeft(2, '0')}',
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

FutureProvider vs StreamProvider 对比

markdown 复制代码
FutureProvider(场景 3)          StreamProvider(场景 7)
┌─────────────────────┐          ┌─────────────────────┐
│  请求 ──→ 等待 ──→ 结果 │          │  订阅 ──→ 事件1      │
│       (一次性)      │          │         ──→ 事件2      │
│                     │          │         ──→ 事件3      │
│                     │          │         ──→ ...持续    │
└─────────────────────┘          └─────────────────────┘
  用于:GET 接口、配置加载           用于:WebSocket、Firebase、
        一次拉取的数据                    实时推送、传感器数据

学到什么

要点 说明
StreamProvider 声明"流怎么来",框架自动订阅/取消,暴露 AsyncValue
ref.onDispose Provider 销毁时的清理回调,用于关闭连接、释放资源
autoDispose + Stream 离开页面 → 无人 watch → Provider 销毁 → Stream 取消 → 不泄漏
watch + listen 配合 watch 驱动 UI 刷新,listen 处理"累积历史"这种副作用

踩坑点

dart 复制代码
// ❌ 忘了 autoDispose → 离开页面后 stream 还在跑,浪费资源
final provider = StreamProvider<Event>((ref) => myStream);

// ✅ 加 autoDispose,离开页面自动取消
final provider = StreamProvider.autoDispose<Event>((ref) => myStream);
dart 复制代码
// ❌ 想在 StreamProvider 里累积历史 → 做不到,它只持有最新值
final historyProvider = StreamProvider<List<Event>>((ref) => ...);
// 每次 stream 发一个 event,你拿到的是单个 event 不是列表

// ✅ StreamProvider 拿最新值 + 另一个 Notifier/StatefulWidget 累积
//    就像上面例子中 ref.listen + setState 的做法

场景 8(番外):测试 ------ Riverpod 的「高级价值」

前面 7 个场景都能用别的方案做,但测试体验是 Riverpod 真正拉开差距的地方。

dart 复制代码
// 纯逻辑测试,不需要 Flutter、不需要 Widget、不需要模拟器
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_notifier.dart';

void main() {
  test('添加待办', () {
    // 创建一个独立容器,和 app 完全隔离
    final container = ProviderContainer();
    addTearDown(container.dispose);

    expect(container.read(todoListProvider), isEmpty);

    container.read(todoListProvider.notifier).add('买牛奶');
    expect(container.read(todoListProvider), hasLength(1));
    expect(container.read(todoListProvider).first.title, '买牛奶');
  });

  test('完成数量自动更新', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    container.read(todoListProvider.notifier).add('任务A');
    container.read(todoListProvider.notifier).add('任务B');
    expect(container.read(completedCountProvider), 0);

    final id = container.read(todoListProvider).first.id;
    container.read(todoListProvider.notifier).toggle(id);
    expect(container.read(completedCountProvider), 1);
  });

  test('替换 Repository 做假数据测试', () async {
    final container = ProviderContainer(
      overrides: [
        // 把真 Repository 换成假的,不发网络请求
        userRepositoryProvider.overrideWithValue(FakeUserRepository()),
      ],
    );
    addTearDown(container.dispose);

    final users = await container.read(userListProvider.future);
    expect(users.first.name, 'Fake User');
  });
}

// 假实现
class FakeUserRepository implements UserRepository {
  @override
  Future<List<User>> fetchUsers() async {
    return [User(id: 1, name: 'Fake User', email: 'fake@test.com')];
  }
}

核心优势ProviderContainer 在纯 Dart 环境下工作,不需要 pumpWidget,测试跑得快;overrides 让你能替换任何一层依赖,不需要全局 mock 框架。


场景 9:模块化开发 ------ 主项目与子插件之间的状态同步

需求 :团队按业务拆包,主项目(app)和多个子插件(package)独立开发。

子插件需要读主项目的状态(比如登录用户信息),主项目也要响应子插件的状态变化(比如购物车数量)。
关键约束:子插件不能 import 主项目代码,否则就不叫"独立"了。

目录结构

vbnet 复制代码
my_flutter_app/               ← 主项目
├── lib/
│   ├── main.dart
│   ├── auth/
│   │   └── auth_providers.dart      ← 主项目持有登录状态
│   └── app_providers.dart           ← 组装所有模块的 overrides
│
├── packages/
│   ├── core_shared/           ← 共享层:只放接口和数据模型,不放实现
│   │   └── lib/
│   │       ├── models/
│   │       │   └── user_info.dart
│   │       └── providers/
│   │           ├── shared_auth_provider.dart    ← 抽象 Provider(占位)
│   │           └── shared_cart_provider.dart    ← 抽象 Provider(占位)
│   │
│   ├── feature_profile/       ← 子插件A:个人中心
│   │   └── lib/
│   │       └── profile_page.dart    ← watch 共享层的 Provider
│   │
│   └── feature_cart/          ← 子插件B:购物车
│       └── lib/
│           ├── cart_notifier.dart    ← 购物车业务逻辑
│           └── cart_page.dart

核心思路:三层

scss 复制代码
┌──────────────────────────────────────────────────────┐
│  主项目 (app)                                         │
│  · 持有真实实现(auth_providers 等)                    │
│  · 在 ProviderScope(overrides: [...]) 里把真实实现      │
│    注入到共享层的"占位 Provider"                        │
├──────────────────────────────────────────────────────┤
│  共享层 (core_shared package)                         │
│  · 只定义接口 + 数据模型 + "占位 Provider"              │
│  · 不依赖任何具体实现                                  │
├──────────────────────────────────────────────────────┤
│  子插件 (feature_xxx packages)                        │
│  · 只 import core_shared                              │
│  · watch/read 共享层的 Provider                        │
│  · 完全不知道主项目的存在                               │
└──────────────────────────────────────────────────────┘

第一步:共享层 ------ 定义接口和占位 Provider

dart 复制代码
// packages/core_shared/lib/models/user_info.dart

class UserInfo {
  final String uid;
  final String name;
  final String avatar;

  const UserInfo({
    required this.uid,
    required this.name,
    required this.avatar,
  });

  static const empty = UserInfo(uid: '', name: '', avatar: '');

  bool get isLoggedIn => uid.isNotEmpty;
}
dart 复制代码
// packages/core_shared/lib/providers/shared_auth_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/user_info.dart';

/// 占位 Provider:运行时必须被主项目 override,否则直接报错。
/// 子插件只 watch 这个,不需要知道登录逻辑的实现细节。
final sharedUserProvider = Provider<UserInfo>((ref) {
  throw UnimplementedError(
    'sharedUserProvider 必须在主项目的 ProviderScope 中 override!'
    '请检查 main.dart 的 ProviderScope(overrides: [...])',
  );
});
dart 复制代码
// packages/core_shared/lib/providers/shared_cart_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

/// 购物车条目数(子插件写入,主项目可以 watch)
/// 默认实现返回 0;子插件会 override 注入真实 Notifier
final sharedCartCountProvider = Provider<int>((ref) => 0);

第二步:子插件 A(个人中心) ------ 只 import 共享层

yaml 复制代码
# packages/feature_profile/pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.6.1
  core_shared:
    path: ../core_shared     # 只依赖共享层
  # ↑ 注意:不依赖主项目,不依赖 feature_cart
dart 复制代码
// packages/feature_profile/lib/profile_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:core_shared/providers/shared_auth_provider.dart';
import 'package:core_shared/providers/shared_cart_provider.dart';

class ProfilePage extends ConsumerWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:sharedUserProvider 或 sharedCartCountProvider 任一变化
    //    → 整个 ProfilePage.build() 重跑
    //    → CircleAvatar、用户名 Text、购物车数 Text 全部更新
    //    ⚠️ CartPage 和 MainShell 不受 ProfilePage 重建影响(各自独立 watch)
    final user = ref.watch(sharedUserProvider);
    final cartCount = ref.watch(sharedCartCountProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('个人中心(子插件A)')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            CircleAvatar(
              radius: 40,
              child: Text(user.name.isNotEmpty ? user.name[0] : '?',
                  style: const TextStyle(fontSize: 32)),
            ),
            const SizedBox(height: 16),
            Text('用户名:${user.name}', style: const TextStyle(fontSize: 20)),
            Text('UID:${user.uid}'),
            const Divider(height: 32),
            Text('购物车商品数:$cartCount',
                style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 12),
            const Text(
              '👆 这个数字来自 feature_cart 子插件,\n'
              '   但 profile 完全不知道 cart 的存在,\n'
              '   两个插件通过共享层的 Provider 间接通信。',
              style: TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

第三步:子插件 B(购物车) ------ 暴露 Notifier 供主项目注入

yaml 复制代码
# packages/feature_cart/pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.6.1
  core_shared:
    path: ../core_shared     # 只依赖共享层
dart 复制代码
// packages/feature_cart/lib/cart_notifier.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:core_shared/providers/shared_auth_provider.dart';
import 'package:core_shared/models/user_info.dart';

class CartItem {
  final String id;
  final String name;
  final int quantity;

  const CartItem({required this.id, required this.name, this.quantity = 1});

  CartItem copyWith({int? quantity}) =>
      CartItem(id: id, name: name, quantity: quantity ?? this.quantity);
}

class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() {
    // 购物车依赖当前用户 → watch 共享层的 user
    // 用户切换时,购物车自动清空重建
    final user = ref.watch(sharedUserProvider);
    if (!user.isLoggedIn) return [];

    // 实际项目这里可以从本地缓存加载该用户的购物车
    return [];
  }

  void addItem(String name) {
    state = [...state, CartItem(id: '${state.length + 1}', name: name)];
  }

  void removeItem(String id) {
    state = state.where((item) => item.id != id).toList();
  }

  void clear() {
    state = [];
  }
}

// 子插件内部的完整 Provider(主项目会用它来 override 共享层的计数)
final cartProvider =
    NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);
dart 复制代码
// packages/feature_cart/lib/cart_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'cart_notifier.dart';

class CartPage extends ConsumerWidget {
  const CartPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:cartProvider 变化(添加/删除/清空商品)
    //    → 整个 CartPage.build() 重跑
    //    → AppBar(件数) + ListView(商品列表) 全部更新
    //    ⚠️ 同时 MainShell 的 cartCount 也会变 → MainShell 也重建(独立的 watch 链路)
    final items = ref.watch(cartProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('购物车(${items.length} 件)'),
        actions: [
          if (items.isNotEmpty)
            IconButton(
              icon: const Icon(Icons.delete_sweep),
              onPressed: () => ref.read(cartProvider.notifier).clear(),
            ),
        ],
      ),
      body: items.isEmpty
          ? const Center(child: Text('购物车是空的'))
          : ListView.builder(
              itemCount: items.length,
              itemBuilder: (_, i) => ListTile(
                title: Text(items[i].name),
                trailing: IconButton(
                  icon: const Icon(Icons.remove_circle_outline),
                  onPressed: () =>
                      ref.read(cartProvider.notifier).removeItem(items[i].id),
                ),
              ),
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(cartProvider.notifier).addItem('商品 ${items.length + 1}'),
        child: const Icon(Icons.add_shopping_cart),
      ),
    );
  }
}

第四步:主项目 ------ 用 overrides 把一切串起来

dart 复制代码
// lib/auth/auth_providers.dart  (主项目内部的真实登录实现)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:core_shared/models/user_info.dart';

class AuthNotifier extends Notifier<UserInfo> {
  @override
  UserInfo build() => UserInfo.empty;

  void login() {
    state = const UserInfo(
      uid: 'u_10086',
      name: '张三',
      avatar: 'https://example.com/avatar.png',
    );
  }

  void logout() {
    state = UserInfo.empty;
  }
}

final authProvider =
    NotifierProvider<AuthNotifier, UserInfo>(AuthNotifier.new);
dart 复制代码
// lib/main.dart  (核心:ProviderScope overrides 把所有模块连起来)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 共享层
import 'package:core_shared/providers/shared_auth_provider.dart';
import 'package:core_shared/providers/shared_cart_provider.dart';

// 主项目
import 'auth/auth_providers.dart';

// 子插件
import 'package:feature_profile/profile_page.dart';
import 'package:feature_cart/cart_notifier.dart';
import 'package:feature_cart/cart_page.dart';

void main() {
  runApp(
    ProviderScope(
      overrides: [
        // ★ 关键:把共享层的"占位 Provider"指向真实实现

        // 1. 共享用户信息 ← 主项目的 authProvider
        sharedUserProvider.overrideWith((ref) {
          return ref.watch(authProvider);  // auth 变 → 共享 user 变 → 子插件自动刷新
        }),

        // 2. 共享购物车数量 ← 子插件 cart 的 cartProvider
        sharedCartCountProvider.overrideWith((ref) {
          return ref.watch(cartProvider).length;  // cart 变 → 数量变 → profile 自动刷新
        }),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '模块化 Riverpod Demo',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
      home: const MainShell(),
    );
  }
}

class MainShell extends ConsumerWidget {
  const MainShell({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:authProvider / cartProvider / _tabIndexProvider 任一变化
    //    → 整个 MainShell.build() 重跑
    //    → AppBar(用户名/登录按钮) + BottomNavigationBar(购物车 Badge + 选中态) 更新
    //    ⚠️ body 是 const _TabBody(),它是独立的 ConsumerWidget
    //       MainShell 重建时 Flutter 发现 _TabBody 是同一个 const 实例,跳过重建
    //       _TabBody 只在自己 watch 的 _tabIndexProvider 变化时才重建
    final user = ref.watch(authProvider);
    final cartCount = ref.watch(cartProvider).length;

    return Scaffold(
      appBar: AppBar(
        title: Text(user.isLoggedIn ? '你好, ${user.name}' : '未登录'),
        actions: [
          if (user.isLoggedIn)
            IconButton(
              icon: const Icon(Icons.logout),
              onPressed: () => ref.read(authProvider.notifier).logout(),
            )
          else
            IconButton(
              icon: const Icon(Icons.login),
              onPressed: () => ref.read(authProvider.notifier).login(),
            ),
        ],
      ),
      body: const _TabBody(),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          const BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
          BottomNavigationBarItem(
            icon: Badge(
              label: Text('$cartCount'),
              isLabelVisible: cartCount > 0,
              child: const Icon(Icons.shopping_cart),
            ),
            label: '购物车',
          ),
        ],
        onTap: (i) => ref.read(_tabIndexProvider.notifier).state = i,
        currentIndex: ref.watch(_tabIndexProvider),
      ),
    );
  }
}

final _tabIndexProvider = StateProvider<int>((ref) => 0);

class _TabBody extends ConsumerWidget {
  const _TabBody();
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🔄 刷新范围:_tabIndexProvider 变化(切换 tab)
    //    → _TabBody.build() 重跑 → 切换显示 ProfilePage 或 CartPage
    //    ⚠️ authProvider / cartProvider 变化不影响 _TabBody(它不 watch 它们)
    //       但 ProfilePage / CartPage 各自有自己的 watch,会独立刷新
    return switch (ref.watch(_tabIndexProvider)) {
      0 => const ProfilePage(),  // 子插件 A
      1 => const CartPage(),     // 子插件 B
      _ => const SizedBox(),
    };
  }
}

数据流全景图

scss 复制代码
┌─ 主项目 ──────────────────────────────────────────────────────────────┐
│                                                                       │
│  authProvider (AuthNotifier)                                          │
│       │                                                               │
│       │  override                                                     │
│       ▼                                                               │
│  ┌─ 共享层 ──────────────────────────────────────────────────────┐    │
│  │  sharedUserProvider ◄──── watch ──── CartNotifier.build()     │    │
│  │                                      (用户切换→购物车清空)     │    │
│  │  sharedCartCountProvider ◄── override ── cartProvider.length  │    │
│  └───────────────────────────────────────────────────────────────┘    │
│       │                    │                                          │
│       │ watch              │ watch                                    │
│       ▼                    ▼                                          │
│  ProfilePage          CartPage                                        │
│  (子插件A)            (子插件B)                                       │
│  显示用户名+购物车数   添加/删除商品                                   │
└───────────────────────────────────────────────────────────────────────┘

学到什么

要点 说明
共享层只放接口 core_shared 只有数据模型 + 占位 Provider,不含任何业务逻辑实现
占位 Provider 抛异常 忘了 override 会立即报错,不会悄悄返回错误数据
overrideWith 建立桥梁 主项目在 ProviderScope 把真实实现注入占位 Provider
子插件互不 import profile 不 import cart,cart 不 import profile,但通过共享层间接通信
依赖方向清晰 子插件 → 共享层 ← 主项目;箭头永远指向共享层,不会交叉
用户切换自动连锁 auth 变 → sharedUser 变 → CartNotifier.build 重跑 → 购物车清空 → 数量归零 → profile 页刷新

踩坑点

dart 复制代码
// ❌ 子插件直接 import 主项目代码 → 循环依赖,无法独立编译
import 'package:my_app/auth/auth_providers.dart'; // 千万别这样

// ✅ 子插件只 import core_shared
import 'package:core_shared/providers/shared_auth_provider.dart';
dart 复制代码
// ❌ 忘了在 ProviderScope 写 override → 运行时 UnimplementedError 崩溃
ProviderScope(
  overrides: [], // 漏了!
  child: MyApp(),
)

// ✅ 占位 Provider 的报错信息会明确告诉你漏了什么
dart 复制代码
// ❌ override 里用 read 代替 watch → 后续变化不同步
sharedUserProvider.overrideWith((ref) {
  return ref.read(authProvider); // 只读一次,用户登出后 profile 不会更新!
}),

// ✅ override 里用 watch → 源头变化自动传播到所有下游
sharedUserProvider.overrideWith((ref) {
  return ref.watch(authProvider); // auth 变 → sharedUser 变 → 全链路刷新
}),

Riverpod 3 迁移速查(影响线上行为的 5 条)

如果你的项目要升级到 Riverpod 3.0,以下是会改变运行时行为的变更:

变更 影响 应对
自动重试 失败的 Provider 默认自动重试 非幂等操作需要显式 retry: (...) => null
不可见时暂停 页面不可见时 watch 暂停 需要后台持续监听的场景用 TickerMode(enabled: true) 包裹
统一用 == 过滤 相等的新值不再触发更新 Stream 场景需注意;可 override updateShouldNotify
异常包装 catch 拿到的是 ProviderException 需要 e.exception is XxxError 二次拆包
Legacy import StateProvider 等移到 legacy.dart 新代码用 Notifier,旧代码改 import 路径

总结:一张表选 Provider 类型

你的需求 用什么 场景参照
一个开关/枚举 StateProvider 场景 1
有方法的同步状态 Notifier + NotifierProvider 场景 2
异步只读数据 FutureProvider 场景 3
按参数缓存 .family 场景 4
页面级临时状态 .autoDispose 场景 4
有方法的异步状态 AsyncNotifier 场景 5
多步数据管道 多个 Provider 链式 watch 场景 6
只读派生计算 Provider watch 上游 场景 2 (completedCount)
实时数据流 StreamProvider.autoDispose + ref.onDispose 场景 7
主项目与子插件状态同步 共享层占位 Provider + overrideWith 场景 9

附录:9 个场景的刷新范围总览

一句话原则

ref.watch 写在哪个 Widget 的 build 里,那个 Widget 就是刷新边界。

Provider 变化只会重建 watch 了它的 ConsumerWidget / ConsumerStatefulWidget,不会波及其他。

全景对照表

scss 复制代码
场景1  isDarkModeProvider 变化
       └─🔄 ThemePage.build()          ← 整个页面重建(Switch + Text)

场景2  todoListProvider 变化
       └─🔄 TodoPage.build()           ← AppBar(计数) + ListView(所有 ListTile) 重建
          └─ completedCountProvider     ← 派生 Provider 跟着自动重算

场景3  userListProvider 状态切换 (loading ↔ data ↔ error)
       └─🔄 UserListPage.build()       ← body 区域三态切换

场景4  userDetailProvider(userId) 状态切换
       └─🔄 UserDetailPage.build()     ← 仅该 userId 的详情页重建
          ⚠️ 其他 userId 的页面不受影响  ← family 的隔离作用

场景5  authProvider 变化 (未登录 → loading → 已登录)
       ├─🔄 _LoginPageState.build()    ← ElevatedButton 切换 loading/可点击
       │  └─ ref.listen → 回调         ← ⚠️ 不触发 build,只执行跳转/SnackBar
       └─🔄 HomePage.build()           ← AppBar 用户名更新

场景6  searchResultsProvider 变化 (防抖 → 请求 → 结果)
       └─🔄 SearchPage.build()         ← Expanded 区域切换
          ⚠️ TextField 不受影响         ← 它通过 ref.read 写入,不 watch

场景7  liveEventProvider (stream 每 2s 推送)
       └─🔄 _LiveEventPageState.build()← Container(状态) + ListView(历史) 更新
          └─ ref.listen + setState      ← 与 watch 合并为同一帧,不双重渲染

场景9  authProvider 变化 (登录/登出)
       ├─🔄 MainShell.build()          ← AppBar(用户名) + BottomNav 更新
       │  └─ const _TabBody()          ← ⚠️ 不被 MainShell 重建波及(const 复用)
       ├─ override 链: auth → sharedUser → CartNotifier.build()
       │  └─🔄 CartPage.build()        ← 购物车清空,列表重建
       │  └─ override 链: cart.length → sharedCartCount
       │     └─🔄 ProfilePage.build()  ← 购物车数字归零
       └─🔄 _TabBody.build()           ← 仅在 _tabIndexProvider 变化时重建

       cartProvider 变化 (添加商品)
       ├─🔄 CartPage.build()           ← 列表更新
       ├─🔄 MainShell.build()          ← Badge 数字更新
       └─🔄 ProfilePage.build()        ← 购物车数字更新
          ⚠️ _TabBody 不受影响          ← 它不 watch cartProvider

如何缩小刷新范围(进阶技巧)

上面每个场景中,ref.watch 都写在 Widget 的 build 最顶层 ,所以整个 build 树都会重建。

实际项目中可以用以下手段 缩小刷新范围

先理解一个前提build() 重跑 ≠ 整个页面像素级重绘。Flutter 渲染分三层:
Widget 树 (build 产出)→ Element 树 (框架做新旧 diff)→ RenderObject 树 (真正布局和绘制像素)。
const Widget 在 diff 时直接跳过;只有真正内容变了的 RenderObject 才会重新 layout/paint。

所以 build 本身是轻量的,真正昂贵的是 layout 和 paint

下面的技巧目标是:连 build 的调用范围也收窄到最小


技巧 1:Consumer 局部包裹 ------ 最常用,改造成本最低

场景:一个商品详情页,只有底部的「购物车数量」需要实时更新,其他部分(图片、标题、描述)都是静态的。

dart 复制代码
// product_detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 假设已有
final cartProvider = StateProvider<int>((ref) => 0);

// ⚠️ 注意:外层是普通 StatelessWidget,不是 ConsumerWidget
//    → 它自己永远不会因为 Provider 变化而重建
class ProductDetailPage extends StatelessWidget {
  const ProductDetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    print('ProductDetailPage.build()'); // 只在首次进入时打印一次

    return Scaffold(
      appBar: AppBar(title: const Text('AirPods Pro')),
      body: Column(
        children: [
          // ────── 这些全是静态内容,永远不重建 ──────
          const SizedBox(
            height: 200,
            child: Placeholder(), // 模拟商品图片
          ),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              'Apple AirPods Pro 第二代',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
          ),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child: Text('主动降噪 · 自适应通透模式 · 个性化空间音频'),
          ),
          const Divider(height: 32),

          // ────── 只有这一小块会因为 cartProvider 变化而重建 ──────
          Consumer(
            builder: (context, ref, child) {
              print('  Consumer.build()'); // 每次 cart 变化都打印
              // 🔄 刷新范围:仅此 Consumer 内部的 Row
              final count = ref.watch(cartProvider);
              return Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('购物车: $count 件',
                        style: const TextStyle(fontSize: 18)),
                    FilledButton.icon(
                      onPressed: () =>
                          ref.read(cartProvider.notifier).state++,
                      icon: const Icon(Icons.add_shopping_cart),
                      label: const Text('加入购物车'),
                    ),
                  ],
                ),
              );
            },
          ),

          // ────── 这下面也是静态内容,永远不重建 ──────
          const SizedBox(height: 20),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('商品详情介绍...(很长的文字)'),
          ),
        ],
      ),
    );
  }
}

刷新范围图

arduino 复制代码
ProductDetailPage (StatelessWidget)          ← 永远不重建
├── AppBar                                   ← 永远不重建
├── 商品图片 (const)                          ← 永远不重建
├── 商品标题 (const)                          ← 永远不重建
├── 商品描述 (const)                          ← 永远不重建
├── Consumer                                 ← 🔄 仅此块重建
│   └── Row: "购物车: N 件" + 按钮            ← 🔄 count 变了才更新
├── 商品详情 (const)                          ← 永远不重建

学到什么

  • 外层用 StatelessWidget 而不是 ConsumerWidget → 整个页面骨架永远不参与刷新
  • Consumer 是一个普通 Widget,可以塞在树的任意位置,不需要重构整个页面
  • 点击「加入购物车」→ cartProvider 变化 → 只有 Consumer 内部的 Row 重建,图片/标题/描述纹丝不动

技巧 2:select 精确订阅某个字段 ------ 大对象只关心一部分时

场景:用户资料有很多字段(头像、昵称、签名、积分、等级...),但某个 Widget 只显示昵称。

dart 复制代码
// user_profile_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserProfile {
  final String name;
  final String avatar;
  final String bio;
  final int points;
  final int level;

  const UserProfile({
    required this.name,
    required this.avatar,
    required this.bio,
    required this.points,
    required this.level,
  });
}

class UserProfileNotifier extends Notifier<UserProfile> {
  @override
  UserProfile build() => const UserProfile(
        name: '张三',
        avatar: 'https://example.com/avatar.png',
        bio: 'Flutter 开发者',
        points: 1000,
        level: 5,
      );

  void addPoints(int p) {
    state = UserProfile(
      name: state.name,
      avatar: state.avatar,
      bio: state.bio,
      points: state.points + p,
      level: state.level,
    );
  }

  void updateName(String name) {
    state = UserProfile(
      name: name,
      avatar: state.avatar,
      bio: state.bio,
      points: state.points,
      level: state.level,
    );
  }
}

final userProfileProvider =
    NotifierProvider<UserProfileNotifier, UserProfile>(UserProfileNotifier.new);
dart 复制代码
// select_demo_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'user_profile_provider.dart';

class SelectDemoPage extends ConsumerWidget {
  const SelectDemoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ❌ 不用 select:user 的任何字段变化(积分、等级...)都会让这里重建
    // final user = ref.watch(userProfileProvider);

    // ✅ 用 select:只订阅 name 字段
    //    积分变了?不重建。等级变了?不重建。只有 name 变了才重建
    // 🔄 刷新范围:仅当 name 字段的 == 比较结果变化时,才重建 SelectDemoPage
    final name = ref.watch(
      userProfileProvider.select((profile) => profile.name),
    );

    print('SelectDemoPage.build() - name=$name');

    return Scaffold(
      appBar: AppBar(title: Text('你好, $name')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('当前用户: $name', style: const TextStyle(fontSize: 24)),
            const SizedBox(height: 24),
            // 点这个按钮 → 积分变了 → 但 name 没变 → SelectDemoPage 不重建
            ElevatedButton(
              onPressed: () =>
                  ref.read(userProfileProvider.notifier).addPoints(100),
              child: const Text('加 100 积分(不触发本页重建)'),
            ),
            const SizedBox(height: 12),
            // 点这个按钮 → name 变了 → SelectDemoPage 重建
            ElevatedButton(
              onPressed: () =>
                  ref.read(userProfileProvider.notifier).updateName('李四'),
              child: const Text('改名为李四(触发本页重建)'),
            ),
            const SizedBox(height: 32),
            // 用另一个 Consumer + select 单独显示积分
            Consumer(
              builder: (context, ref, _) {
                // 🔄 刷新范围:仅当 points 变化时,这个 Consumer 重建
                final points = ref.watch(
                  userProfileProvider.select((p) => p.points),
                );
                print('  Points Consumer.build() - points=$points');
                return Text('积分: $points', style: const TextStyle(fontSize: 20));
              },
            ),
          ],
        ),
      ),
    );
  }
}

刷新范围图

scss 复制代码
用户点击「加 100 积分」→ userProfileProvider 变化(points 字段改了)

SelectDemoPage (select: name)                ← ⚠️ 不重建!name 没变
├── AppBar(title: '你好, 张三')               ← ⚠️ 不重建
├── Text('当前用户: 张三')                     ← ⚠️ 不重建
├── ElevatedButton('加积分')                  ← ⚠️ 不重建
├── ElevatedButton('改名')                    ← ⚠️ 不重建
└── Consumer (select: points)                ← 🔄 重建!points 变了
    └── Text('积分: 1100')                    ← 🔄 更新数字

用户点击「改名为李四」→ userProfileProvider 变化(name 字段改了)

SelectDemoPage (select: name)                ← 🔄 重建!name 变了
├── AppBar(title: '你好, 李四')               ← 🔄 更新
├── Text('当前用户: 李四')                     ← 🔄 更新
└── Consumer (select: points)                ← ⚠️ 不重建!points 没变

学到什么

  • select== 对比投影结果,相等就跳过 → 精确到字段级别的刷新控制
  • 同一个 Provider 可以在不同位置用不同 select,各自只关心自己要的字段
  • 特别适合大 Model 对象(User、Order、Settings...),避免无关字段变化导致的连锁重建

技巧 3:拆分成多个小 ConsumerWidget ------ 中大型页面的标准做法

场景:把场景 2 的待办清单拆分,让「AppBar 计数」和「列表内容」各自独立刷新。

dart 复制代码
// todo_page_optimized.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_notifier.dart';

// ⚠️ 主页面是普通 StatelessWidget,不参与任何 Provider 刷新
class TodoPageOptimized extends StatelessWidget {
  const TodoPageOptimized({super.key});

  @override
  Widget build(BuildContext context) {
    print('TodoPageOptimized.build()'); // 只在首次时打印一次
    return Scaffold(
      appBar: AppBar(
        title: const _TodoAppBarTitle(), // 独立 ConsumerWidget
      ),
      body: const _TodoListBody(),       // 独立 ConsumerWidget
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (_) => Consumer(
        builder: (context, ref, _) => AlertDialog(
          title: const Text('添加待办'),
          content: TextField(controller: controller, autofocus: true),
          actions: [
            TextButton(
              onPressed: () {
                if (controller.text.isNotEmpty) {
                  ref.read(todoListProvider.notifier).add(controller.text);
                }
                Navigator.pop(context);
              },
              child: const Text('确定'),
            ),
          ],
        ),
      ),
    );
  }
}

// ────── 拆出来的小组件 1:AppBar 标题 ──────
class _TodoAppBarTitle extends ConsumerWidget {
  const _TodoAppBarTitle();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print('  _TodoAppBarTitle.build()');
    // 🔄 刷新范围:仅此 Widget
    //    用 select 只订阅 length 和完成数,列表内容变化但数量不变时不重建
    final total = ref.watch(todoListProvider.select((list) => list.length));
    final done = ref.watch(completedCountProvider);
    return Text('待办 (已完成 $done/$total)');
  }
}

// ────── 拆出来的小组件 2:列表 ──────
class _TodoListBody extends ConsumerWidget {
  const _TodoListBody();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print('  _TodoListBody.build()');
    // 🔄 刷新范围:仅此 Widget
    //    列表数据变化时只重建列表,不影响 AppBar
    final todos = ref.watch(todoListProvider);
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (_, i) {
        final todo = todos[i];
        return ListTile(
          leading: Checkbox(
            value: todo.completed,
            onChanged: (_) =>
                ref.read(todoListProvider.notifier).toggle(todo.id),
          ),
          title: Text(
            todo.title,
            style: TextStyle(
              decoration: todo.completed ? TextDecoration.lineThrough : null,
            ),
          ),
          trailing: IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () =>
                ref.read(todoListProvider.notifier).remove(todo.id),
          ),
        );
      },
    );
  }
}

刷新范围对比(优化前 vs 优化后)

scss 复制代码
优化前(场景 2 原始写法):
  todoListProvider 变化
  └─🔄 TodoPage.build()               ← 整页重建(AppBar + ListView + FAB 全跑一遍)

优化后(拆分写法):
  勾选一个待办(toggle)→ todoListProvider 变化
  ├─ TodoPageOptimized.build()         ← ⚠️ 不重建(StatelessWidget,没 watch)
  ├─🔄 _TodoAppBarTitle.build()        ← 🔄 重建(completedCount 变了)
  ├─🔄 _TodoListBody.build()           ← 🔄 重建(列表内容变了)
  └─ FloatingActionButton              ← ⚠️ 不重建(const Icon)

  添加一个待办 → todoListProvider 变化,但没有新完成的
  ├─ TodoPageOptimized.build()         ← ⚠️ 不重建
  ├─🔄 _TodoAppBarTitle.build()        ← 🔄 重建(total 从 2 变 3)
  ├─🔄 _TodoListBody.build()           ← 🔄 重建(列表多了一条)
  └─ FloatingActionButton              ← ⚠️ 不重建

学到什么

  • 把一个大 ConsumerWidget 拆成 外壳 StatelessWidget + 多个小 ConsumerWidget
  • 每个小组件只 watch 自己关心的数据 → 不相关的变化不会波及
  • 配合 select 还能进一步收窄(比如 _TodoAppBarTitle 只 select length)
  • 这是中大型项目的标准做法,不是过度优化

三种技巧选择指南
情况 推荐技巧 改造成本
页面大部分是静态的,只有一小块需要动态数据 Consumer 局部包裹 最低,加 3 行代码
一个大对象(User/Order),但只用其中 1-2 个字段 select 精确订阅 低,改一行 watch
页面复杂、多个区域依赖不同 Provider 拆分小 ConsumerWidget 中等,需要提取组件
以上组合 Consumer + select + 拆 Widget 混用 视情况而定

总结 :场景 1-9 的示例为了教学清晰,用整个 ConsumerWidget 做页面。

生产项目中应该按上面的技巧 把刷新范围收窄到真正需要更新的部分
build() 重跑不等于像素重绘(Flutter 框架会 diff),但收窄 build 范围仍然是好习惯------

减少不必要的 Widget 实例创建、diff 对比和 GC 压力,尤其在列表页和复杂表单页效果明显。

相关推荐
MonkeyKing71557 小时前
Flutter Riverpod 2.x 设计思想与最佳实践
前端·flutter
梦想不只是梦与想8 小时前
Flutter中 yield*关键字
flutter·生成器函数
用户游民10 小时前
Flutter GetX实现原理
前端·flutter
MonkeyKing715510 小时前
Flutter列表性能极致优化:从卡顿到丝滑
flutter
恋猫de小郭11 小时前
实用性 Max ,新 Flutter & Dart Agent Skills 深度解读
android·前端·flutter
Jolyne_1 天前
flutter学习(一)环境搭建及基础速通
flutter
MonkeyKing71551 天前
Flutter状态管理实战:全局、局部、页面状态拆分指南
前端·flutter
MonkeyKing71551 天前
Flutter异步状态统一处理实战:告别混乱,优雅管理请求与加载
flutter
MonkeyKing71551 天前
Flutter项目结构与模块化、组件化、插件化
flutter