- 上一节,我们熟悉了Widget、文字样式、ListView,本节,我们主要讲
开发过程中最常见的自动布局:
-
Center和Alignment - 三种布局方式(
Row、Column、Stack) -
Expanded自动填充和AspectRatio宽高比 -
StatefulWidget可变组件 与StatelessWidget不可变组件
- 本节代码,都默认使用下面
main.dart文件:
设置APP入口组件,使用MaterialApp默认样式,设置home根组件,使用Scaffold支持导航控制器,设置body内容组件。
import 'package:flutter/material.dart';
import 'layout_demo.dart';
// 入口,展示MyWidget组件
void main() => runApp(App());
// 根组件
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
theme: ThemeData(
primaryColor: Colors.yellow
),
);
}
}
// Home 组件
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: Text("Flutter Demo"),
),
body: LayoutDemo(), // 展示LayoutDemo组件
);
}
}
- 下面的代码,都在
LayoutDemo中实现。
1. Center和Alignment
-
Center: 居中 -
Alignment: 自定义水平和垂直方向位置
1.1 Center
-
Center是常用的快捷布局方式,将子组件居中展示在父组件中:
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
// Center: 居中
child: Center(
child: Text( "Layout Demo" ),
),
);
}
}
-
展示样式:
image.png
1.2 Alignment
-
Alignment是自定义水平和垂直方向位置。 默认(0,0)是居中对齐,与center一样。
【参数一】水平方向的百分比。-1:对齐最左边,0:水平居中,1:对齐最右边。-0.5和0.5等中间数值,是按比例展示位置。
【参数二】垂直方向的百分比。-1:对齐最上边,0:垂直居中,1:对齐最下边。-0.5和0.5等中间数值,是按比例展示位置。
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
// Alignment: 按比例展示位置
alignment: Alignment(-1,-1),
child: Text( "Layout Demo" ),
);
}
}
-
展示样式:
image.png
2. 三种布局方式(Row、Column、Stack)
-
Row、Column、Stack实际上是空间坐标系的x、y、z三个方向的布局。
【Row】X轴(水平方向)布局
【Column】Y轴(垂直方向)布局
【Stack】Z轴(前后纵深方向)布局
2.1 Row
-
Row: X轴(水平方向)布局,默认水平撑满父容器空间。
Alignment(0, 0)标注child子容器Row是居中对齐:
由于Row是水平撑满父容器空间,所以会看到Row的子元素没水平居中对齐的假象。 实际上我们Alignment是影响了Row容器水平对齐了,Row的子元素的对齐方式,是Row内部处理的。
mainAxisAlignment:主轴方向对齐(Row和Column都有这个属性)
在Row中:
start:靠左(默认)
end:靠右
center:居中
spaceAround: 剩下空间平均分配在周围(每个部件周围等间距)
spaceBetween:剩下空间平均分配在小部件中间(两边无间距,中间等间距)
spaceEvenly: 完全等间距)
crossAxisAlignment:交叉轴方向对齐(Row和Column都有这个属性)
在Row中:
start:居上
center:居中(默认)
end:居下
baseline:文字底部对齐(如果在Column中,必须配合textBaseline使用,后面具体分析)
stretch:垂直填充长条
Expanded:填充式布局,完全等分主轴宽度,会自动换行。row宽度无效,column高度无效(后面会具体分析)
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0), // 居中对齐
// Row 水平(X轴水平方向)
// Row 是在父控件`Container`的居中展示。内部元素默认是从左到右
child: Row(
// 主轴方向(start:靠左(默认),end: 靠右, center: 居中
// spaceAround: 剩下空间平均分配在周围(每个部件周围等间距)
// spaceBetween: 剩下空间平均分配在小部件中间(两边无间距,中间等间距),
// spaceEvenly: 完全等间距)
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// 交叉轴(y轴方向) (start: 居上,center: 居中(默认), end: 居下,
// baseline: 文字底部对齐,stretch: 垂直填充长条)
crossAxisAlignment: CrossAxisAlignment.baseline,
children: <Widget>[
// 使用Expanded填充式布局,完全等分主轴宽度,会自动换行。row宽度无效,column高度无效
Expanded(
child: Container(
child: Icon(
Icons.add,
size: 120,
),
color: Colors.red),
),
Expanded(
child: Container(
child: Icon(
Icons.ac_unit,
size: 60,
),
color: Colors.blue),
),
Expanded(
child: Container(
child: Icon(
Icons.access_alarm,
size: 30,
),
color: Colors.white),
),
],
),
);
}
}
-
展示样式:
image.png
2.2 Column
-
Column:Y轴(垂直方向)布局,默认垂直撑满父容器空间。
Alignment对齐方式和mainAxisAlignment主轴对齐、crossAxisAlignment交叉轴对齐与Row类似,只是一个是水平一个是垂直方向。- 需要注意
crossAxisAlignment的textBaseline类型,必须配合textBaseline使用(后面具体分析)
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0),
// Column 垂直(y轴垂直)
// Column 是在父控件`Container`的居中展示。内部元素是从上到下
child: Column(
// 主轴方向(start:靠上(默认),end: 靠下, center: 居中,
// spaceAround: 剩下空间平均分配在周围(每个部件周围等间距)
// spaceBetween: 剩下空间平均分配在小部件中间(两边无间距,中间等间距),
// spaceEvenly: 完全等间距)
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// 交叉轴(y轴方向) (start: 居左,center: 居中(默认), end: 居右,
// baseline: 文字底部对齐(针对文本,需要配合textBaseLine),
// stretch: 水平填充长条)
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: Icon(
Icons.add,
size: 120,
),
color: Colors.red),
Container(
child: Icon(
Icons.ac_unit,
size: 60,
),
color: Colors.blue),
Container(
child: Icon(
Icons.access_alarm,
size: 30,
),
color: Colors.white),
],
),
);
}
}
-
展示样式:
image.png
2.2.1 演示baseline(文字底部对齐)
-
baseline: 基于文字底部对齐,我们用下面这个案例(多个Text组件高度相同,字体不同)来体会:
crossAxisAlignment设置为baseline时,需要添加textBaseline属性(Row非必须,但Column是必须),可以设置为:
alphabetic:字母ideographic:中文(好像没区别)
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
// 演示baseline(文字底部对齐)
alignment: Alignment(0,0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// row不会报错,但是Column会报错,必须指定textBaseline
crossAxisAlignment: CrossAxisAlignment.baseline,
// 文本基线textBaseline: alphabetic:英文字符 ideographic: 中文字符
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
Container(
child: Text("你好!", style: TextStyle(fontSize: 20)),
color: Colors.red,
height: 80
),
Container(
child: Text("我是", style: TextStyle(fontSize: 30)),
color: Colors.green,
height: 80
),
Container(
child: Text("哈哈哈", style: TextStyle(fontSize: 40)),
color: Colors.blue,
height: 80
)],
)
);
}
}

2.3 Stack
-
Stack:Z轴(前后纵深方向)布局(设置多个大小不同的组件,就可以看到效果:)
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0),
// Stack 重叠 (Z轴纵深方向)
// Stack 是在父控件`Container`的居中展示。内部元素是从里到外
child: Stack(
children: <Widget>[
Container(
child: Icon(
Icons.add,
size: 120,
),
color: Colors.red),
Container(
child: Icon(
Icons.ac_unit,
size: 60,
),
color: Colors.blue),
Container(
child: Icon(
Icons.access_alarm,
size: 30,
),
color: Colors.white),
],
),
);
}
}
-
展示样式:
image.png
2.3.1 positioned
在一个
200 * 200 白色背景组件中,如果我们想自主控制每个组件的位置,可以通过positioned来设置:具体的参数设置,可参考
positioned的构造方法和内部描述。
第一个子控件左上角展示,第二个子控件右下角展示,第三个子控件靠右,离顶部60像素展示
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0),
child: Container(
width: 200,
height: 200,
color: Colors.white,
child: getStack(),
)
);
}
// 获取Stack组件
Widget getStack() {
return Stack(
children: <Widget>[
// 左上(默认)
Positioned(
child:
Container(
child: Icon(
Icons.add,
size: 120,
),
color: Colors.red),
),
// 右下
Positioned(
right: 0,
bottom: 0,
child:
Container(
child: Icon(
Icons.ac_unit,
size: 60,
),
color: Colors.blue),
),
// 右上(距离顶部60像素)
Positioned(
right: 0,
top: 60,
child:
Container(
child: Icon(
Icons.access_alarm,
size: 30,
),
color: Colors.white),
),
],
);
}
}
-
展示样式:
image.png
3. Expanded自动填充和AspectRatio宽高比
3.1 Expanded自动填充
上面提到Expanded是填充式布局,完全等分主轴宽度,会自动换行。(row宽度无效,column高度无效)
- 现在,我们以
多个文本组件为例,体验两种情况:
- 组件内
所有子组件都是Expanded,布局情况如何? - 组件内
Expanded和其他组件共存,布局情况如何?
1. 全Expanded
- 在
Row组件中,child设置三个Text组件,字体和内容都不一样,但使用Expanded后,所有组件宽度相等,自动换行:
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
}
// 获取Row组件
Widget getRow() {
return Row(children: <Widget>[
Expanded(
child: Container(
child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
color: Colors.red),
),
Expanded(
child: Container(
child: Text("我是我是我是我是我是", style: TextStyle(fontSize: 30)),
color: Colors.green),
),
Expanded(
child: Container(
child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
color: Colors.blue),
),
]);
}
}
-
展示样式:
image.png
2. Expanded与其他组件共存
- 我们将
第二个子组件改为非Expanded组件,可以看到``非Expanded组件布局完成之后,剩余空间给Expanded组件平分宽度:
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
}
// 获取Row组件
Widget getRow() {
return Row(children: <Widget>[
Expanded(
child: Container(
child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
color: Colors.red),
),
// 第二个子组件不是`Expanded`
Container(
child: Text("我是我是我是我是我是我是", style: TextStyle(fontSize: 30)),
color: Colors.green),
Expanded(
child: Container(
child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
color: Colors.blue),
),
]);
}
}
-
展示样式:
image.png
如果
超过屏幕宽度,我们需要对宽高做设置
3.2 AspectRatio 宽高比
-
AspectRatio宽高比部件,可以设置child子视图的宽高比:
import 'package:flutter/material.dart';
// 布局Demo
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
alignment: Alignment(0, 0),
child: Container(
color: Colors.blue,
width: 300,
// AspectRatio 宽高比部件
child: AspectRatio(
aspectRatio: 2/1, // 设置宽高比
child: Icon(Icons.add),
),
)
);
}
}
-
展示样式:
image.png 我们也可以通过
属性设定,来读取宽高值。下面分析可变组件和不可变组件的本质区别。
4. StatefulWidget可变部件 与 StatelessWidget不可变部件
-
StatelessWidget: 不可变部件,所有变量都是final修饰,不可以变更状态
(实际每次变更状态,就是销毁原部件,根据新初始值创建新不可变部件) -
StatefulWidget:可变部件,实际是状态值(记录UI状态)与不可变部件(描述UI)的组合。
(每一次状态值变更,都触发不可变部件的重新创建。而状态值会跟随StatefulWidget的销毁而销毁。)
所以实际上可变部件与不可变部件的区别就是可变部件 多记录了状态值,在UI上,本质都是通过创建和销毁 不可变部件来进行展示。
4.1 重写计数器Demo
- 我们通过重写
Flutter默认的计数器Demo,来体验StatelessWidget不可变部件和StatefulWidget可变部件的区别:
4.1.1 main.dart创建
- 指定
APP入口为App(),使用MaterialApp构建APP,将home根视图设置为StateManagerDemo ()
import 'package:flutter/material.dart';
import 'package:hello_flutter/state_manager_demo.dart';
// 入口,展示MyWidget组件
void main() => runApp(App());
// 根组件
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 返回`MaterialApp`
return MaterialApp(
debugShowCheckedModeBanner: false,
home: StateManagerDemo(), // 指定根视图为`StateManagerDemo`
theme: ThemeData(
primaryColor: Colors.blue
),
);
}
}
4.1.2 StateManagerDemo创建(StatelessWidget不可变部件)
- 新建
state_manager_demo.dart文件,StateManagerDemo为StatelessWidget不可变部件,使用Scaffold导航控制器部件设置appBar导航栏、body内容和floatingActionButton浮动按钮。
floatingActionButton新增onPressed点击事件,触发回调函数(每次点击,count+1,打印count值)
import 'package:flutter/material.dart';
class StateManagerDemo extends StatelessWidget {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("StateManagerDemo"),
),
body: Center(
child: Chip(label: Text("$count"),), // 全圆角的组件
),
// 浮动按钮(默认右下角)
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
// 点击触发: 给一个无参回调函数,可以直接给`(){}`,也可以外部创建一个函数,传入函数名
// 每次点击,count+1,打印count值
onPressed: (){
count += 1;
print("count = $count");
},
)
);
}
}
- 当我们在
StatelessWidget不可变部件中创建变量未使用final修饰,而使用var修饰时,会有提示:
This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: StateManagerDemo.count
image.png
我们忽略不管(暂时不管,后面做比较),继续运行,可以看到界面运行正常:
image.png
- 点击
浮动按钮 ➕号,打印台可以看到count数递增了,但是页面中心的UI未同步更新。

我们发现,不用
final修饰变量count,也可以正常运行,并且count完成了计数任务,但是界面UI未更新
- 如果我们使用
final声明变量,就等同于swift中的let声明,是不可变的,不能进行count+=1的操作。实际上,这是
Flutter的底层渲染原理决定的,StatelessWidget不可变部件,本身不支持记录状态,每次都是销毁再重新渲染UI,所以编译器提示我们不可变部件内全部使用final修饰。这样从编码层就减少错误的产生。
- 如果一定需要
记录状态,请使用StatefulWidget可变部件。
4.1.3 StateManagerDemo创建(StatefulWidget可变部件)
- 上面说到,如果要
记录状态,请使用StatefulWidget可变部件。
但StatefulWidget本身并不是直接改变UI,而是在StatelessWidget不可变部件的基础上,多了一个管理状态的State组件。这个组件记录了状态,并决定了返回的UI样式。 - 所以,
记录状态的变量(count)和返回组件的构造方法(build),都应该放在State中。 而StatefulWidget就是直接返回State根据当前数据build返回的UI部件。 - 需要补充说明的是: 我们需要
部件及时更新,需要在数据变动处,调用setState(){}更新状态。
import 'package:flutter/material.dart';
class StateManagerDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 直接返回State根据最新数据build的组件
return _StateManagerState();
}
}
class _StateManagerState extends State<StateManagerDemo> {
// 记录计数
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("StateManagerDemo"),
),
body: Center(
child: Chip(label: Text("$count"),), // 全圆角的组件
),
// 浮动按钮(默认右下角)
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
// 点击触发: 给一个无参回调函数,可以直接给`(){}`,也可以外部创建一个函数,传入函数名
// 每次点击,count+1,打印count值
onPressed: (){
count += 1; // 数据变动
setState(() {}); // 更新状态(自动触发UI的重新渲染)
print("count = $count");
},
)
);
}
}
-
页面样式:
image.png
总结:
StatelessWidget不可变部件:直接
build返回UI部件即可,变量都使用final修饰,不可更改。每次通过构造方法传入新数据,都会进行旧部件的销毁和新部件的创建。
StatefulWidget可变部件:
State组件管理状态,并build返回最新状态的UI部件。每次状态变更,需要setState(() {})更新UI部件
(渲染机制会自动销毁旧UI部件,创建新UI部件,State的生命周期与StatefulWidget一致)
StatefulWidget可变部件只需要返回State对象即可
(实际是返回State中build的最新UI部件)
【思考】
实际上,在
Flutter开发中我们需要注意一些优化点。
- 我们整个页面,其实只有
+号按钮点击,触发中心的计数UI的更新。并不需要每次数据变化,都更新整个外部部件。- 我们可以将
Chip更新为一个StatefulWidget可变部件,点击+号时,通过回调让Chip部件重新渲染即可。
本节,我们主要分析自动布局的几种方式,以及StatefulWidget和StatefulWidget的区别。
下一节,Flutter入门四:搭建项目、资源调用、简单开发。从项目入手,一步步理解和熟悉Flutter。











