第 7.5 章 Riverpod 入门详解(重点)

📎 先跑起来再读原理 第 7.2 节已经给出一个完整的购物车单文件程序(含 ProviderScope、Notifier、派生 Provider)。建议先把它跑起来,再回来读本章的原理与六种 Provider 的区别。

Riverpod 是 Flutter 社区目前最主流的状态管理库(pub.dev 点赞数第一梯队),由 Provider 作者 Remi Rousselet 重写。它编译期安全(类型错误直接编译不过)、不依赖 BuildContext、天然支持异步状态与测试。

7.5.1 安装与最小示例

# pubspec.yaml(版本以 pub.dev 上 flutter_riverpod 最新稳定版为准)
dependencies:
  flutter_riverpod: ^3.0.0
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ① 定义 Provider:全局常量,类型是 Provider<int>
final counterProvider = Provider<int>((ref) => 42);

// ② 根组件包 ProviderScope(整个应用只能有一个)
void main() => runApp(const ProviderScope(child: MaterialApp(home: Home())));

// ③ 用 ConsumerWidget 替代 StatelessWidget,build 里多出 ref 参数
class Home extends ConsumerWidget {
  const Home({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(counterProvider); // 监听:值变则重建
    return Scaffold(body: Center(child: Text('值 = $value')));
  }
}

7.5.2 六种 Provider,各管一件事

类型用途状态可变?典型场景
Provider只读值 / 依赖注入否配置、Repository、Service
StateProvider简单可变值是(整体替换)TabIndex、开关、筛选标记
NotifierProvider复杂可变状态(推荐主力)是(方法修改)购物车、表单、列表增删改
FutureProvider一次性异步结果否(可 refresh)读一篇文章、请求配置
StreamProvider持续异步流随流更新倒计时、WebSocket、传感器
AsyncNotifierProvider异步 + 可变(如分页列表)是带刷新/加载更多的列表

7.5.3 NotifierProvider:日常开发的主力(重点掌握)

// ① 写一个 Notifier 类:状态 + 修改状态的方法
class CartNotifier extends Notifier<int> {
  @override
  int build() => 0; // 初始状态

  void add() => state++;          // state 就是当前状态,直接改
  void reset() => state = 0;
}

// ② 挂到 NotifierProvider 上
final cartProvider = NotifierProvider<CartNotifier, int>(CartNotifier.new);

// ③ 界面里 watch + 调用方法
class CartPage extends ConsumerWidget {
  const CartPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(cartProvider);
    return Scaffold(
      body: Center(child: Text('购物车 $count 件')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(cartProvider.notifier).add(), // read.notifier 拿到对象调方法
        child: const Icon(Icons.add),
      ),
    );
  }
}
class Todo {
  const Todo({required this.title, this.done = false});
  final String title;
  final bool done;
  Todo copyWith({String? title, bool? done}) =>
      Todo(title: title ?? this.title, done: done ?? this.done);
}

// 不可变列表状态:每次修改都生成新 List(Riverpod 依赖 == 判断是否刷新)
class TodoNotifier extends Notifier<List<Todo>> {
  @override
  List<Todo> build() => [];

  void add(String title) => state = [...state, Todo(title: title)];

  void toggle(int index) => state = [
        for (var i = 0; i < state.length; i++)
          i == index ? state[i].copyWith(done: !state[i].done) : state[i],
      ];

  void remove(int index) => state = [...state]..removeAt(index);
}

final todoProvider = NotifierProvider<TodoNotifier, List<Todo>>(TodoNotifier.new);

class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoProvider);
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (_, i) => CheckboxListTile(
        value: todos[i].done,
        onChanged: (_) => ref.read(todoProvider.notifier).toggle(i),
        title: Text(todos[i].title,
            style: TextStyle(decoration: todos[i].done ? TextDecoration.lineThrough : null)),
        secondary: IconButton(
          icon: const Icon(Icons.delete_outline),
          onPressed: () => ref.read(todoProvider.notifier).remove(i),
        ),
      ),
    );
  }
}
💡 watch 与 read 的铁律 ref.watch(provider) 写在 build 方法体里——「我要盯着这个状态」;ref.read(provider.notifier).xxx() 写在回调函数体里——「我要执行一个动作」。在 build 里 read、在回调里 watch 都是新手常见反模式。

7.5.4 FutureProvider / StreamProvider:异步状态开箱即用

final userProvider = FutureProvider<Map<String, dynamic>>((ref) async {
  final resp = await http.get(Uri.parse('https://api.example.com/user/1'));
  return jsonDecode(resp.body);
});

class UserPage extends ConsumerWidget {
  const UserPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    return userAsync.when(          // AsyncValue 三态自动映射到 UI
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('加载失败: $e')),
      data: (user) => Text('欢迎,${user['name']}'),
    );
  }
}
// 手动刷新:ref.invalidate(userProvider);  或 ref.refresh(userProvider);

不需要自己写 loading 布尔标记、try-catch 三件套——AsyncValue.when 把「加载中/失败/成功」三种 UI 一网打尽,这是 Riverpod 最省心的地方。

7.5.5 Provider 组合与传参(family)

// Provider 可以依赖其他 Provider,自动建立依赖关系
final repoProvider = Provider((ref) => UserRepository());

// family:同一个 Provider 按参数缓存多份(如「按 id 请求详情」)
final detailProvider = FutureProvider.family<Detail, int>((ref, id) async {
  final repo = ref.watch(repoProvider); // 复用上面的依赖
  return repo.fetchDetail(id);
});
// 使用:ref.watch(detailProvider(42));

7.5.6 实用建议与常见坑

✅ Riverpod vs Provider vs Bloc vs GetX 怎么选? 新项目个人与中小团队:Riverpod(生态最活跃、官方文档最全);已用 Provider 的老项目继续用没问题;大型企业项目有严格事件驱动规范可考虑 Bloc;GetX 上手最快但「全局函数式」设计争议较大、不利于测试。本手册后续实战采用 Riverpod。
← 上一章第 7 章 状态管理(完整可运行示例)下一章 →第 8 章 网络请求与 JSON 解析(完整可运行示例)
✉ 联系作者:linghurongjun@sina.com