构建 Flutter 布局
学习如何在 Flutter 中构建布局。
本教程说明如何在 Flutter 中设计并构建布局。
若使用提供的示例代码,你可以构建如下应用。
The finished app.
图片来自 Unsplash 上的 Dino Reichmuth。文字来自 Switzerland Tourism。
要更好理解布局机制,请先阅读 Flutter 的布局方法。
考虑如何摆放用户界面各组件的位置。布局由这些摆放的最终结果构成。规划布局有助于加快编码。用视觉线索判断元素在屏幕上的位置会很有帮助。
你可用喜欢的方式,如界面设计工具或纸笔,在写代码前想好元素在屏幕上的位置。这是「量两次,裁一次」这句俗语在编程中的体现。
绘制布局草图
#在本节中,考虑你希望为应用用户提供怎样的体验。
考虑如何摆放用户界面各组件的位置。布局由这些摆放的最终结果构成。规划布局有助于加快编码。用视觉线索判断元素在屏幕上的位置会很有帮助。
你可用喜欢的方式,如界面设计工具或纸笔,在写代码前想好元素在屏幕上的位置。这是「量两次,裁一次」这句俗语在编程中的体现。
用以下问题将布局分解为基本元素。
Can you identify the rows and columns?
Does the layout include a grid?
Are there overlapping elements?
Does the UI need tabs?
What do you need to align, pad, or border?
能否识别出行与列?
布局是否包含网格?
是否有重叠元素?
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 应用代码。
-
Replace the contents of
lib/main.dartwith the following code. This app uses a parameter for the app title and the title shown on the app'sappBar. This decision simplifies the code.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'), ), ), ); } }
将
lib/main.dart的内容替换为以下代码。此应用使用参数设置应用标题以及在appBar中显示的标题,以简化代码。
添加标题区域
#本节创建一个与下列布局相似的 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'),
],
),
);
}
}
-
To use all remaining free space in the row, use the
Expandedwidget to stretch theColumnwidget. To place the column at the start of the row, set thecrossAxisAlignmentproperty toCrossAxisAlignment.start. To add space between the rows of text, put those rows in a
Paddingwidget.-
The title row ends with a red star icon and the text
41. The entire row falls inside aPaddingwidget and pads each edge by 32 pixels. -
要在行中使用所有剩余空闲空间,用
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: [
这些代码更新会以如下方式改变应用。
-
A
SingleChildScrollViewwidget can scroll. This allows elements that don't fit on the current screen to display. -
A
Columnwidget displays any elements within itschildrenproperty in the order listed. The first element listed in thechildrenlist displays at the top of the list. Elements in thechildrenlist display in array order on the screen from top to bottom. 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 中。
Add three instances of the
ButtonWithTextwidget, once for each button.Pass the color,
Icon, and text for that specific button.-
Align the columns along the main axis with the
MainAxisAlignment.spaceEvenlyvalue. The main axis for aRowwidget is horizontal and the main axis for aColumnwidget is vertical. This value, then, tells Flutter to arrange the free space in equal amounts before, between, and after each column along theRow. 为每个按钮各添加一个
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 文件。
Create an
imagesdirectory at the top of the project.在项目顶层创建
images目录。-
Download the
lake.jpgimage and add it to the newimagesdirectory. -
下载
lake.jpg图片并添加到新的images目录。 -
To include images, add an
assetstag to thepubspec.yamlfile at the root directory of your app. When you addassets, it serves as the set of pointers to the images available to your code. -
要包含图片,在应用根目录的
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-04。查看文档源码 或者 为本页面内容提出建议。