前言
在前两篇文章中,我们创建了 Flutter 项目,还学会了自定义一个 Tile 组件。但一个孤零零的方块并不能构成游戏界面——我们需要把 25 个方块整整齐齐地排成 5 行 5 列的网格。
今天这篇文章基于官方教程的「Layout」章节,我们将学习 Flutter 中最核心的布局知识:如何用 Scaffold、AppBar、Column 和 Row 来组织界面结构。学完之后,你就能搭建出 Birdle 猜词游戏的完整棋盘了。
一、用 Scaffold 和 AppBar 搭建页面骨架
1.1 什么是 Scaffold?
打开任何一个手机 App,你会发现它们的页面结构通常都很类似:顶部有一个标题栏,中间是主要内容,底部可能有导航栏。
Scaffold 就是 Flutter 提供的一个"页面脚手架",它帮你把这些常见的页面区域预先划分好了。你只需要往对应的"槽位"里填内容就行。
你可以把 Scaffold 想象成一套精装修的毛坯房——墙壁、地板、天花板都做好了,你只需要摆放家具。
1.2 什么是 AppBar?
AppBar 就是页面顶部的标题栏。它通常用来显示应用名称、返回按钮或设置按钮等。
1.3 给 Birdle 添加标题栏
修改 MainApp 的 build 方法,添加 AppBar:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
// MaterialApp 是整个应用的根组件,提供主题、路由等基础能力
return MaterialApp(
// Scaffold 提供了标准的页面结构:标题栏 + 内容区
home: Scaffold(
// appBar 属性用来设置页面顶部的标题栏
appBar: AppBar(
// Align 组件控制子组件的对齐方式
// centerLeft 表示靠左居中(AppBar 默认会把标题居中)
title: Align(
alignment: Alignment.centerLeft,
child: Text('Birdle'), // 标题文字
),
),
// body 是页面的主要内容区域
body: Center(child: Text('Hello World!')),
),
);
}
}
这里有个新面孔——Align 组件。它的作用是控制子组件的对齐方式。Alignment.centerLeft 表示靠左居中。因为 AppBar 默认会把标题居中显示,我们用 Align 把它调整到左边,更符合常见的设计风格。
热重载后,你会看到页面顶部出现了一个标题栏,上面写着"Birdle"。
二、创建游戏页面组件
在正式开始布局之前,我们先创建一个专门用于游戏界面的组件 GamePage。为什么要单独创建?因为把不同功能的 UI 拆分成独立组件是 Flutter 的最佳实践——代码更清晰,维护也更方便。
在 main.dart 文件中添加以下代码:
class GamePage extends StatelessWidget {
GamePage({super.key});
// Game 对象来自 game.dart 文件
// 它负责管理猜词游戏的逻辑(猜测、判定等),与 UI 无关
final Game _game = Game();
@override
Widget build(BuildContext context) {
// 目前返回一个空容器,后面会替换成实际的游戏棋盘
return Container();
}
}
然后把 MainApp 中 body 的内容替换成 GamePage:
// 将 body 中的内容从 Text 替换为 GamePage 组件
// Center 让 GamePage 在页面中居中显示
body: Center(child: GamePage()),
这样,MainApp 负责整体页面结构(标题栏 + 内容区),GamePage 负责游戏界面的具体布局,各司其职。
三、认识 Column 和 Row
这是今天最重要的内容。在 Flutter 中,排列组件主要靠两个布局 Widget:
Column:把子组件从上到下竖着排(垂直方向)Row:把子组件从左到右横着排(水平方向)
打个比方:Column 就像一摞书,一本叠一本往上放;Row 就像书架上的书,一本挨一本横着排。
3.1 理解游戏棋盘的结构
Birdle 的游戏棋盘是一个 5×5 的网格:5 行,每行 5 个方块。如何用 Column 和 Row 来实现呢?
思路很简单:
- 整体用一个 Column(竖着排 5 行)
- 每一行用一个 Row(横着排 5 个方块)
结构示意:
Column(竖着排)
├── Row(第 1 行)→ Tile Tile Tile Tile Tile
├── Row(第 2 行)→ Tile Tile Tile Tile Tile
├── Row(第 3 行)→ Tile Tile Tile Tile Tile
├── Row(第 4 行)→ Tile Tile Tile Tile Tile
└── Row(第 5 行)→ Tile Tile Tile Tile Tile
这就是 Flutter 布局的核心思想:通过 Column 和 Row 的嵌套来构建任意复杂的界面。
四、编写布局代码
4.1 先搭出 Column 框架
修改 GamePage 的 build 方法:
@override
Widget build(BuildContext context) {
// Padding 给子组件四周加上内边距,防止内容紧贴屏幕边缘
// EdgeInsets.all(8.0) 表示上下左右各留 8 像素的空白
return Padding(
padding: const EdgeInsets.all(8.0),
// Column 将子组件从上到下竖着排列
child: Column(
// spacing 在每个子组件之间自动添加 5 像素的间隔
spacing: 5.0,
// children 是子组件列表,接下来我们会用循环填充它
children: [
// 待添加行
],
),
);
}
这里出现了两个新东西:
Padding:给子组件四周加上内边距。EdgeInsets.all(8.0)表示上下左右各 8 个像素的间距,让内容不会紧贴屏幕边缘。spacing: 5.0:Column 的间距属性,在每个子组件之间自动加上 5 像素的间隔。
4.2 用循环动态生成行
这里是本节的高光时刻。我们不需要手写 5 个 Row,而是用 Dart 的集合 for 语法(collection for)从数据自动生成:
child: Column(
spacing: 5.0,
children: [
// 集合 for 语法:遍历 _game.guesses 列表
// _game.guesses 包含 5 个元素,对应 5 次猜测机会
// 每次循环会创建一个 Row(横向排列的一行)
for (var guess in _game.guesses)
Row(
// 每个方块之间也留 5 像素的水平间距
spacing: 5.0,
children: [
// 待添加方块
],
),
],
),
_game.guesses 是一个包含 5 个元素的列表(对应 5 次猜测机会)。这个 for 循环会遍历列表,为每个元素创建一个 Row。
集合 for 语法是 Dart 的一个语法糖,它允许你在构建列表时直接使用 for 循环。这比先创建列表再用 map 转换要直观得多。
4.3 在每行中添加方块
同样的思路,在每个 Row 内部再嵌套一个循环,为每个字母生成一个 Tile:
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
spacing: 5.0,
children: [
// 外层循环:遍历每一次猜测(共 5 次),生成 5 行
for (var guess in _game.guesses)
Row(
spacing: 5.0,
children: [
// 内层循环:遍历当前猜测中的每个字母(共 5 个),生成 5 个方块
// letter.char → 字母字符,如 'a'、'b'
// letter.type → 猜测结果类型:hit(猜对)/ partial(位置错)/ miss(猜错)/ none(未猜)
for (var letter in guess)
Tile(letter.char, letter.type),
],
),
],
),
);
}
这里 guess 本身是可迭代的(包含 5 个字母),每个 letter 是一个记录(record),拥有 char(字母字符)和 type(猜测结果类型)两个属性。
热重载后,你应该能看到屏幕上出现了一个 5×5 的白色方块网格——这就是 Birdle 的游戏棋盘!
五、理解 Widget 树的变化
让我们看看现在的 Widget 树变成了什么样:
MainApp
└── MaterialApp
└── Scaffold
├── AppBar ← 标题栏分支
│ └── Align
│ └── Text ('Birdle')
└── Center ← 内容区分支
└── GamePage
└── Padding
└── Column ← 竖着排 5 行
├── Row → [Tile, Tile, Tile, Tile, Tile] ← 第 1 行
├── Row → [Tile, Tile, Tile, Tile, Tile] ← 第 2 行
├── Row → [Tile, Tile, Tile, Tile, Tile] ← 第 3 行
├── Row → [Tile, Tile, Tile, Tile, Tile] ← 第 4 行
└── Row → [Tile, Tile, Tile, Tile, Tile] ← 第 5 行
树变得更复杂了,但逻辑依然清晰:Scaffold 分出了 AppBar 和 body 两个分支,body 中是 GamePage,GamePage 里用 Column 和 Row 构建了网格。
随着 Widget 树越来越大,关注树的结构会帮助你快速理解和调试界面。
六、关键概念回顾
6.1 Column 和 Row 的对比
| 特性 | Column | Row |
|---|---|---|
| 排列方向 | 垂直(从上到下) | 水平(从左到右) |
| 主轴 | 垂直方向 | 水平方向 |
| 交叉轴 | 水平方向 | 垂直方向 |
| 常见用途 | 表单、列表、页面内容 | 导航栏、工具栏、图标行 |
两者都接受一个 children 列表,可以放入任意数量的子组件。而且它们可以互相嵌套——这正是我们构建网格的方式。
6.2 集合 for 语法
// ❌ 传统写法:先创建空列表,再用循环逐个添加元素
List<Widget> rows = [];
for (var guess in _game.guesses) {
rows.add(Row(children: [...]));
}
// ✅ 集合 for 语法:直接在列表字面量中使用循环,更简洁
children: [ for (var guess in _game.guesses) Row(children: [...]),
]
// ✅ 也等价于 map 写法,但集合 for 可读性更好
children: [ ..._game.guesses.map((guess) => Row(children: [...])),
]
集合 for 语法让你可以在列表字面量中直接使用循环,代码更紧凑,也更符合 Flutter 的声明式编程风格。这种写法在 Flutter 开发中非常常见,值得尽早熟悉。
6.3 spacing 属性
// ❌ 旧做法:手动在每两个子组件之间插入 SizedBox 来添加间距
Column(
children: [
widget1,
SizedBox(height: 5), // 手动添加间距,麻烦且容易遗漏
widget2,
SizedBox(height: 5),
widget3,
],
)
// ✅ 新做法:使用 spacing 属性,自动在子组件之间添加均匀间距
Column(
spacing: 5.0, // 一行搞定,所有子组件之间都有 5 像素间距
children: [widget1, widget2, widget3],
)
Column 和 Row 都支持 spacing 属性,它会在子组件之间添加均匀的间距。这比手动在每两个子组件之间插入 SizedBox 要方便得多。
七、本节知识点小结
Scaffold 和 AppBar: Scaffold 提供了标准的 Material Design 页面结构,AppBar 是页面顶部的标题栏。它们是搭建页面骨架的"标配组件"。
Column 和 Row: Flutter 最基础也最常用的布局组件。Column 竖着排,Row 横着排,通过嵌套可以构建任意复杂的网格结构。
集合 for 语法: Dart 提供的语法糖,可以在构建列表时直接使用循环,让数据驱动 UI 的写法更加简洁自然。
组件拆分: 把不同功能的 UI 拆分成独立的 Widget(如 GamePage),是保持代码清晰和可维护的最佳实践。
八、下一步学习
棋盘已经搭好了!下一课我们将学习 DevTools——Flutter 提供的强大开发工具,帮助你检查 Widget 树、调试布局问题、分析性能等。
我们下篇文章见!