本文主要介紹了Flutter布局相關(guān)的內(nèi)容,對(duì)相關(guān)知識(shí)點(diǎn)進(jìn)行了梳理,并從實(shí)際例子觸發(fā),進(jìn)一步講解該如何去進(jìn)行布局。
1. 簡(jiǎn)介
在介紹Flutter布局之前,我們得先了解Flutter中的一些布局相關(guān)的特性。
1.1 邊界約束(box constraints)
box constraints有人也翻譯為盒約束、箱約束,我個(gè)人還是覺得邊界約束可能更直觀一些。
Flutter中的邊界約束,是指widget可以按照指定限定條件,來決定自身如何占用布局空間。Flutter借鑒了很多React相關(guān)的東西,包括一些布局思想,但是它自身沒有抽離出布局樣式,而是用不同的widget去實(shí)現(xiàn)不同的布局,將樣式嵌入widget中,用戶可以像搭積木一樣寫布局,寫法上跟React很像,只不過沒了樣式的設(shè)定。
這樣做的好處,我覺得可能是為了統(tǒng)一的渲染。加入樣式,會(huì)讓布局復(fù)雜不少,在渲染層面會(huì)降低很多性能。因此,F(xiàn)lutter在大的方向上,加入不同類型的布局widget。在小的方向上,只給出很少的定制化的東西,將布局限定在有限的范圍內(nèi),在完成布局的同時(shí),讓整個(gè)渲染能夠統(tǒng)一,加快了更新和渲染。
但是,缺點(diǎn)也是同樣明顯,少了很多靈活性,不同的布局方式都被抽離出了widget,大家需要了解的widget非常多,增加了學(xué)習(xí)成本。
1.2 約束種類
在Flutter中,widget是由其底層的RenderBox渲染,渲染邊界的約束(Constraints)由父級(jí)給出,widget在這些約束下調(diào)整自身尺寸。約束包括最小最大寬高,尺寸則是具體的寬高。
在Android中,布局的寬高限定有三種,match_parent、wrap_content以及具體尺寸。在Flutter中,也有這三種約束。
- 盡可能大的約束,例如Center、ListView等;
- 跟隨內(nèi)容大小的約束,例如Transform、Opacity等;
- 指定尺寸的約束,例如Image、Text等;
但是,F(xiàn)lutter并沒有把widget處理的這么絕對(duì),這些約束條件包含在widget里,不像Android可以在外面去指定。因此,一些widget,例如Container,會(huì)根據(jù)參數(shù)的不同,約束條件也不同。Container默認(rèn)是盡可能大的,但是給定尺寸的話,就會(huì)優(yōu)先使用具體值。不同的widget可能設(shè)置條件不同、其子widget不同,約束條件也會(huì)不一樣。Flutter將每種widget限制在不同的約束范圍里,實(shí)際布局的時(shí)候,還需要綜合去考慮。
2. 分類
按照約束條件來分類,很多widget不太好區(qū)分開來,官方也是根據(jù)其子元素的個(gè)數(shù)來分類。
- 單個(gè)子元素(child)的布局,包括Container、Padding等18種(目前是2018年5月25日,后續(xù)我想肯定會(huì)增加的,下面類似);
- 多個(gè)子元素(children)的布局,包括Row、Column等11種;
- layout helper,例如ListView.Builder,在元素多的時(shí)候,用這種方式更加的高效,類似Android的RecyclerView,有自動(dòng)的回收機(jī)制。這種嚴(yán)格意義上不能算是一個(gè)種類,我覺得這種helper會(huì)越來越多。
2.1 優(yōu)缺點(diǎn)
其中日常中用的多的,例如Container、Padding、Center、Align、Row、Column、Stack、ListView等也有上十種。
Flutter提供接近30多種不同的布局widget,還是源于其對(duì)widget的定位(在此處不再闡述,想了解的,可以翻看筆者之前文章的介紹)。對(duì)比其他移動(dòng)端的開發(fā)平臺(tái),可以看出Flutter的布局widget是巨多,這也是為什么Flutter現(xiàn)在學(xué)習(xí)曲線很長(zhǎng)的一個(gè)原因。
先來說下優(yōu)點(diǎn),統(tǒng)一渲染,更新效率更高。但是,對(duì)于普通開發(fā)者而言,是不會(huì)去太在乎這些的。性能高本來就是平臺(tái)應(yīng)該提供的最基本的能力,難道不是你應(yīng)該提供的嗎?
然后說下缺點(diǎn)吧,掌握起來還是非常費(fèi)事的,布局起來也是挺蛋疼的。常規(guī)的布局還好,一到復(fù)雜的布局,覺得這個(gè)也能實(shí)現(xiàn),那個(gè)也能實(shí)現(xiàn),最后不知道哪個(gè)可以實(shí)現(xiàn)。
2.2 個(gè)人看法
Flutter對(duì)于性能的優(yōu)化,把平臺(tái)側(cè)的一些成本轉(zhuǎn)接到開發(fā)者身上,不過呢,現(xiàn)在也是Flutter的初期,可以看出,整體的設(shè)計(jì)思路還是非常好的,也只有谷歌這種輪子大廠才敢這么干。但是,很明顯少了些人為關(guān)懷,不同widget間約束條件穿插著,也可以說Flutter布局控件種類的增加,是其不斷的打補(bǔ)丁導(dǎo)致的,后續(xù)的各種helper,也是為了填坑,這一塊兒Flutter顯然沒有處理的很好。
但是,凡事都有個(gè)過程,不能說Flutter這些地方做的不好,只是目前看起來比較混亂,理想的架構(gòu)設(shè)計(jì),落地下來,可能就不是那么簡(jiǎn)單,開發(fā)者的需求千差萬別,有了生態(tài),什么都好說,當(dāng)然這個(gè)過程,預(yù)計(jì)是會(huì)非常的緩慢。
3. widget詳解
在Flutter中,我們平時(shí)自定義的widget,一般都是繼承自StatefulWidget或StatelessWidget(并不是只有這兩種),這兩種widget也是目前最常用的兩種。如果一個(gè)控件自身狀態(tài)不會(huì)去改變,創(chuàng)建了就直接顯示,不會(huì)有色值、大小或者其他屬性的變化,這種widget一般都是繼承自StatelessWidget,常見的有Container、ScrollView等。如果一個(gè)控件需要?jiǎng)討B(tài)的去改變或者相應(yīng)一些狀態(tài),例如點(diǎn)擊態(tài)、色值、內(nèi)容區(qū)域等,那么一般都是繼承自StatefulWidget,常見的有CheckBox、AppBar、TabBar等。其實(shí)單純的從名字也可以看出這兩種widget的區(qū)別,這兩種widget都是繼承自Widget類。
3.1 Widget類
Widget類在Flutter中是非常重要的,繼承自Widget類的有PreferredSizeWidget、ProxyWidget、RenderObjectWidget、StatefulWidget、StatelessWidget。我們?nèi)粘J褂玫慕^大部分widget都是繼承自Widget類,
查看Widget類源碼,內(nèi)部實(shí)現(xiàn)非常簡(jiǎn)單,構(gòu)造函數(shù)如下
const Widget({ this.key });
final Key key;
這個(gè)key的作用,注視上寫的很清楚,是用來控制在widget樹中替換widget的時(shí)候使用的。其中Key類是Widget、Element以及SemanticsNode的唯一標(biāo)識(shí)符,繼承自Key的還有LocalKey以及GlobalKey。
3.2 State
在說到StatefulWidget之前,先說下State。State的作用有兩點(diǎn):
- 在widget構(gòu)建的時(shí)候可以被同步讀?。?/li>
- 在widget的生命周期中可能會(huì)被改變。
3.2.1 State生命周期
State的生命周期有四種狀態(tài):
- created:當(dāng)State對(duì)象被創(chuàng)建時(shí)候,State.initState方法會(huì)被調(diào)用;
- initialized:當(dāng)State對(duì)象被創(chuàng)建,但還沒有準(zhǔn)備構(gòu)建時(shí),State.didChangeDependencies在這個(gè)時(shí)候會(huì)被調(diào)用;
- ready:State對(duì)象已經(jīng)準(zhǔn)備好了構(gòu)建,State.dispose沒有被調(diào)用的時(shí)候;
- defunct:State.dispose被調(diào)用后,State對(duì)象不能夠被構(gòu)建。

完整生命周期如下:
- 創(chuàng)建一個(gè)State對(duì)象時(shí),會(huì)調(diào)用StatefulWidget.createState;
- 和一個(gè)BuildContext相關(guān)聯(lián),可以認(rèn)為被加載了(mounted);
- 調(diào)用initState;
- 調(diào)用didChangeDependencies;
- 經(jīng)過上述步驟,State對(duì)象被完全的初始化了,調(diào)用build;
- 如果有需要,會(huì)調(diào)用didUpdateWidget;
- 如果處在開發(fā)模式,熱加載會(huì)調(diào)用reassemble;
- 如果它的子樹(subtree)包含需要被移除的State對(duì)象,會(huì)調(diào)用deactivate;
- 調(diào)用dispose,State對(duì)象以后都不會(huì)被構(gòu)建;
- 當(dāng)調(diào)用了dispose,State對(duì)象處于未加載(unmounted),已經(jīng)被dispose的State對(duì)象沒有辦法被重新加載(remount)。
3.2.2 setState
State中比較重要的一個(gè)方法是setState,當(dāng)修改狀態(tài)時(shí),widget會(huì)被更新。比方說點(diǎn)擊CheckBox,會(huì)出現(xiàn)選中和非選中狀態(tài)之間的切換,就是通過修改狀態(tài)來達(dá)到的。
查看setState源碼,在一些異常的情況下將會(huì)拋出異常:
- 傳入的為null;
- 處在defunct階段;
- created階段還沒有被加載(mounted);
- 參數(shù)返回一個(gè)Future對(duì)象。
檢查完一系列異常后,最后調(diào)用代碼如下:
_element.markNeedsBuild();
markNeedsBuild內(nèi)部,則是通過標(biāo)記element為diry,在下一幀的時(shí)候重建(rebuild)??梢钥闯鰏etState并不是立即生效,它只是將widget進(jìn)行了標(biāo)記,真正的rebuild操作,則是等到下一幀的時(shí)候才會(huì)去進(jìn)行。
3.3 StatefulWidget和StatelessWidget
StatefulWidget和StatelessWidget如下所示

一個(gè)StatelessWidget可以用多個(gè)不同的BuildContext構(gòu)建,而一個(gè)StatefulWidget會(huì)為每個(gè)BuildContext創(chuàng)建一個(gè)State對(duì)象。
3.3.1 StatelessWidget
對(duì)于StatelessWidget,build方法會(huì)在如下三種情況下調(diào)用,
- widget第一次被插入到樹中;
- widget的父節(jié)點(diǎn)更改了配置(configuration);
- widget依賴的InheritedWidget改變了。
class GreenFrog extends StatelessWidget {
const GreenFrog({ Key key }) : super(key: key);
@override
Widget build(BuildContext context) {
return new Container(color: const Color(0xFF2DBD3A));
}
}
3.3.2 StatefulWidget
StatefulWidget的兩個(gè)主要類別:
- 在initState中創(chuàng)建資源,在dispose中銷毀,但是不依賴于InheritedWidget或者調(diào)用setState方法,這類widget基本上用在一個(gè)應(yīng)用或者頁面的root;
- 使用setState或者依賴于InheritedWidget,這種在營(yíng)業(yè)生命周期中會(huì)被重建(rebuild)很多次。
class YellowBird extends StatefulWidget {
const YellowBird({ Key key }) : super(key: key);
@override
_YellowBirdState createState() => new _YellowBirdState();
}
class _YellowBirdState extends State<YellowBird> {
@override
Widget build(BuildContext context) {
return new Container(color: const Color(0xFFFFE306));
}
}
4. 如何布局
每個(gè)頁面設(shè)計(jì)都不一樣,相同頁面可選擇的布局方式也不一樣,如果單純的說應(yīng)該如何去布局,我覺得不現(xiàn)實(shí),大家可以參考下Flutter官方的布局教程。接下來,筆者,通過一個(gè)簡(jiǎn)單的頁面,來一步一步的拆解布局的流程。整個(gè)過程,基本上按照拆解、組件封裝、具體布局這三步來的。
4.1 拆解

4.1.1 整體拆解
根據(jù)設(shè)計(jì)圖,可以看出整體時(shí)分行展示的,因此最外層是一個(gè)Column元素
- 第一行為標(biāo)題,涉及到不對(duì)稱的布局,可以用一個(gè)Stack或者Row來進(jìn)行,用Row的話,則需要右邊填上一個(gè)空白的widget占位。也可能會(huì)使用AppBar,將底部陰影去掉也能實(shí)現(xiàn)相同效果;
- 第二行可以看作一個(gè)Row,分兩塊布局。右邊部分,涉及到疊加,會(huì)考慮Stack;
- 第三行比較復(fù)雜,整體看,也是一行一行進(jìn)行展示的,因此最外層時(shí)一個(gè)Column。中間的文本部分需要根據(jù)個(gè)數(shù)自動(dòng)換行,因此考慮使用Wrap。預(yù)習(xí)這個(gè)地方涉及到疊加,考慮Stack實(shí)現(xiàn);
- 第四行可以看作一個(gè)Row,分三塊進(jìn)行布局;
- 第五行可以看作一個(gè)Row,分兩塊布局。
每一行之間的間隔,則可以考慮用Padding或者Container來設(shè)置。
通過上面這樣一步一步的分析后,基本上對(duì)大致的布局有了一個(gè)了解,最外層的控件大致選對(duì)(只要能實(shí)現(xiàn)的話,就是復(fù)雜度以及效率的問題),然后一步一步的拆解每一行的元素,如果有重復(fù)的或者覺得可以封裝出來的部分,則進(jìn)行下一步。
4.1.2 局部拆解
每一行的拆解,大致也是按照這個(gè)思路來進(jìn)行,因此筆者在這里就不做講解了。
4.2 組件封裝
例如上面,筆者想對(duì)第四行的這種展示進(jìn)行封裝,覺得今后的布局可能會(huì)用到,因此在這一步,可以先把這一塊兒抽離出一個(gè)控件。利用Row的mainAxisAlignment以及Expanded來實(shí)現(xiàn)這種效果,具體的實(shí)現(xiàn)筆者不再詳細(xì)的描述了。
經(jīng)過這一步,整體的規(guī)劃設(shè)計(jì)圖已經(jīng)有了,各個(gè)組件也都有了,接下來的工作就是組裝了。
4.3 具體布局
具體布局設(shè)計(jì)到一些細(xì)節(jié)的地方,例如間隔(Padding或者Container)、居左居右居中(Align)、點(diǎn)擊事件(GestureDetector)以及圓角(ClipRRect)等一些特殊情況,基本上就是嵌套,一層一層去實(shí)現(xiàn)。
在實(shí)際布局中,筆者實(shí)際使用的是Scaffold,頂部的AppBar將陰影直接去掉即可實(shí)現(xiàn)效果,body部分則實(shí)現(xiàn)2-5行的內(nèi)容。最外層套一個(gè)Column也能實(shí)現(xiàn),本質(zhì)上都沒什么區(qū)別,運(yùn)行效果圖如下所示。

4.4 代碼
5. 后話
筆者建了一個(gè)flutter學(xué)習(xí)相關(guān)的項(xiàng)目,github地址,里面包含了筆者寫的關(guān)于flutter學(xué)習(xí)相關(guān)的一些文章,會(huì)定期更新,也會(huì)上傳一些學(xué)習(xí)demo,歡迎大家關(guān)注。