2.3 状态管理
📚 核心知识点
- 三种状态管理方式
- 如何选择合适的管理方式
- 父子组件通信
- 混合管理的应用场景
💡 核心概念
什么是状态管理?
状态(State) 是指在应用运行过程中可以改变的数据。
状态管理 就是决定:
- 状态应该存储在哪里?
- 谁来管理这个状态?
- 如何更新状态?
三种状态管理方式
graph TB
A[状态管理] --> B[Widget自己管理]
A --> C[父Widget管理]
A --> D[混合管理]
B --> B1[封装性好
适合UI效果] C --> C1[便于控制
适合用户数据] D --> D1[灵活性高
职责清晰] style A fill:#E1F5FE,stroke:#01579B style B fill:#E8F5E9,stroke:#2E7D32 style C fill:#FFF3E0,stroke:#E65100 style D fill:#F3E5F5,stroke:#4A148C
适合UI效果] C --> C1[便于控制
适合用户数据] D --> D1[灵活性高
职责清晰] style A fill:#E1F5FE,stroke:#01579B style B fill:#E8F5E9,stroke:#2E7D32 style C fill:#FFF3E0,stroke:#E65100 style D fill:#F3E5F5,stroke:#4A148C
🎯 方式1:Widget管理自己的状态
适用场景
✅ 状态是内部实现细节
- 动画进度
- 高亮效果
- 选中效果
- 展开/折叠状态
示例:TapboxA
scala
class TapboxA extends StatefulWidget {
@override
State<TapboxA> createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false; // ← 自己管理状态
void _handleTap() {
setState(() {
_active = !_active; // ← 自己更新状态
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
color: _active ? Colors.green : Colors.grey,
child: Text(_active ? 'Active' : 'Inactive'),
),
);
}
}
优点
✅ 封装性好 - 外部不需要知道内部状态 ✅ 可复用性强 - 可以直接在多处使用 ✅ 使用简单 - TapboxA() 即可,无需传参
缺点
❌ 外部无法访问或控制状态 ❌ 不适合需要共享的状态
🎯 方式2:父Widget管理子Widget的状态
适用场景
✅ 状态是用户数据
- 复选框的选中状态
- 滑块的位置
- 表单输入的值
- 需要外部控制的状态
示例:TapboxB
scala
// 父Widget
class ParentWidget extends StatefulWidget {
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false; // ← 父组件管理状态
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue; // ← 父组件更新状态
});
}
@override
Widget build(BuildContext context) {
return TapboxB(
active: _active, // ← 传递状态给子组件
onChanged: _handleTapboxChanged, // ← 传递回调
);
}
}
// 子Widget(无状态)
class TapboxB extends StatelessWidget {
final bool active;
final ValueChanged<bool> onChanged;
const TapboxB({required this.active, required this.onChanged});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!active), // ← 通知父组件改变状态
child: Container(
color: active ? Colors.green : Colors.grey,
child: Text(active ? 'Active' : 'Inactive'),
),
);
}
}
数据流向
sequenceDiagram
participant 父组件
participant 子组件
父组件->>子组件: 传递 active 状态
Note over 子组件: 用户点击
子组件->>父组件: 调用 onChanged(新值)
Note over 父组件: setState更新状态
父组件->>子组件: 传递新的 active 状态
Note over 子组件: UI更新
优点
✅ 父组件可以控制 - 方便统一管理 ✅ 适合表单场景 - 便于收集数据 ✅ 便于状态同步 - 多个子组件共享状态
缺点
❌ 子组件需要更多参数 ❌ 增加了父子组件的耦合
🎯 方式3:混合状态管理
适用场景
✅ 不同状态有不同的管理需求
- active(用户数据) → 父组件管理
- highlight(UI效果) → 子组件管理
示例:TapboxC
scala
// 父Widget - 管理active状态
class ParentWidgetC extends StatefulWidget {
@override
State<ParentWidgetC> createState() => _ParentWidgetCState();
}
class _ParentWidgetCState extends State<ParentWidgetC> {
bool _active = false; // ← 父组件管理:active状态
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
);
}
}
// 子Widget - 同时管理highlight状态
class TapboxC extends StatefulWidget {
final bool active;
final ValueChanged<bool> onChanged;
const TapboxC({required this.active, required this.onChanged});
@override
State<TapboxC> createState() => _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false; // ← 子组件自己管理:highlight状态
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true; // ← 按下时高亮
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false; // ← 抬起时取消高亮
});
}
void _handleTap() {
widget.onChanged(!widget.active); // ← 通知父组件改变active
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTap: _handleTap,
child: Container(
color: widget.active ? Colors.green : Colors.grey,
decoration: BoxDecoration(
border: _highlight ? Border.all(color: Colors.teal, width: 10) : null,
),
child: Text(widget.active ? 'Active' : 'Inactive'),
),
);
}
}
状态职责划分
| 状态 | 管理者 | 原因 |
|---|---|---|
active |
父组件 | 用户数据,需要外部访问 |
highlight |
子组件 | UI效果,内部实现细节 |
优点
✅ 职责清晰 - 各自管理适合的状态 ✅ 灵活性高 - 兼顾封装和控制 ✅ 性能优化 - highlight变化不影响父组件
📋 如何选择管理方式?
决策流程
flowchart TD
A["这个状态是什么?"] -->|UI效果| B["Widget自己管理"]
A -->|用户数据| C["需要外部访问吗?"]
C -->|需要| D["父Widget管理"]
C -->|不需要| B
A -->|混合情况| E["混合管理"]
style B fill:#E8F5E9,stroke:#2E7D32
style D fill:#FFF3E0,stroke:#E65100
style E fill:#F3E5F5,stroke:#4A148C
判断标准
| 问题 | 答案 | 建议方式 |
|---|---|---|
| 是UI效果(动画、高亮)? | 是 | Widget自己管理 |
| 是用户数据(选中、输入)? | 是 | 父Widget管理 |
| 需要多个Widget共享? | 是 | 父Widget管理 |
| 外部需要控制这个状态? | 是 | 父Widget管理 |
| 有多种状态需求不同? | 是 | 混合管理 |
常见场景示例
less
// ✅ Widget自己管理
AnimatedContainer(...) // 动画状态
ExpansionTile(...) // 展开/折叠状态
// ✅ 父Widget管理
Checkbox(value: _checked, onChanged: ...) // 选中状态
Slider(value: _volume, onChanged: ...) // 滑块值
TextField(controller: _controller) // 输入内容
// ✅ 混合管理
// 按钮的pressed状态自己管理,但enabled状态由父组件管理
ElevatedButton(
onPressed: _isEnabled ? _handleTap : null, // 父组件控制
// 按压效果由按钮内部管理
child: Text('按钮'),
)
🌍 全局状态管理
什么时候需要全局状态管理?
当状态需要在多个不相关的Widget之间共享时:
- 用户登录状态
- 主题设置
- 语言设置
- 购物车数据
两种方案
1. 事件总线(Event Bus)
scss
// 发布事件
eventBus.fire(LanguageChangedEvent('zh-CN'));
// 订阅事件
eventBus.on<LanguageChangedEvent>().listen((event) {
setState(() {
// 更新UI
});
});
2. 状态管理库
常用库:
- Provider - Flutter官方推荐
- Riverpod - Provider的改进版
- Bloc - 基于Stream的状态管理
- GetX - 轻量级状态管理
Provider示例:
scala
// 1. 定义状态类
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 通知监听者
}
}
// 2. 提供状态
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
)
// 3. 使用状态
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('${counter.count}');
},
)
对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 事件总线 | 简单、解耦 | 难以调试、类型不安全 |
| Provider | 官方推荐、类型安全 | 需要学习 |
| Bloc | 架构清晰、可测试 | 代码量大 |
| GetX | 简单易用 | 不够规范 |
🎯 核心总结
- Widget自己管理 - UI效果、内部细节
- 父Widget管理 - 用户数据、需要控制
- 混合管理 - 不同状态不同需求
- 全局管理 - 跨组件共享状态(Provider等)
选择原则:
- 用户数据 → 父管理
- UI效果 → 自己管理
- 共享状态 → 共同父管理
📝 常见问题
Q1: 为什么不能直接修改状态,必须用setState?
A:
scss
// ❌ 错误:直接修改
_count++; // 状态改了,但UI不更新
// ✅ 正确:使用setState
setState(() {
_count++; // Flutter知道要更新UI
});
原理: setState()告诉Flutter框架状态变了,需要重新build UI。
Q2: 父子组件通信的ValueChanged是什么?
A:
dart
typedef ValueChanged<T> = void Function(T value);
它是一个回调函数类型,用于从子组件向父组件传递值。
scss
// 父组件
void _handleChanged(bool newValue) {
setState(() => _active = newValue);
}
// 传给子组件
TapboxB(onChanged: _handleChanged)
// 子组件调用
widget.onChanged(true); // 触发父组件的_handleChanged(true)
Q3: 混合管理中,子组件如何访问父组件传来的状态?
A: 使用 widget.xxx:
scala
class _TapboxCState extends State<TapboxC> {
@override
Widget build(BuildContext context) {
// ✅ 通过widget访问父组件传来的属性
return Container(
color: widget.active ? Colors.green : Colors.grey,
);
}
}
原理: State对象持有Widget的引用,可以通过widget访问。
Q4: 什么时候应该用全局状态管理?
A:
需要:
- 登录状态(多个页面都要用)
- 主题/语言(全局生效)
- 购物车(跨页面共享)
不需要:
- 表单输入(局部数据)
- 按钮按压(临时效果)
- 列表滚动位置(组件内部)
🎓 跟着做练习
练习1:改进计数器 ⭐
目标: 把2.1的计数器改成父组件管理状态
提示:
- 创建父组件管理
_counter - 创建子组件显示计数
- 用回调函数通知父组件
scala
// 父组件
class ParentCounter extends StatefulWidget {
@override
State<ParentCounter> createState() => _ParentCounterState();
}
class _ParentCounterState extends State<ParentCounter> {
int _counter = 0;
void _increment() {
setState(() => _counter++);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('计数:$_counter'),
CounterButton(onPressed: _increment),
],
);
}
}
// 子组件
class CounterButton extends StatelessWidget {
final VoidCallback onPressed;
const CounterButton({required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text('+1'),
);
}
}
练习2:创建一个开关组件 ⭐⭐
目标: 创建一个自己管理状态的开关组件
要求:
- 点击切换开/关
- 开:绿色,显示"ON"
- 关:灰色,显示"OFF"
scala
class MySwitch extends StatefulWidget {
@override
State<MySwitch> createState() => _MySwitchState();
}
class _MySwitchState extends State<MySwitch> {
bool _isOn = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isOn = !_isOn;
});
},
child: Container(
width: 60,
height: 30,
decoration: BoxDecoration(
color: _isOn ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
_isOn ? 'ON' : 'OFF',
style: TextStyle(color: Colors.white),
),
),
),
);
}
}
练习3:购物车数量选择器 ⭐⭐⭐
目标: 创建一个商品数量选择器(混合管理)
要求:
- 数量(count)由父组件管理
- 按钮按压高亮由子组件管理
- 有 + 和 - 按钮
scala
// 父组件
class ShoppingCart extends StatefulWidget {
@override
State<ShoppingCart> createState() => _ShoppingCartState();
}
class _ShoppingCartState extends State<ShoppingCart> {
int _count = 1;
@override
Widget build(BuildContext context) {
return QuantitySelector(
count: _count,
onChanged: (newCount) {
setState(() {
_count = newCount.clamp(0, 99); // 限制范围
});
},
);
}
}
// 子组件
class QuantitySelector extends StatefulWidget {
final int count;
final ValueChanged<int> onChanged;
const QuantitySelector({required this.count, required this.onChanged});
@override
State<QuantitySelector> createState() => _QuantitySelectorState();
}
class _QuantitySelectorState extends State<QuantitySelector> {
bool _minusHighlight = false;
bool _plusHighlight = false;
@override
Widget build(BuildContext context) {
return Row(
children: [
GestureDetector(
onTapDown: (_) => setState(() => _minusHighlight = true),
onTapUp: (_) => setState(() => _minusHighlight = false),
onTapCancel: () => setState(() => _minusHighlight = false),
onTap: () => widget.onChanged(widget.count - 1),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _minusHighlight ? Colors.red[300] : Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.remove, color: Colors.white),
),
),
Container(
width: 60,
alignment: Alignment.center,
child: Text('${widget.count}', style: TextStyle(fontSize: 20)),
),
GestureDetector(
onTapDown: (_) => setState(() => _plusHighlight = true),
onTapUp: (_) => setState(() => _plusHighlight = false),
onTapCancel: () => setState(() => _plusHighlight = false),
onTap: () => widget.onChanged(widget.count + 1),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _plusHighlight ? Colors.green[300] : Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.add, color: Colors.white),
),
),
],
);
}
}