1. 簡介
在介紹Flutter佈局之前,我們得先了解Flutter中的一些佈局相關的特性。
1.1 邊界約束(box constraints)
box constraints有人也翻譯為盒約束、箱約束,我個人還是覺得邊界約束可能更直觀一些。
Flutter中的邊界約束,是指widget可以按照指定限定條件,來決定自身如何佔用佈局空間。Flutter借鑑了很多React相關的東西,包括一些佈局思想,但是它自身沒有抽離出佈局樣式,而是用不同的widget去實現不同的佈局,將樣式嵌入widget中,使用者可以像搭積木一樣寫佈局,寫法上跟React很像,只不過沒了樣式的設定。 這樣做的好處,我覺得可能是為了統一的渲染。加入樣式,會讓佈局複雜不少,在渲染層面會降低很多效能。因此,Flutter在大的方向上,加入不同型別的佈局widget。在小的方向上,只給出很少的定製化的東西,將佈局限定在有限的範圍內,在完成佈局的同時,讓整個渲染能夠統一,加快了更新和渲染。 但是,缺點也是同樣明顯,少了很多靈活性,不同的佈局方式都被抽離出了widget,大家需要了解的widget非常多,增加了學習成本。
1.2 約束種類
在Flutter中,widget是由其底層的RenderBox渲染,渲染邊界的約束(Constraints)由父級給出,widget在這些約束下調整自身尺寸。約束包括最小最大寬高,尺寸則是具體的寬高。
在Android中,佈局的寬高限定有三種,match_parent、wrap_content以及具體尺寸。在Flutter中,也有這三種約束。
- 儘可能大的約束,例如Center、ListView等;
- 跟隨內容大小的約束,例如Transform、Opacity等;
- 指定尺寸的約束,例如Image、Text等;
但是,Flutter並沒有把widget處理的這麼絕對,這些約束條件包含在widget裡,不像Android可以在外面去指定。因此,一些widget,例如Container,會根據引數的不同,約束條件也不同。Container預設是儘可能大的,但是給定尺寸的話,就會優先使用具體值。不同的widget可能設定條件不同、其子widget不同,約束條件也會不一樣。Flutter將每種widget限制在不同的約束範圍裡,實際佈局的時候,還需要綜合去考慮。
2. 分類
按照約束條件來分類,很多widget不太好區分開來,官方也是根據其子元素的個數來分類。
- 單個子元素(child)的佈局,包括Container、Padding等18種(目前是2018年5月25日,後續我想肯定會增加的,下面類似);
- 多個子元素(children)的佈局,包括Row、Column等11種;
- layout helper,例如ListView.Builder,在元素多的時候,用這種方式更加的高效,類似Android的RecyclerView,有自動的回收機制。這種嚴格意義上不能算是一個種類,我覺得這種helper會越來越多。
2.1 優缺點
其中日常中用的多的,例如Container、Padding、Center、Align、Row、Column、Stack、ListView等也有上十種。
Flutter提供接近30多種不同的佈局widget,還是源於其對widget的定位(在此處不再闡述,想了解的,可以翻看筆者之前文章的介紹)。對比其他移動端的開發平臺,可以看出Flutter的佈局widget是巨多,這也是為什麼Flutter現在學習曲線很長的一個原因。
先來說下優點,統一渲染,更新效率更高。但是,對於普通開發者而言,是不會去太在乎這些的。效能高本來就是平臺應該提供的最基本的能力,難道不是你應該提供的嗎?
然後說下缺點吧,掌握起來還是非常費事的,佈局起來也是挺蛋疼的。常規的佈局還好,一到複雜的佈局,覺得這個也能實現,那個也能實現,最後不知道哪個可以實現。
3. widget詳解
在Flutter中,我們平時自定義的widget,一般都是繼承自StatefulWidget或StatelessWidget(並不是只有這兩種),這兩種widget也是目前最常用的兩種。如果一個控制元件自身狀態不會去改變,建立了就直接顯示,不會有色值、大小或者其他屬性的變化,這種widget一般都是繼承自StatelessWidget,常見的有Container、ScrollView等。如果一個控制元件需要動態的去改變或者相應一些狀態,例如點選態、色值、內容區域等,那麼一般都是繼承自StatefulWidget,常見的有CheckBox、AppBar、TabBar等。其實單純的從名字也可以看出這兩種widget的區別,這兩種widget都是繼承自Widget類。
3.1 Widget類
Widget類在Flutter中是非常重要的,繼承自Widget類的有PreferredSizeWidget、ProxyWidget、RenderObjectWidget、StatefulWidget、StatelessWidget。我們日常使用的絕大部分widget都是繼承自Widget類。
3.2 State
在說到StatefulWidget之前,先說下State。State的作用有兩點:
- 在widget構建的時候可以被同步讀取;
- 在widget的生命週期中可能會被改變。
3.2.1 State生命週期
State的生命週期有四種狀態:
- created:當State物件被建立時候,State.initState方法會被呼叫;
- initialized:當State物件被建立,但還沒有準備構建時,State.didChangeDependencies在這個時候會被呼叫;
- ready:State物件已經準備好了構建,State.dispose沒有被呼叫的時候;
- defunct:State.dispose被呼叫後,State物件不能夠被構建。
完整生命週期如下:
- 建立一個State物件時,會呼叫StatefulWidget.createState;
- 和一個BuildContext相關聯,可以認為被載入了(mounted);
- 呼叫initState;
- 呼叫didChangeDependencies;
- 經過上述步驟,State物件被完全的初始化了,呼叫build;
- 如果有需要,會呼叫didUpdateWidget;
- 如果處在開發模式,熱載入會呼叫reassemble;
- 如果它的子樹(subtree)包含需要被移除的State物件,會呼叫deactivate;
- 呼叫dispose,State物件以後都不會被構建;
- 當呼叫了dispose,State物件處於未載入(unmounted),已經被dispose的State物件沒有辦法被重新載入(remount)。
3.2.2 setState
State中比較重要的一個方法是setState,當修改狀態時,widget會被更新。比方說點選CheckBox,會出現選中和非選中狀態之間的切換,就是通過修改狀態來達到的。
檢視setState原始碼,在一些異常的情況下將會丟擲異常:
- 傳入的為null;
- 處在defunct階段;
- created階段還沒有被載入(mounted);
- 引數返回一個Future物件。
3.3 StatefulWidget和StatelessWidget
3.3.1 StatelessWidget
對於StatelessWidget,build方法會在如下三種情況下呼叫,
- widget第一次被插入到樹中;
- widget的父節點更改了配置(configuration);
- widget依賴的InheritedWidget改變了。
3.3.2 StatefulWidget
StatefulWidget的兩個主要類別:
- 在initState中建立資源,在dispose中銷燬,但是不依賴於InheritedWidget或者呼叫setState方法,這類widget基本上用在一個應用或者頁面的root;
- 使用setState或者依賴於InheritedWidget,這種在營業生命週期中會被重建(rebuild)很多次。
4. 如何佈局
每個頁面設計都不一樣,相同頁面可選擇的佈局方式也不一樣,如果單純的說應該如何去佈局,我覺得不現實,大家可以參考下Flutter官方的佈局教程。