- 上一節(jié),我們熟悉了Widget、文字樣式、ListView,本節(jié),我們主要講
開發(fā)過程中最常見的自動布局:
-
Center和Alignment - 三種布局方式(
Row、Column、Stack) -
Expanded自動填充和AspectRatio寬高比 -
StatefulWidget可變組件 與StatelessWidget不可變組件
- 本節(jié)代碼,都默認使用下面
main.dart文件:
設(shè)置APP入口組件,使用MaterialApp默認樣式,設(shè)置home根組件,使用Scaffold支持導航控制器,設(shè)置body內(nèi)容組件。
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中實現(xiàn)。
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一樣。
【參數(shù)一】水平方向的百分比。-1:對齊最左邊,0:水平居中,1:對齊最右邊。-0.5和0.5等中間數(shù)值,是按比例展示位置。
【參數(shù)二】垂直方向的百分比。-1:對齊最上邊,0:垂直居中,1:對齊最下邊。-0.5和0.5等中間數(shù)值,是按比例展示位置。
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內(nèi)部處理的。
mainAxisAlignment:主軸方向?qū)R(Row和Column都有這個屬性)
在Row中:
start:靠左(默認)
end:靠右
center:居中
spaceAround: 剩下空間平均分配在周圍(每個部件周圍等間距)
spaceBetween:剩下空間平均分配在小部件中間(兩邊無間距,中間等間距)
spaceEvenly: 完全等間距)
crossAxisAlignment:交叉軸方向?qū)R(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`的居中展示。內(nèi)部元素默認是從左到右
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`的居中展示。內(nèi)部元素是從上到下
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設(shè)置為baseline時,需要添加textBaseline屬性(Row非必須,但Column是必須),可以設(shè)置為:
alphabetic:字母ideographic:中文(好像沒區(qū)別)
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軸(前后縱深方向)布局(設(shè)置多個大小不同的組件,就可以看到效果:)
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`的居中展示。內(nèi)部元素是從里到外
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來設(shè)置:具體的參數(shù)設(shè)置,可參考
positioned的構(gòu)造方法和內(nèi)部描述。
第一個子控件左上角展示,第二個子控件右下角展示,第三個子控件靠右,離頂部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高度無效)
- 現(xiàn)在,我們以
多個文本組件為例,體驗兩種情況:
- 組件內(nèi)
所有子組件都是Expanded,布局情況如何? - 組件內(nèi)
Expanded和其他組件共存,布局情況如何?
1. 全Expanded
- 在
Row組件中,child設(shè)置三個Text組件,字體和內(nèi)容都不一樣,但使用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
如果
超過屏幕寬度,我們需要對寬高做設(shè)置
3.2 AspectRatio 寬高比
-
AspectRatio寬高比部件,可以設(shè)置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, // 設(shè)置寬高比
child: Icon(Icons.add),
),
)
);
}
}
-
展示樣式:
image.png 我們也可以通過
屬性設(shè)定,來讀取寬高值。下面分析可變組件和不可變組件的本質(zhì)區(qū)別。
4. StatefulWidget可變部件 與 StatelessWidget不可變部件
-
StatelessWidget: 不可變部件,所有變量都是final修飾,不可以變更狀態(tài)
(實際每次變更狀態(tài),就是銷毀原部件,根據(jù)新初始值創(chuàng)建新不可變部件) -
StatefulWidget:可變部件,實際是狀態(tài)值(記錄UI狀態(tài))與不可變部件(描述UI)的組合。
(每一次狀態(tài)值變更,都觸發(fā)不可變部件的重新創(chuàng)建。而狀態(tài)值會跟隨StatefulWidget的銷毀而銷毀。)
所以實際上可變部件與不可變部件的區(qū)別就是可變部件 多記錄了狀態(tài)值,在UI上,本質(zhì)都是通過創(chuàng)建和銷毀 不可變部件來進行展示。
4.1 重寫計數(shù)器Demo
- 我們通過重寫
Flutter默認的計數(shù)器Demo,來體驗StatelessWidget不可變部件和StatefulWidget可變部件的區(qū)別:
4.1.1 main.dart創(chuàng)建
- 指定
APP入口為App(),使用MaterialApp構(gòu)建APP,將home根視圖設(shè)置為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創(chuàng)建(StatelessWidget不可變部件)
- 新建
state_manager_demo.dart文件,StateManagerDemo為StatelessWidget不可變部件,使用Scaffold導航控制器部件設(shè)置appBar導航欄、body內(nèi)容和floatingActionButton浮動按鈕。
floatingActionButton新增onPressed點擊事件,觸發(fā)回調(diào)函數(shù)(每次點擊,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),
// 點擊觸發(fā): 給一個無參回調(diào)函數(shù),可以直接給`(){}`,也可以外部創(chuàng)建一個函數(shù),傳入函數(shù)名
// 每次點擊,count+1,打印count值
onPressed: (){
count += 1;
print("count = $count");
},
)
);
}
}
- 當我們在
StatelessWidget不可變部件中創(chuàng)建變量未使用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
我們忽略不管(暫時不管,后面做比較),繼續(xù)運行,可以看到界面運行正常:
image.png
- 點擊
浮動按鈕 ?號,打印臺可以看到count數(shù)遞增了,但是頁面中心的UI未同步更新。

我們發(fā)現(xiàn),不用
final修飾變量count,也可以正常運行,并且count完成了計數(shù)任務(wù),但是界面UI未更新
- 如果我們使用
final聲明變量,就等同于swift中的let聲明,是不可變的,不能進行count+=1的操作。實際上,這是
Flutter的底層渲染原理決定的,StatelessWidget不可變部件,本身不支持記錄狀態(tài),每次都是銷毀再重新渲染UI,所以編譯器提示我們不可變部件內(nèi)全部使用final修飾。這樣從編碼層就減少錯誤的產(chǎn)生。
- 如果一定需要
記錄狀態(tài),請使用StatefulWidget可變部件。
4.1.3 StateManagerDemo創(chuàng)建(StatefulWidget可變部件)
- 上面說到,如果要
記錄狀態(tài),請使用StatefulWidget可變部件。
但StatefulWidget本身并不是直接改變UI,而是在StatelessWidget不可變部件的基礎(chǔ)上,多了一個管理狀態(tài)的State組件。這個組件記錄了狀態(tài),并決定了返回的UI樣式。 - 所以,
記錄狀態(tài)的變量(count)和返回組件的構(gòu)造方法(build),都應(yīng)該放在State中。 而StatefulWidget就是直接返回State根據(jù)當前數(shù)據(jù)build返回的UI部件。 - 需要補充說明的是: 我們需要
部件及時更新,需要在數(shù)據(jù)變動處,調(diào)用setState(){}更新狀態(tài)。
import 'package:flutter/material.dart';
class StateManagerDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 直接返回State根據(jù)最新數(shù)據(jù)build的組件
return _StateManagerState();
}
}
class _StateManagerState extends State<StateManagerDemo> {
// 記錄計數(shù)
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),
// 點擊觸發(fā): 給一個無參回調(diào)函數(shù),可以直接給`(){}`,也可以外部創(chuàng)建一個函數(shù),傳入函數(shù)名
// 每次點擊,count+1,打印count值
onPressed: (){
count += 1; // 數(shù)據(jù)變動
setState(() {}); // 更新狀態(tài)(自動觸發(fā)UI的重新渲染)
print("count = $count");
},
)
);
}
}
-
頁面樣式:
image.png
總結(jié):
StatelessWidget不可變部件:直接
build返回UI部件即可,變量都使用final修飾,不可更改。每次通過構(gòu)造方法傳入新數(shù)據(jù),都會進行舊部件的銷毀和新部件的創(chuàng)建。
StatefulWidget可變部件:
State組件管理狀態(tài),并build返回最新狀態(tài)的UI部件。每次狀態(tài)變更,需要setState(() {})更新UI部件
(渲染機制會自動銷毀舊UI部件,創(chuàng)建新UI部件,State的生命周期與StatefulWidget一致)
StatefulWidget可變部件只需要返回State對象即可
(實際是返回State中build的最新UI部件)
【思考】
實際上,在
Flutter開發(fā)中我們需要注意一些優(yōu)化點。
- 我們整個頁面,其實只有
+號按鈕點擊,觸發(fā)中心的計數(shù)UI的更新。并不需要每次數(shù)據(jù)變化,都更新整個外部部件。- 我們可以將
Chip更新為一個StatefulWidget可變部件,點擊+號時,通過回調(diào)讓Chip部件重新渲染即可。
本節(jié),我們主要分析自動布局的幾種方式,以及StatefulWidget和StatefulWidget的區(qū)別。
下一節(jié),Flutter入門四:搭建項目、資源調(diào)用、簡單開發(fā)。從項目入手,一步步理解和熟悉Flutter。











