状态管理
Provider/Riverpod/BLoC。
状态管理
这一课你将能做到
- 用
setState 管理单个 Widget 内部状态,并清楚它的边界
- 用 Provider + ChangeNotifier 在多个页面之间共享状态
- 用 Riverpod 写现代化的、可测试的状态管理,并理解状态提升
为什么要学
setState 在 1-2 个页面就够用,一旦跨页面共享数据(购物车、登录用户、主题),就必须用专门的状态管理方案。不会状态管理 = 只能写 demo。
人话:setState 是「家里的便签」,Provider/Riverpod 是「公司内部的 OA 系统」。便签贴自己屏幕上行,要发给同事就得走 OA。
先建立图像
┌──────────────────┐
│ ChangeNotifier │ ← 数据 + 通知
└────────▲─────────┘
│ notifyListeners()
│
UI 层 │ 动作层
┌──────────────┐ │ ┌──────────────────┐
│ Consumer/ │ ←─────────┘ │ 按钮 onPressed │
│ context.watch│ 自动 rebuild │ notifier.add() │
└──────────────┘ └──────────────────┘
数据单向流动:事件 → 修改 Model → Model 通知 → UI 重建。
跟着做
步骤 1:setState 的边界
setState 适合:单个 Widget 内部的、不需要跨页面共享的状态。例如表单输入、Tab 选中:
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
String _email = '';
bool _obscure = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: (v) => setState(() => _email = v)),
TextField(
obscureText: _obscure,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscure = !_obscure),
),
),
),
Text('当前输入:$_email'),
],
);
}
}
红线:跨页面共享、复杂业务状态、需要持久化——不要用 setState。
步骤 2:InheritedWidget 是底层
Flutter 内置 InheritedWidget,让子孙 Widget 通过 context.dependOnInheritedWidgetOfExactType 访问数据并自动 rebuild。Provider 就是对它的友好封装。理解即可,不要手写。
class Counter extends InheritedWidget {
final int value;
const Counter({super.key, required this.value, required super.child});
static Counter of(BuildContext ctx) =>
ctx.dependOnInheritedWidgetOfExactType<Counter>()!;
@override
bool updateShouldNotify(Counter old) => value != old.value;
}
步骤 3:Provider 实战
dependencies:
provider: ^6.1.0
import 'package:flutter/foundation.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners(); // 通知所有监听者 rebuild
}
void remove(String item) {
_items.remove(item);
notifyListeners();
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) =>
const MaterialApp(home: ShopPage());
}
class ShopPage extends StatelessWidget {
const ShopPage({super.key});
@override
Widget build(BuildContext context) {
// 三种读法
final cart = context.watch<CartModel>(); // 订阅 + rebuild
// final cart = context.read<CartModel>(); // 只读一次,不订阅
// final count = context.select<CartModel, int>((c) => c.count); // 精准订阅
return Scaffold(
appBar: AppBar(title: Text('购物车 ${cart.count}')),
body: ListView(
children: [
for (var p in ['苹果', '香蕉', '橙子'])
ListTile(
title: Text(p),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () => context.read<CartModel>().add(p),
),
),
const Divider(),
for (var item in cart.items) ListTile(title: Text('已选: $item')),
],
),
);
}
}
核心规则:事件回调里用 context.read,build 里要响应数据用 context.watch 或 Consumer。
步骤 4:Consumer 精准 rebuild
// 只有这个小块会 rebuild
Consumer<CartModel>(
builder: (ctx, cart, child) => Text('数量: ${cart.count}'),
)
context.watch 会让整个所在的 Widget rebuild;Consumer 把范围圈小。
步骤 5:Riverpod(新一代推荐)
dependencies:
flutter_riverpod: ^2.5.0
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. 定义 Provider
final counterProvider = StateProvider<int>((ref) => 0);
void main() => runApp(const ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) =>
const MaterialApp(home: CounterPage());
}
// 2. 用 ConsumerWidget
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count', style: const TextStyle(fontSize: 48))),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
- 编译期安全:不存在 ProviderNotFoundException
- 可覆盖:测试时
ProviderScope(overrides: [...]) 一行替换实现
- 解耦:Provider 不依赖 BuildContext,纯 Dart 测试友好
步骤 6:状态提升
兄弟 Widget 共享状态 → 把状态提升到最近的公共祖先:
class Parent extends StatefulWidget {
const Parent({super.key});
@override
State<Parent> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
bool _on = false;
@override
Widget build(BuildContext context) => Column(children: [
Switch(value: _on, onChanged: (v) => setState(() => _on = v)),
Lamp(on: _on),
]);
}
class Lamp extends StatelessWidget {
final bool on;
const Lamp({super.key, required this.on});
@override
Widget build(BuildContext context) =>
Icon(Icons.lightbulb, color: on ? Colors.yellow : Colors.grey, size: 64);
}
状态提升是状态管理的根本思想,Provider/Riverpod 只是把「公共祖先」搬到了 App 顶层。
讲清楚要点
- setState 边界:单 Widget、临时状态、不涉及业务。
- ChangeNotifier + Provider:经典方案,简单好懂,文档丰富。
- Riverpod:编译安全 + 测试友好,新项目首选。
- BLoC:把事件和状态分离成 Stream,大型项目可读性高但样板代码多。
- 状态提升:共享状态放公共祖先;状态管理中所有方案都是这个思想的扩展。
你可能会懵的地方
-
Provider 和 Riverpod 是同一个作者?
是,都是 Remi Rousselet。Riverpod 是 Provider 的「重写版」,解决了 Provider 依赖 BuildContext 的问题。
-
watch vs read 怎么选?
build 方法里要 UI 跟随数据变化 → watch;事件回调里调用方法 → read。在 build 里用 read 不会触发更新。
-
ChangeNotifier 里的字段要不要加 final?
可变状态字段不要 final(要改),但对外的 getter 要返回不可修改的视图(如 List.unmodifiable),避免外部绕开 notifyListeners 直接改。
-
为什么不直接用全局变量?
全局变量没有通知机制,UI 不知道何时刷新;测试时也无法隔离。
-
Riverpod 的 ref.watch 能写在 build 之外吗?
不能。ref.watch 只能在 build 中调用,事件回调里用 ref.read。
错法对照
| 错法 | 问题 | 正解 |
|---|
在 onPressed 里 context.watch | 编译期不报错但触发异常 | 事件回调用 context.read |
notifyListeners() 之后修改字段 | UI 拿到旧值 | 先改字段再 notify |
| 把 ChangeNotifier 字段直接暴露成可变 List | 外部绕过 notify 直接改 | getter 返回 List.unmodifiable |
顶层只用 Provider.value 包已存在对象 | 无法 dispose,内存泄漏 | 用 ChangeNotifierProvider(create: ...) 让 Provider 管理生命周期 |
在 Riverpod 的 build 里 ref.read 又 watch 字段 | 数据不刷新 | build 里只用 ref.watch |
卡点急救
- ProviderNotFoundException:Provider 没在上层注册,或读的 context 来自注册点之上。检查
ChangeNotifierProvider 位置是否包住使用点。
- watch 后 UI 不刷新:字段是不是改了?
notifyListeners() 调了吗?返回的对象是不是同一个引用?考虑返回新对象。
- Riverpod
Bad state: Tried to read ... from a place other than build:在事件回调里写了 ref.watch,改成 ref.read。
- 内存泄漏:AnimationController/StreamController 放 ChangeNotifier 里时,重写
dispose() 里 dispose 掉。
面试常问
-
setState 的局限?
只能影响当前 Widget 子树;状态无法跨页面共享;业务逻辑混在 UI 里难测试。
-
Provider 的原理?
基于 InheritedWidget:通过 context.dependOnInheritedWidgetOfExactType 建立依赖,数据变化时 InheritedWidget 的 updateShouldNotify 触发依赖方 rebuild。
-
Riverpod 相比 Provider 解决了什么?
去掉了对 BuildContext 的依赖(编译期安全);支持 override 方便测试;可组合(一个 Provider 依赖另一个);不依赖 Flutter 也可在纯 Dart 使用。
-
BLoC 和 Provider 怎么选?
项目小、团队新 → Provider/Riverpod;事件流复杂、需要清晰的事件-状态映射、团队大 → BLoC。
自测三问
- 为什么
onPressed: () => context.watch<CartModel>().add('x') 是错的?应该怎么写?
- 写一个 ChangeNotifier 暴露
count,并用 Provider 在两个页面之间共享。
- Riverpod 的
ref.watch 和 ref.read 区别?各自的使用场景?
小结一句话
小范围用 setState,跨页面用 Provider,新项目用 Riverpod;核心思想是「状态提升 + 单向数据流」。
延伸