构建 Flutter 布局
学习如何在 Flutter 中构建布局。
本教程说明如何在 Flutter 中设计并构建布局。
若使用提供的示例代码,你可以构建如下应用。
The finished app.
图片来自 Unsplash 上的 Dino Reichmuth。文字来自 Switzerland Tourism。
要更好理解布局机制,请先阅读 Flutter 的布局方法。
绘制布局草图
#在本节中,考虑你希望为应用用户提供怎样的体验。
考虑如何摆放用户界面各组件的位置。布局由这些摆放的最终结果构成。规划布局有助于加快编码。用视觉线索判断元素在屏幕上的位置会很有帮助。
你可用喜欢的方式,如界面设计工具或纸笔,在写代码前想好元素在屏幕上的位置。这是「三思而后行 (Measure twice, cut once)」这句俗语在编程中的体现。
用以下问题将布局分解为基本元素。
能否识别出行与列?
布局是否包含网格?
是否有重叠元素?
UI 是否需要标签页?
需要对齐、内边距或边框的是什么?
识别较大的元素。本例中,你将图片、标题、按钮和描述排成一列。
Major elements in the layout: image, row, row, and text block
绘制每一行。
第 1 行 Title 区域有三个子节点:一列文字、星形图标和一个数字。其第一个子节点(列)包含两行文字,该列可能需要更多空间。
Title section with text blocks and an icon
第 2 行 Button 区域有三个子节点:每个子节点包含一列,列内再有图标和文字。
The Button section with three labeled buttons
绘制布局草图后,考虑如何编码实现。
你会把所有代码写在一个类里,还是为布局的每个部分各创建一个类?
遵循 Flutter 最佳实践时,为布局的每个部分创建一个类或 Widget。当 Flutter 需要重新渲染 UI 的某一部分时,只更新变化的最小部分。这就是 Flutter「万物皆 widget」的原因。若 Text widget 中只有文字变化,Flutter 只重绘该文字。
Flutter 响应用户输入时尽可能少地改变 UI。
本教程中,将你识别的每个元素写成各自的 widget。
创建应用基础代码
#本节搭建启动应用所需的基础 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 widget。
The Title section as sketch and prototype UI
添加 TitleSection Widget
#
在 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'),
],
),
);
}
}
-
要在行中使用所有剩余空闲空间,用
Expandedwidget 拉伸Columnwidget。要将列放在行首,将crossAxisAlignment设为CrossAxisAlignment.start。 -
要在文本行之间添加间距,将这些行放在
Paddingwidget 中。 -
标题行以红色星形图标和文本
41结束。整行位于Paddingwidget 内,四边各留 32 像素内边距。
将应用 body 改为可滚动视图
#
在 body 属性中,将 Center widget 替换为 SingleChildScrollView widget。在 SingleChildScrollView
widget 内,将 Text widget 替换为 Column widget。
body: const Center(
child: Text('Hello World'),
body: const SingleChildScrollView(
child: Column(
children: [
这些代码更新会以如下方式改变应用。
-
SingleChildScrollViewwidget 可以滚动,使放不进当前屏幕的元素得以显示。 -
Columnwidget 按children属性中列出的顺序显示其中的元素。children列表中的第一项显示在顶部,列表中的元素按数组顺序自上而下显示在屏幕上。
更新应用以显示标题区域
#
将 TitleSection widget 作为 children 列表的第一项添加,使其显示在屏幕顶部。将提供的名称和位置传给 TitleSection 构造函数。
children: [
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
],
添加按钮区域
#本节添加为应用增加功能的按钮。
Button 区域包含三列,使用相同布局:图标在上、文字在下。
The Button section as sketch and prototype UI
计划将这些列放在一行中,使每列占用相同空间。将所有文字和图标绘制为主题主色。
添加 ButtonSection widget
#
在 TitleSection widget 之后添加以下代码,用于构建按钮行。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
// ···
}
}
创建用于制作按钮的 widget
#
由于每列代码可使用相同写法,创建一个名为 ButtonWithText 的 widget。其构造函数接受颜色、图标数据和按钮标签。
widget 用这些值构建包含 Icon 和样式化 Text widget 的 Column。为分隔子节点,用 Padding widget 包裹 Text widget。
在 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 widget 摆放按钮
#
将以下代码添加到 ButtonSection widget 中。
-
为每个按钮各添加一个
ButtonWithTextwidget 实例。 -
传入该按钮对应的颜色、
Icon和文字。 -
用
MainAxisAlignment.spaceEvenly沿主轴对齐各列。Rowwidget 的主轴是水平的,Columnwidget 的主轴是垂直的。该值告诉 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(),
],
添加文本区域
#本节为应用添加文字描述。
The text block as sketch and prototype UI
添加 TextSection widget
#
在 ButtonSection widget 之后作为独立 widget 添加以下代码。
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 widget 作为子节点。添加 TextSection widget 时,将其 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 widget
#
在其他声明之后定义以下 ImageSection widget。
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 在两项约束下显示图片:首先尽可能小地显示图片;其次覆盖布局分配的全部空间,即 render box。
更新应用以显示图片区域
#
将 ImageSection widget 作为 children 列表的第一项添加。将 image 属性设为你在 配置应用以使用提供的图片
中添加的图片路径。
children: [
ImageSection(
image: 'images/lake.jpg',
),
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
恭喜
#就是这样!热重载应用后,应用应如下所示。
The finished app
资源
#可从以下位置访问本教程使用的资源:
Dart code: main.dart
Image: ch-photo
Pubspec: pubspec.yaml
下一步
#要为该布局添加交互性,请参阅 交互性教程。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-17。查看文档源码 或者 为本页面内容提出建议。