前言
在上一篇文章中,我们给 Birdle 游戏添加了输入框和提交按钮,玩家已经可以输入猜测的单词了。但有一个问题------输入完按回车后,棋盘上什么都没变。单词虽然被提交了(在控制台能看到打印),但界面纹丝不动。
这是因为到目前为止,我们所有的组件都是 StatelessWidget(无状态组件)。它们一旦创建就"定型了",不会自动更新。
今天这篇文章基于官方教程的「Stateful Widgets」章节,我们将学习 Flutter 中最关键的概念之一------StatefulWidget (有状态组件)和 setState。学完之后,Birdle 游戏就真正能玩起来了!
一、为什么需要 StatefulWidget?
1.1 回顾 StatelessWidget 的局限
StatelessWidget 就像一张打印好的照片------内容在创建时就确定了,之后不会再变。这对于显示固定内容(比如标题、图标)来说足够了。
但游戏棋盘不是固定的。每当玩家提交一个猜测,棋盘就需要更新:显示猜测的字母,并用绿色、黄色、灰色标记对错。这种需要在运行过程中改变外观或数据 的场景,就需要用到 StatefulWidget。
1.2 StatefulWidget 的工作原理
StatefulWidget 由两个类组成:
- Widget 类本身:和 StatelessWidget 一样,它是不可变的(immutable)。你可以理解为"外壳"。
- State 类:一个长期存在的伴侣对象,保存着可变的数据。当数据改变时,它能触发界面重新构建。
打个比方:StatefulWidget 就像一块白板 。白板本身(Widget)不会变,但白板上写的内容(State)可以随时擦掉重写。每次内容变了,Flutter 就会重新"拍一张照"(调用 build 方法),把最新的内容展示在屏幕上。
1.3 基本结构预览
scala
// StatefulWidget 的标准写法由两个类组成:
// 第一个类:Widget 本身(不可变的"外壳")
class ExampleWidget extends StatefulWidget {
ExampleWidget({super.key});
// createState() 方法创建并返回对应的 State 对象
@override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
// 第二个类:State 对象(保存可变数据 + build 方法)
// 命名惯例:_WidgetName + State,前面加下划线表示私有
class _ExampleWidgetState extends State<ExampleWidget> {
// 可变数据放在这里
// ...
// build 方法从 Widget 类移到了 State 类中
@override
Widget build(BuildContext context) {
return Container();
}
}
二、将 GamePage 转换为 StatefulWidget
2.1 为什么是 GamePage?
回想一下我们的组件结构:GamePage 持有 Game 对象,而 Game 对象保存着所有猜测记录。每次玩家提交猜测,Game 的数据就会改变,棋盘就需要重新绘制。所以 GamePage 是需要"变成有状态"的那个组件。
而 Tile 和 GuessInput 不需要改------它们只是接收数据并展示,本身不管理任何会变化的数据。
2.2 转换步骤
转换过程分为三步。VS Code 的 Flutter 插件提供了"快速辅助"功能,可以一键完成转换(光标放在类名上,按 Ctrl + .,选择"Convert to StatefulWidget")。但为了理解原理,我们手动来做一次。
第一步 :把 GamePage 的父类从 StatelessWidget 改为 StatefulWidget,并添加 createState 方法。
第二步 :创建 _GamePageState 类,继承 State<GamePage>。
第三步 :把 build 方法和所有可变属性从 GamePage 移到 _GamePageState 中。
转换后的代码:
scala
// ===== 转换前(StatelessWidget)=====
// class GamePage extends StatelessWidget {
// GamePage({super.key});
// final Game _game = Game();
// @override
// Widget build(BuildContext context) { ... }
// }
// ===== 转换后(StatefulWidget)=====
// 第一个类:GamePage 本身变成了一个轻量的"外壳"
// 只负责创建 State 对象,不再包含 build 方法
class GamePage extends StatefulWidget {
GamePage({super.key});
// createState() 创建并返回 _GamePageState 实例
// Flutter 内部会调用这个方法来获取 State 对象
@override
State<GamePage> createState() => _GamePageState();
}
// 第二个类:_GamePageState 保存可变数据和 build 方法
// 下划线 _ 开头表示这个类是私有的,只在当前文件中可见
class _GamePageState extends State<GamePage> {
// _game 被移到了 State 类中
// 因为它保存着可变的游戏数据(猜测记录等)
final Game _game = Game();
// build 方法也移到了 State 类中
// 每次调用 setState 后,Flutter 会重新调用这个方法
@override
Widget build(BuildContext context) {
// ... 界面代码(下一节补充完整)
}
}
三、用 setState 触发界面更新
3.1 setState 是什么?
setState 是 State 类中最重要的方法。它的作用是:
- 执行数据修改(在传入的函数中修改状态数据)
- 通知 Flutter:"我的数据变了,请重新调用 build 方法,重绘界面"
如果你修改了数据但没有调用 setState,数据确实会变,但 Flutter 不知道需要重绘,用户在屏幕上看不到任何变化。
3.2 在 GamePage 中使用 setState
现在把 onSubmitGuess 回调中的 print 替换为真正的游戏逻辑:
scss
GuessInput(
onSubmitGuess: (String guess) {
// setState 告诉 Flutter:"我要修改数据了,改完请重绘界面"
setState(() {
// 在 setState 内部修改游戏状态
// _game.guess() 会:
// 1. 把玩家猜的单词与目标单词逐字母比较
// 2. 给每个字母标记 hit/partial/miss
// 3. 把结果保存到 _game.guesses 列表中
_game.guess(guess);
});
// setState 执行完毕后,Flutter 会自动重新调用 build 方法
// build 方法中的 for 循环会遍历更新后的 _game.guesses
// 棋盘上就会显示出玩家的猜测和颜色标记
},
),
3.3 数据流动的完整过程
让我们梳理一下从用户输入到界面更新的完整流程:
erlang
用户输入 "abbey" → 按回车
↓
GuessInput._onSubmit() 被调用
↓
onSubmitGuess("abbey") 回调触发
↓
setState(() { _game.guess("abbey"); })
├── _game.guess("abbey") → 数据更新
│ ├── 字母 a: hit(位置和字母都对)→ 绿色
│ ├── 字母 b: partial(字母对,位置不对)→ 黄色
│ ├── 字母 b: miss(多余的 b)→ 灰色
│ ├── 字母 e: miss → 灰色
│ └── 字母 y: miss → 灰色
└── Flutter 收到通知 → 重新调用 build()
↓
build() 重新执行,遍历更新后的 _game.guesses
↓
棋盘第一行的 Tile 更新:显示字母和颜色
四、StatelessWidget vs StatefulWidget 对比
| 特性 | StatelessWidget | StatefulWidget |
|---|---|---|
| 数据是否可变 | 不可变,创建后固定 | State 对象中的数据可变 |
| 是否能自动更新 UI | 不能 | 调用 setState 后自动重绘 |
| 类的数量 | 1 个类 | 2 个类(Widget + State) |
| build 方法位置 | 在 Widget 类中 | 在 State 类中 |
| 适用场景 | 静态展示(标题、图标、固定布局) | 需要响应交互或数据变化的界面 |
| 生命周期 | 短暂,随时可能被重建 | State 对象长期存在 |
| 本系列中的例子 | Tile、GuessInput、MainApp | GamePage(本篇转换) |
简单的判断规则:如果一个组件的内容在创建后永远不需要变化 ,用 StatelessWidget;如果它的内容可能在运行过程中改变,用 StatefulWidget。
五、常见误区
5.1 不要忘记调用 setState
scss
// ❌ 错误:修改了数据,但没有调用 setState
// 数据确实变了,但 Flutter 不知道,界面不会更新
onSubmitGuess: (String guess) {
_game.guess(guess); // 数据变了,但界面没动
},
// ✅ 正确:用 setState 包裹数据修改
// Flutter 会在 setState 执行完后重新调用 build
onSubmitGuess: (String guess) {
setState(() {
_game.guess(guess); // 数据变了,界面也跟着更新
});
},
5.2 不要把所有组件都变成 StatefulWidget
只有真正需要管理可变数据 的组件才需要是 StatefulWidget。在我们的应用中,只有 GamePage 需要转换,因为它持有会变化的 Game 对象。Tile 和 GuessInput 保持 StatelessWidget 就好------它们只是接收数据并展示。
5.3 VS Code 快速转换技巧
不需要手动做上面的转换工作!VS Code 提供了快捷操作:
markdown
1. 把光标放在 StatelessWidget 类名上
2. 按 Ctrl + .(或 Cmd + . on Mac)
3. 选择 "Convert to StatefulWidget"
4. VS Code 自动完成所有修改
六、本节知识点小结
StatefulWidget: 当组件的外观或数据需要在运行过程中改变时使用。由两个类组成------Widget 类(不可变外壳)和 State 类(保存可变数据 + build 方法)。
State 类: StatefulWidget 的伴侣对象,长期存在。通过 createState() 方法创建。可变数据和 build 方法都放在这里。
setState: State 类中最重要的方法。在回调中修改数据时必须用 setState 包裹,它会通知 Flutter 重新调用 build 方法来更新界面。忘记调用 setState 是最常见的新手错误。
转换方法: 将 StatelessWidget 转换为 StatefulWidget 需要三步------改父类、创建 State 类、移动 build 方法和可变属性。VS Code 可以一键完成。
七、下一步学习
Birdle 游戏现在已经可以完整运行了!玩家输入单词、提交猜测、棋盘实时更新颜色。下一课我们将学习隐式动画(Implicit Animations),给方块的颜色变化添加平滑的过渡效果,让游戏体验更加丝滑。
我们下篇文章见!