2.1 计数器应用示例
📚 核心知识点
- Flutter应用的基本结构
- StatefulWidget 和 State 的使用
- Widget树的构建过程
- setState() 状态管理机制
💡 核心概念
1. Widget是什么?
Widget是Flutter中UI的基本构建块,理解为"UI的描述"。
关键特点:
- 不可变(immutable) :创建后不能修改
- 轻量级:只是配置信息,不是真正的UI元素
- 可复用:同一个Widget可以在多处使用
类比理解:
Widget = 建筑图纸
Element = 施工队(负责实际建造)
RenderObject = 真正的建筑物
2. StatelessWidget vs StatefulWidget
| 特性 | StatelessWidget | StatefulWidget |
|---|---|---|
| 状态 | 无状态 | 有状态 |
| 重建 | 不会重建 | 可以多次重建 |
| 适用场景 | 静态UI | 动态UI |
| 示例 | Text, Icon | Checkbox, TextField |
判断标准:
- 需要改变UI → StatefulWidget
- 不需要改变UI → StatelessWidget
3. State生命周期
stateDiagram-v2
[*] --> createState: Widget创建
createState --> initState: State初始化
initState --> build: 首次构建UI
build --> mounted: 等待交互
mounted --> setState: 状态改变
setState --> build: 重新构建UI
build --> mounted: 更新完成
mounted --> dispose: Widget销毁
dispose --> [*]: 清理完成
关键方法:
createState()- 创建State对象(只调用一次)initState()- 初始化数据、订阅事件(只调用一次)build()- 构建UI(每次状态改变都执行)dispose()- 清理资源(只调用一次)
4. setState() 工作原理
void _incrementCounter() {
setState(() {
_counter++; // 1. 修改状态
});
// 2. 标记widget为dirty
// 3. 在下一帧调用build()
// 4. 使用diff算法更新UI
}
为什么必须用setState()?
// ❌ 错误:UI不会更新
void _incrementCounter() {
_counter++; // 只修改了数据,Flutter不知道要更新UI
}
// ✅ 正确:UI会更新
void _incrementCounter() {
setState(() {
_counter++; // Flutter知道状态改变了,会触发UI更新
});
}
5. setState()执行流程
sequenceDiagram
participant User as 用户
participant Widget as Widget
participant State as State
participant Flutter as Flutter框架
participant UI as UI界面
User->>Widget: 点击按钮
Widget->>State: 触发 _incrementCounter()
rect rgb(255, 243, 224)
Note over State: setState() 开始
State->>State: 1. 执行回调函数
State->>State: 2. _counter++
State->>Flutter: 3. 标记为dirty
end
Flutter->>Flutter: 等待下一帧
Flutter->>State: 调用 build()
State->>Flutter: 返回新Widget树
rect rgb(232, 245, 233)
Note over Flutter: Diff算法
Flutter->>Flutter: 对比新旧树
Flutter->>Flutter: 找出差异
end
Flutter->>UI: 只更新变化部分
UI->>User: 显示新的计数器值
6. 为什么build()在State中?
原因1:状态访问方便
// 如果build在StatefulWidget中:
Widget build(BuildContext context, State state) {
return Text('${state.counter}'); // ❌ 需要公开状态,破坏封装
}
// build在State中:
Widget build(BuildContext context) {
return Text('$_counter'); // ✅ 直接访问私有状态
}
原因2:继承灵活性
如果build在StatefulWidget中,子类需要访问父类的State,会导致复杂的状态传递机制。build在State中,子类只关注自己的逻辑。
🎨 Widget树结构
计数器应用的Widget树:
graph TD
A[MaterialApp] --> B[MyHomePage<br/>StatefulWidget]
B --> C[_MyHomePageState<br/>State]
C --> D[Scaffold]
D --> E[AppBar]
D --> F[body: Center]
D --> G[FloatingActionButton]
E --> E1[Text 标题]
F --> F1[Column]
F1 --> F11[Text 提示]
F1 --> F12[Text 计数]
F1 --> F13[Padding]
G --> G1[Icon +]
style A fill:#E1F5FE,stroke:#01579B
style B fill:#F3E5F5,stroke:#4A148C
style C fill:#FCE4EC,stroke:#880E4F
style D fill:#FFF3E0,stroke:#E65100
🔍 Flutter声明式UI
核心理念: UI = f(State)
graph TB
A[State 状态] -->|f 函数| B[UI 界面]
B -->|用户交互| C[事件触发]
C -->|修改| A
style A fill:#4CAF50,stroke:#2E7D32,stroke-width:3px,color:#fff
style B fill:#2196F3,stroke:#1565C0,stroke-width:3px,color:#fff
style C fill:#FF9800,stroke:#E65100,stroke-width:3px,color:#fff
状态改变驱动UI更新,而不是手动操作DOM。
📝 常见问题
Q1: StatefulWidget和State为什么要分开?
A:
- StatefulWidget是配置:不可变,可以频繁重建
- State是状态持有者:可变,在重建过程中保持不变
- 好处:配置和状态分离,性能更好,逻辑更清晰
Q2: setState()能在build()方法中调用吗?
A: 不能!会导致无限循环:
Widget build(BuildContext context) {
setState(() {}); // ❌ 错误!
// build → setState → build → setState → ...
return Container();
}
Q3: 可以不用setState()直接修改状态吗?
A: 可以修改变量值,但UI不会更新:
void _increment() {
_counter++; // ✅ 变量值改了
// ❌ 但屏幕上的数字不会变
}
原理: Flutter不会主动检测状态变化,必须通过setState()告诉框架。
Q4: 什么时候用StatelessWidget?
A:
Text('Hello') // ← StatelessWidget(文本不变)
Checkbox(value: _flag) // ← StatefulWidget(选中状态会变)
🎯 核心总结
- Widget - UI的描述,不可变、轻量级
- StatelessWidget - 无状态,build一次就不变
- StatefulWidget - 有状态,可以多次rebuild
- State - 可变状态的持有者,在widget重建时保持不变
- setState() - 通知框架状态改变,触发UI更新
- build()在State中 - 方便访问状态、保持封装、灵活继承
🎓 跟着做练习
练习1:修改初始值 ⭐
目标: 将计数器的初始值改为 10
步骤:
- 找到
_MyHomePageState类中的_counter变量 - 把
int _counter = 0;改为int _counter = 10; - 运行查看效果
完整代码:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 10; // ← 改这里
void _incrementCounter() {
setState(() {
_counter++;
});
}
// ... 其他代码不变
}
练习2:添加减少按钮 ⭐⭐
目标: 在页面上添加一个减少按钮,点击后计数器减1
步骤:
- 先写一个减少的方法(仿照
_incrementCounter) - 在
body中添加一个按钮
完整代码:
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
// ← 新增:减少方法
void _decrementCounter() {
setState(() {
_counter--;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('你已经点击按钮这么多次:'),
Text('$_counter', style: Theme.of(context).textTheme.headlineLarge),
const SizedBox(height: 20),
// ← 新增:两个按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 减少按钮
FloatingActionButton(
onPressed: _decrementCounter,
tooltip: '减少',
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
// 增加按钮
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '增加',
child: const Icon(Icons.add),
),
],
),
],
),
),
// ← 删除原来的 floatingActionButton(因为已经放到 body 里了)
);
}
}
练习3:添加重置按钮 ⭐⭐
目标: 在 AppBar 右侧添加一个重置按钮,点击后归零
步骤:
- 写一个重置方法
- 在 AppBar 的
actions里添加按钮
完整代码:
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
// ← 新增:重置方法
void _resetCounter() {
setState(() {
_counter = 0;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
centerTitle: true,
title: const Text('2.1 计数器应用示例'),
// ← 新增:右侧按钮
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: '重置',
onPressed: _resetCounter,
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('你已经点击按钮这么多次:'),
Text('$_counter', style: Theme.of(context).textTheme.headlineLarge),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '增加',
child: const Icon(Icons.add),
),
);
}
}
练习4:数字颜色随奇偶变化 ⭐⭐⭐
目标: 偶数显示蓝色,奇数显示红色
关键知识:
- 奇偶判断:
_counter % 2 == 0(偶数为true) - 三元运算符:
条件 ? 值1 : 值2
完整代码:
Text(
'$_counter',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
// ← 关键代码:根据奇偶设置颜色
color: _counter % 2 == 0 ? Colors.blue : Colors.red,
),
),
练习5:修改主题颜色 ⭐
目标: 将应用主题色改为绿色
步骤: 找到 CounterApp 中的 seedColor,改为 Colors.green
完整代码:
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 计数器示例',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green, // ← 改这里
),
useMaterial3: true,
),
home: const CounterPage(),
);
}
}
💡 练习建议
- 一个一个来:先完成练习1,运行看效果,再做练习2
- 对照代码:把答案和原代码对比,看看改了哪里
- 理解原理:不要只复制粘贴,想想为什么这么写
- 多次运行:每次修改后都运行一下,立即看到效果
- 尝试变化:完成后可以试试改变颜色、图标等