构建 Flutter 布局
本教程解释了如何在 Flutter 中设计和构建布局。
如果您使用提供的示例代码,您可以构建以下应用程序。

照片由 Dino Reichmuth 摄于 Unsplash。文本由 瑞士旅游局 提供。
要更好地了解布局机制,请从 Flutter 的布局方法 开始。
绘制布局图
#在本节中,请考虑您希望为应用程序用户提供什么样的用户体验。
考虑如何定位用户界面的组件。布局由这些定位的总最终结果组成。考虑规划您的布局以加快编码速度。使用视觉线索来了解屏幕上的内容位置会非常有帮助。
使用您喜欢的方法,例如界面设计工具或铅笔和一张纸。在编写代码之前,弄清楚您想将元素放在屏幕上的什么位置。这是“三思而后行”这句格言的编程版本。
提出以下问题,将布局分解为基本元素。
- 您能识别行和列吗?
- 布局是否包含网格?
- 是否存在重叠元素?
- UI 是否需要选项卡?
- 您需要对什么进行对齐、填充或边框?
识别较大的元素。在此示例中,您将图像、标题、按钮和描述排列成一列。
布局中的主要元素:图像、行、行和文本块 绘制每行的图示。
第 1 行,**标题**部分,有三个子项:一列文本、一个星形图标和一个数字。它的第一个子项,即该列,包含两行文本。第一列可能需要更多空间。
带文本块和图标的标题部分 第 2 行,**按钮**部分,有三个子项:每个子项包含一列,该列又包含一个图标和文本。
带有三个带标签按钮的按钮部分
绘制布局图后,考虑如何编写代码。
您会将所有代码都写在一个类中吗?或者,您会为布局的每个部分创建一个类吗?
为了遵循 Flutter 的最佳实践,请创建一个类或组件来包含布局的每个部分。当 Flutter 需要重新渲染 UI 的一部分时,它会更新发生变化的最小部分。这就是 Flutter 将“一切皆组件”的原因。如果只有 `Text` 组件中的文本发生变化,Flutter 只会重新绘制该文本。Flutter 会根据用户输入尽可能少地更改 UI。
对于本教程,将您已识别的每个元素编写为自己的组件。
创建应用程序基础代码
#在本节中,构建基本的 Flutter 应用代码以启动您的应用。
用以下代码替换 `lib/main.dart` 的内容。此应用为应用标题和显示在应用 `appBar` 上的标题使用了一个参数。此决定简化了代码。
dartimport 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { const String appTitle = 'Flutter layout demo'; return MaterialApp( title: appTitle, home: Scaffold( appBar: AppBar(title: const Text(appTitle)), body: const Center( child: Text('Hello World'), ), ), ); } }
添加标题部分
#在本节中,创建一个类似于以下布局的 `TitleSection` 组件。
添加 `TitleSection` 组件
#在 `MyApp` 类之后添加以下代码。
class TitleSection extends StatelessWidget {
const TitleSection({super.key, required this.name, required this.location});
final String name;
final String location;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Text(location, style: TextStyle(color: Colors.grey[500])),
],
),
),
/*3*/
Icon(Icons.star, color: Colors.red[500]),
const Text('41'),
],
),
);
}
}
- 为了利用行中所有剩余的可用空间,使用 `Expanded` 组件来拉伸 `Column` 组件。为了将列放置在行的开头,将 `crossAxisAlignment` 属性设置为 `CrossAxisAlignment.start`。
- 为了在文本行之间添加空间,将这些行放入 `Padding` 组件中。
- 标题行以一个红色星形图标和文本 `41` 结束。整个行位于一个 `Padding` 组件内,并对每个边缘填充 32 像素。
将应用主体更改为可滚动视图
#在 `body` 属性中,将 `Center` 组件替换为 `SingleChildScrollView` 组件。在 `SingleChildScrollView` 组件内,将 `Text` 组件替换为 `Column` 组件。
body: const Center(
child: Text('Hello World'),
body: const SingleChildScrollView(
child: Column(
children: [
这些代码更新以以下方式更改了应用程序。
- `SingleChildScrollView` 组件可以滚动。这允许显示不适合当前屏幕的元素。
- `Column` 组件按照列出的顺序显示其 `children` 属性中的任何元素。 `children` 列表中列出的第一个元素显示在列表的顶部。 `children` 列表中的元素在屏幕上按数组顺序从上到下显示。
更新应用程序以显示标题部分
#将 `TitleSection` 组件作为 `children` 列表中的第一个元素添加。这会将其放置在屏幕顶部。将提供的名称和位置传递给 `TitleSection` 构造函数。
children: [
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
],
添加按钮部分
#在本节中,添加将为您的应用程序添加功能的按钮。
**按钮**部分包含三个使用相同布局的列:图标位于一行文本上方。
计划将这些列分布在一行中,以便每个列占用相同的空间量。用主色绘制所有文本和图标。
添加 `ButtonSection` 组件
#在 `TitleSection` 组件之后添加以下代码,以包含构建按钮行的代码。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
// ···
}
}
创建用于生成按钮的组件
#由于每个列的代码可以使用相同的语法,因此创建一个名为 `ButtonWithText` 的组件。该组件的构造函数接受颜色、图标数据和按钮的标签。使用这些值,该组件构建一个 `Column`,其中包含一个 `Icon` 和一个样式化的 `Text` 组件作为其子项。为了帮助分隔这些子项,`Text` 组件被一个 `Padding` 组件包裹。
在 `ButtonSection` 类之后添加以下代码。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
// ···
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
使用 `Row` 组件定位按钮
#将以下代码添加到 `ButtonSection` 组件中。
- 添加三个 `ButtonWithText` 组件实例,每个按钮一个。
- 传递该特定按钮的颜色、`Icon` 和文本。
- 使用 `MainAxisAlignment.spaceEvenly` 值沿主轴对齐列。 `Row` 组件的主轴是水平的,`Column` 组件的主轴是垂直的。因此,此值告诉 Flutter 沿 `Row` 在每个列之前、之间和之后平均分配可用空间。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
return SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ButtonWithText(color: color, icon: Icons.call, label: 'CALL'),
ButtonWithText(color: color, icon: Icons.near_me, label: 'ROUTE'),
ButtonWithText(color: color, icon: Icons.share, label: 'SHARE'),
],
),
);
}
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
// ···
);
}
}
更新应用程序以显示按钮部分
#将按钮部分添加到 `children` 列表。
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
],
添加文本部分
#在本节中,将文本描述添加到此应用程序中。
添加 `TextSection` 组件
#在 `ButtonSection` 组件之后,将以下代码添加为一个单独的组件。
class TextSection extends StatelessWidget {
const TextSection({super.key, required this.description});
final String description;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Text(description, softWrap: true),
);
}
}
通过将 `softWrap` 设置为 `true`,文本行在单词边界处换行之前会填满列宽。
更新应用程序以显示文本部分
#在 `ButtonSection` 之后添加一个新的 `TextSection` 组件作为子组件。添加 `TextSection` 组件时,将其 `description` 属性设置为位置描述的文本。
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
TextSection(
description:
'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
'Bernese Alps. Situated 1,578 meters above sea level, it '
'is one of the larger Alpine Lakes. A gondola ride from '
'Kandersteg, followed by a half-hour walk through pastures '
'and pine forest, leads you to the lake, which warms to 20 '
'degrees Celsius in the summer. Activities enjoyed here '
'include rowing, and riding the summer toboggan run.',
),
],
添加图像部分
#在本节中,添加图像文件以完成您的布局。
配置您的应用程序以使用提供的图像
#要配置您的应用程序以引用图像,请修改其 `pubspec.yaml` 文件。
在项目顶部创建一个 `images` 目录。
下载 `lake.jpg` 图像并将其添加到新的 `images` 目录中。
要包含图像,请在应用程序根目录的 `pubspec.yaml` 文件中添加一个 `assets` 标签。当您添加 `assets` 时,它充当指向代码可用图像的指针集。
pubspec.yamlyamlflutter: uses-material-design: true assets: - images/lake.jpg
创建 `ImageSection` 组件
#在其他声明之后定义以下 `ImageSection` 组件。
class ImageSection extends StatelessWidget {
const ImageSection({super.key, required this.image});
final String image;
@override
Widget build(BuildContext context) {
return Image.asset(image, width: 600, height: 240, fit: BoxFit.cover);
}
}
`BoxFit.cover` 值告诉 Flutter 以两个约束条件显示图像。首先,尽可能小地显示图像。其次,覆盖布局分配的所有空间,称为渲染框。
更新应用程序以显示图像部分
#将 `ImageSection` 组件作为 `children` 列表中的第一个子组件添加。将 `image` 属性设置为您在 配置您的应用程序以使用提供的图像 中添加的图像路径。
children: [
ImageSection(
image: 'images/lake.jpg',
),
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
恭喜
#就是这样!当您热重载应用程序时,您的应用程序应该看起来像这样。

资源
#您可以从以下位置访问本教程中使用的资源
Dart 代码: `main.dart`
图片: ch-photo
Pubspec: `pubspec.yaml`
下一步
#要为此布局添加交互性,请遵循 交互性教程。