Flutter StatefulWidget让界面动起来(六)

前言

在上一篇文章中,我们给 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 是需要"变成有状态"的那个组件。

TileGuessInput 不需要改------它们只是接收数据并展示,本身不管理任何会变化的数据。

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 类中最重要的方法。它的作用是:

  1. 执行数据修改(在传入的函数中修改状态数据)
  2. 通知 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),给方块的颜色变化添加平滑的过渡效果,让游戏体验更加丝滑。

我们下篇文章见!

参考资料:Flutter 官方教程 - Stateful Widgets

相关推荐
a1117764 分钟前
堆叠式流程图编辑器(html 开源)
开发语言·前端·javascript·开源·编辑器·html·流程图
墨渊君5 分钟前
前端工程化进阶:Monorepos 架构简析(水文)
前端
兆子龙5 分钟前
前端必学:完美组件封装的 7 个原则
前端·javascript
兆子龙6 分钟前
React 性能坑:别让 AI 踩了,快来添加 rule 吧
前端·javascript
光影少年7 分钟前
Vue的生命周期有哪些及执行机制?
前端·vue.js·掘金·金石计划
来碗疙瘩汤8 分钟前
Vue 事件绑定完全指南:官方文档未详述的事件大全
前端·javascript·vue.js
天涯学馆9 分钟前
从 V8 引擎看 JS 代码是如何一步步变成机器指令的
前端·javascript·面试
Elaine33610 分钟前
【通过 Vue 实例劫持突破 Web 编辑器的粘贴限制】
前端·javascript·vue.js·chrome devtools·前端逆向
哔哩哔哩技术10 分钟前
从“截图大法”到真实交互:B站专栏视频卡的技术革命
前端
程序员讲BPM工作流16 分钟前
npm非全局方式安装小龙虾OpenClaw
前端·npm·node.js