学习 Flutter(二)
Flutter 布局构建指南摘要 本文介绍了如何使用 Flutter 构建应用布局,重点关注 widget 的排列组合。主要内容包括: 布局规划:通过绘制布局图,识别行、列等基本元素,将 UI 分解为可实现的 widget 结构。 Title 部分实现:创建 TitleSection 组件,包含名称、位置和评分信息,使用 Row 和 Padding 进行排列。 滚动视图:将 Center 替换为
学习 Flutter(二)
本文章学习内容来源于官方文档 https://docs.flutter.cn/ , 本文章只是用于记录学习。
构建 Flutter 布局
学习内容
如何使 Widget 彼此相邻布局
如何在小组件之间添加空间
添加和嵌套 Widget 如何导致 Flutter 布局
根据文档提供的代码,我们可以构建以下应用程序

绘制布局图
在本节中,请考虑您想要的用户体验类型 您的应用用户。
考虑如何定位用户界面的组件。 布局由这些定位的总最终结果组成。 考虑规划布局以加快编码速度。 使用视觉提示来了解屏幕上的内容可能会有很大帮助。
使用您喜欢的任何方法,例如界面设计工具或铅笔 和一张纸。确定要将元素放置在 screen 之前。这是这句格言的编程版本: “测量两次,切割一次。”
-
提出这些问题以将布局分解为其基本元素。
- 您能识别行和列吗?
- 布局是否包含网格?
- 是否有重叠的元素?
- UI 需要选项卡吗?
- 您需要对齐、填充或边框的内容是什么?
-
确定较大的元素。在此示例中,您将 image、title 和 按钮和 description 合并到一个列中。

布局中的主要元素: image、row、row 和 text block
-
绘制每行的图表
a. 第 1 行(标题部分)有三个子项: 一列文本、一个星形图标和一个数字。 它的第一个子项 (列) 包含两行文本。 第一列可能需要更多空间。

b. 第 2 行 Button 部分有三个子项:每个子项包含 一个列,然后包含一个图标和文本。

绘制布局图后,请考虑如何对其进行编码。
你会在一个类中编写所有代码吗? 或者,您是否为布局的每个部分创建一个类?
要遵循 Flutter 最佳实践,请创建一个 class 或 Widget, 以包含布局的每个部分。 当 Flutter 需要重新渲染 UI 的一部分时, 它会更新更改的最小部分。 这就是为什么 Flutter 将 “一切都是 widget” 的原因。 如果 widget 中只有文本发生变化,Flutter 只会重绘该文本。 Flutter 会尽可能少地更改 UI 以响应用户输入。Text
在本教程中,将编写您标识为自身小组件的每个元素。
创建应用程序的基本代码
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
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'),
),
),
);
}
}
添加 Title 部分

标题部分作为草图和原型 UI
创建一个新 Widget TitleSection
import 'package:flutter/material.dart';
class TitleSection extends StatelessWidget {
const TitleSection({super.key, required this.name, required this.location});
final String name;
final String location;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(name,
style: const TextStyle(fontWeight: FontWeight.bold))),
Text(location, style: TextStyle(color: Colors.grey[500]))
],
)),
Icon(Icons.star, color: Colors.red[500]),
const Text('41')
],
));
}
}
- 要使用行中所有剩余的空闲空间,请使用 Widget 执行以下作 拉伸 Widget。 要将列放在行的开头, 将属性设置为 。
Expanded``Column``crossAxisAlignment``CrossAxisAlignment.start - 要在文本行之间添加间距,请将这些行放在 Widget 中。
Padding - 标题行以红色星形图标和文本 . 整行落在 widget 内并填充每条边缘 32 像素。
41``Padding
将应用程序正文更改为滚动视图
在该属性中,将 Widget 替换为 Widget。 在 SingleChildScrollView 小组件中,将小组件替换为小组件。body``Center``SingleChildScrollView``Text``Column
-body: const Center(
- child: Text('Hello World'),
+body: const SingleChildScrollView(
+ child: Column(
+ children: [
这些代码更新会以以下方式更改应用程序。
- 小组件可以滚动。 这允许显示当前屏幕不适合的元素。
SingleChildScrollView - 小组件显示其属性中的任何元素 按列出的顺序。 列表中列出的第一个元素显示在 列表的顶部。列表中显示的元素 在屏幕上从上到下按数组顺序排列。
Column``children``children``children
更新应用程序以显示标题部分
将 Widget 添加为列表中的第一个元素。 这会将其置于屏幕顶部。 将提供的 name 和 location 传递给构造函数。TitleSection``children``TitleSection
class BuildLayoutExample extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("布局构建示例")),
body: const SingleChildScrollView(
child: Column(
children: [
TitleSection(
name: "Oeschinen Lake Campground",
location: "Kandersteg, Switzerland",
),
],
),
),
);
}
}
添加 Button 部分
在本节中,添加将为您的应用程序添加功能的按钮。
Button 部分包含使用相同布局的三列: 一行文本上的图标。

计划将这些列分布在一行中,以便每列采用相同的 空间量。使用主要颜色绘制所有文本和图标。
添加小组件ButtonSection
在 widget 后面添加以下代码以包含代码 构建按钮行。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
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;
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),
),
),
],
);
}
}
更新应用程序以显示按钮部分
将按钮部分添加到列表中。
class BuildLayoutExample extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("布局构建示例")),
body: const SingleChildScrollView(
child: Column(
children: [
TitleSection(
name: "Oeschinen Lake Campground",
location: "Kandersteg, Switzerland",
),
ButtonSection(),
],
),
),
);
}
}
添加 Text 部分
在此部分中,将文本描述添加到此应用程序。

添加小组件TextSection
将以下代码作为单独的 Widget 添加到 Widget 之后。
class TextSection extends StatelessWidget {
const TextSection({super.key, required this.description});
final String description;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Text(
description,
softWrap: true,
),
);
}
}
通过将 softWrap 设置为 ,文本行将填充之前的列宽 在 word 边界处换行。true
更新应用程序以显示文本部分
在 . 添加 widget 时,将其属性设置为 位置描述的文本
class BuildLayoutExample extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("布局构建示例")),
body: const SingleChildScrollView(
child: Column(
children: [
TitleSection(
name: "Oeschinen Lake Campground",
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.',
),
],
),
),
);
}
}
添加 Image 部分
在此部分中,添加图像文件以完成布局。
将应用配置为使用提供的图像
要将应用程序配置为引用图像,请修改其文件。pubspec.yaml
-
在项目顶部创建一个目录。
images -
下载
lake.jpg映像并将其添加到新目录。images -
要包含图像,请向文件添加标签 在应用的根目录中。 添加 时,它用作指向图像的指针集 available 提供给您的代码。
assets``pubspec.yaml``assetsflutter: uses-material-design: true assets: - assets/images/
创建 WidgetImageSection
在其他声明之后定义以下小部件。
class ImageSection extends StatelessWidget {
const ImageSection({super.key, required this.image});
final String image;
Widget build(BuildContext context) {
return Image.asset(image, width: 600, height: 240, fit: BoxFit.cover);
}
}
该值告诉 Flutter 使用 两个约束。首先,将图像显示得尽可能小。 其次,覆盖布局分配的所有空间,称为 render box。BoxFit.cover
更新应用程序以显示图像部分
将 Widget 添加为列表中的第一个子项。 将属性设置为您在 配置应用程序以使用提供的图像 中添加的图像的路径。ImageSection``children``image
class BuildLayoutExample extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("布局构建示例")),
body: const SingleChildScrollView(
child: Column(
children: [
ImageSection(image: 'assets/images/lake.jpg'),
TitleSection(
name: "Oeschinen Lake Campground",
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.',
),
],
),
),
);
}
}
最终我们就实现了如下所示的应用程序

如何修改你的应用程序以使其对用户输入做出反应?接下来,你将为仅包含非交互式 widget 的应用程序添加交互性。具体来说,你将通过创建一个管理两个无状态 widget 的自定义有状态 widget,修改一个图标实现使其可点击。
当应用第一次启动时,这个星形图标是实心红色,表明这个湖以前已经被收藏过了。星号旁边的数字表示 41 个人已经收藏了此湖。完成本教程后,点击星形图标将取消收藏状态,然后用轮廓线的星形图标代替实心的,并减少计数。再次点击会重新收藏,并增加计数。

为了实现这个,你将创建一个包含星形图标和计数的自定义 widget,它们都是 widget。因为点击星形图标会更改这两个 widget 的状态,所以同一个 widget 应该同时管理这两个 widget。
有状态和无状态的 widgets
有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是 有状态的。
无状态的 widget 自身无法改变。 Icon、IconButton 和 Text 都是无状态 widget,它们都是 StatelessWidget 的子类。
而 有状态的 widget 自身是可动态改变的(基于State)。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。 Checkbox、Radio、Slider、 InkWell、Form 和 TextField 都是有状态 widget,它们都是 StatefulWidget 的子类。
一个 widget 的状态保存在一个 State 对象中,它和 widget 的显示分离。 Widget 的状态是一些可以更改的值,如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时,State 对象调用 setState(),告诉框架去重绘 widget。
创建一个有状态的 widget
重点是什么?
-
实现一个有状态 widget 需要创建两个类:一个
StatefulWidget的子类和一个State的子类。 -
State 类包含该 widget 的可变状态并定义该 widget 的
build()方法。 -
当 widget 状态改变时,State 对象调用
setState(),告诉框架去重绘 widget。
在本节中,你将创建一个自定义有状态的 widget。你将使用一个自定义有状态 widget 来替换两个无状态 widget—— 红色实心星形图标和其旁边的数字计数—— 该 widget 用两个子 widget 管理一行 IconButton 和 Text。
实现一个自定义的有状态 widget 需要创建两个类:
-
一个 StatefulWidget 的子类,用来定义一个 widget 类。
-
一个
State的子类,包含该widget状态并定义该 widget 的build()方法。
这一节展示如何为 Lakes 应用程序构建一个名为 FavoriteWidget 的 StatefulWidget。第一步是选择如何管理 FavoriteWidget 的状态。
Step 1: 决定哪个对象管理 widget 的状态
一个 widget 的状态可以通过多种方式进行管理,但在我们的示例中,widget 本身 ——FavoriteWidget—— 将管理自己的状态。在这个例子中,切换星形图标是一个独立的操作,不会影响父窗口 widget 或其他用户界面,因此该 widget 可以在内部处理它自己的状态。
你可以在 状态管理 中了解更多关于 widget 和状态的分离以及如何管理状态的信息。
Step 2: 创建 StatefulWidget 的子类
FavoriteWidget 类管理自己的状态,因此它通过重写 createState() 来创建状态对象。框架会在构建 widget 时调用 createState()。在这个例子中,createState() 创建 _FavoriteWidgetState 的实例,你将在下一步中实现该实例。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
Step 3: 创建 State 的子类
_FavoriteWidgetState 类存储可变信息;可以在 widget 的生命周期内改变逻辑和内部状态。当应用第一次启动时,用户界面显示一个红色实心的星星形图标,表明该湖已经被收藏,并有 41 个「喜欢」。状态对象存储这些信息在 _isFavorited 和 _favoriteCount 变量中。
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
状态对象也定义了 build() 方法。这个 build() 方法创建一个包含红色 IconButton 和 Text 的行。该 widget 使用 IconButton(而不是 Icon),因为它具有一个 onPressed 属性,该属性定义了处理点击的回调方法 (_toggleFavorite)。你将会在接下来的步骤中尝试定义它。
class _FavoriteWidgetState extends State<FavoriteWidget> {
// ···
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(0),
child: IconButton(
padding: const EdgeInsets.all(0),
alignment: Alignment.center,
icon: (_isFavorited
? const Icon(Icons.star)
: const Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(width: 18, child: SizedBox(child: Text('$_favoriteCount'))),
],
);
}
// ···
}
按下 IconButton 时会调用 _toggleFavorite() 方法,然后它会调用 setState()。调用 setState() 是至关重要的,因为这告诉框架, widget 的状态已经改变,应该重绘。 setState() 在如下两种状态中切换 UI:
-
实心的星形图标和数字 41
-
轮廓线的星形图标
star_border和数字 40 之间切换 UI
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
完整的 FavoriteWidget 如下
import 'package:flutter/material.dart';
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(0),
child: IconButton(
padding: const EdgeInsets.all(0),
alignment: Alignment.center,
onPressed: _toggleFavorite,
icon: (_isFavorited
? const Icon(Icons.star)
: const Icon(Icons.star_border)),
color: Colors.red[500],
),
),
SizedBox(
width: 18,
child: SizedBox(child: Text('$_favoriteCount')),
)
],
);
}
}
Step 4: 将有 stateful widget 插入 widget 树中
将你自定义 stateful widget 在 build() 方法中添加到 widget 树中。首先,找到创建 Icon 和 Text 的代码,并删除它,在相同的位置创建有状态的 widget,修改后TitleSection代码如下所示
import 'package:flutter/material.dart';
import 'package:flutter_official_example/widgets/favorite_widget.dart';
class TitleSection extends StatelessWidget {
const TitleSection({super.key, required this.name, required this.location});
final String name;
final String location;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(name,
style: const TextStyle(fontWeight: FontWeight.bold))),
Text(location, style: TextStyle(color: Colors.grey[500]))
],
)),
const FavoriteWidget(),
],
));
}
}
更多推荐


所有评论(0)