Flutter 布局入门用 Column 和 Row 搭建游戏网格(三)

1 阅读8分钟

前言

在前两篇文章中,我们创建了 Flutter 项目,还学会了自定义一个 Tile 组件。但一个孤零零的方块并不能构成游戏界面——我们需要把 25 个方块整整齐齐地排成 5 行 5 列的网格。

今天这篇文章基于官方教程的「Layout」章节,我们将学习 Flutter 中最核心的布局知识:如何用 ScaffoldAppBarColumnRow 来组织界面结构。学完之后,你就能搭建出 Birdle 猜词游戏的完整棋盘了。


一、用 Scaffold 和 AppBar 搭建页面骨架

1.1 什么是 Scaffold?

打开任何一个手机 App,你会发现它们的页面结构通常都很类似:顶部有一个标题栏,中间是主要内容,底部可能有导航栏。

Scaffold 就是 Flutter 提供的一个"页面脚手架",它帮你把这些常见的页面区域预先划分好了。你只需要往对应的"槽位"里填内容就行。

你可以把 Scaffold 想象成一套精装修的毛坯房——墙壁、地板、天花板都做好了,你只需要摆放家具。

1.2 什么是 AppBar?

AppBar 就是页面顶部的标题栏。它通常用来显示应用名称、返回按钮或设置按钮等。

1.3 给 Birdle 添加标题栏

修改 MainAppbuild 方法,添加 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();
  }
}

然后把 MainAppbody 的内容替换成 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 框架

修改 GamePagebuild 方法:

@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 的对比

特性ColumnRow
排列方向垂直(从上到下)水平(从左到右)
主轴垂直方向水平方向
交叉轴水平方向垂直方向
常见用途表单、列表、页面内容导航栏、工具栏、图标行

两者都接受一个 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],
)

ColumnRow 都支持 spacing 属性,它会在子组件之间添加均匀的间距。这比手动在每两个子组件之间插入 SizedBox 要方便得多。


七、本节知识点小结

Scaffold 和 AppBar: Scaffold 提供了标准的 Material Design 页面结构,AppBar 是页面顶部的标题栏。它们是搭建页面骨架的"标配组件"。

Column 和 Row: Flutter 最基础也最常用的布局组件。Column 竖着排,Row 横着排,通过嵌套可以构建任意复杂的网格结构。

集合 for 语法: Dart 提供的语法糖,可以在构建列表时直接使用循环,让数据驱动 UI 的写法更加简洁自然。

组件拆分: 把不同功能的 UI 拆分成独立的 Widget(如 GamePage),是保持代码清晰和可维护的最佳实践。


八、下一步学习

棋盘已经搭好了!下一课我们将学习 DevTools——Flutter 提供的强大开发工具,帮助你检查 Widget 树、调试布局问题、分析性能等。

我们下篇文章见!


参考资料:Flutter 官方教程 - Layout