詳解Flutter Widget
概述:
所有的一切都可以被稱為widget
在開發(fā) Flutter 應(yīng)用過程中,接觸最多的無疑就是Widget
,是『描述』 Flutter UI 的基本單元,通過Widget
可以做到:
- 描述 UI 的層級結(jié)構(gòu) (通過
Widget
嵌套); - 定制 UI 的具體樣式 (如:
font
、color
等); - 指導(dǎo) UI 的布局過程 (如:
padding
、center
等); - …
Google 在設(shè)計Widget
時,還賦予它一些鮮明的特點:
- 聲明式 UI —— 相對于傳統(tǒng) Native 開發(fā)中的命令式 UI,聲明式 UI 有不少優(yōu)勢,如:開發(fā)效率顯著提升、UI 可維護(hù)性明顯加強(qiáng)等;
- 不可變性 —— Flutter 中所有
Widget
都是不可變的(immutable),即其內(nèi)部成員都是不可變的(final
),對于變化的部分需要通過「Stateful Widget-State」的方式實現(xiàn); - 組合大于繼承 ——
Widget
設(shè)計遵循組合大于繼承這一優(yōu)秀的設(shè)計理念,通過將多個功能相對單一的Widget
組合起來便可得到功能相對復(fù)雜的Widget
。
Widget的本質(zhì):
在Widget源碼中有這樣一段注釋:
這段注釋闡明了Widget
的本質(zhì):用于配置Element
的,Widget
本質(zhì)上是 UI 的配置信息 (附帶部分業(yè)務(wù)邏輯)。
我們通常會將通過
Widget
描述的 UI 層級結(jié)構(gòu)稱之為「Widget Tree」,但與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,實質(zhì)上并不存在「Widget Tree」。為了描述方便,將 Widget 組合描述的 UI 層級結(jié)構(gòu)稱之為「Widget Tree」,也未嘗不可。
分類:
Widget
Widget
,所有 Widget 的基類。
如上圖所示,在 Widget
基類中有 3 個重要的方法 (屬性):
- Key key —— 在同一父節(jié)點下,用作兄弟節(jié)點間的唯一標(biāo)識,主要用于控制當(dāng) Widget 更新時,對應(yīng)的 Element 如何處理 (是更新還是新建)。若某 Widget 是其「Parent Widget」唯一的子節(jié)點時,一般不用設(shè)置 key;
GlobalKey 是一類較特殊的 key,在介紹 Element 時會附帶介紹。
- Element createElement() —— 每個
Widget
都有一個與之對應(yīng)的Element
,由該方法負(fù)責(zé)創(chuàng)建,createElement
可以理解為設(shè)計模式中的工廠方法,具體的Element
類型由對應(yīng)的Widget
子類負(fù)責(zé)創(chuàng)建; - static bool canUpdate(Widget oldWidget, Widget newWidget) —— 是否可以用 new widget 修改前一幀用 old widget 生成的 Element,而不是創(chuàng)建新的 Element,
Widget
類的默認(rèn)實現(xiàn)為:2個Widget
的runtimeType
與key
都相等時,返回true
,即可以直接更新 (key 為 null 時,認(rèn)為相等)。
上述更新流程,同樣在介紹 Element 時會重點分析。
StatelessWidget
無狀態(tài)-組合型 Widget,由其build
方法描述組合 UI 的層級結(jié)構(gòu)。在其生命周期內(nèi)狀態(tài)不可變。
/// A widget that does not require mutable state. /// /// A stateless widget is a widget that describes part of the user interface by /// building a constellation of other widgets that describe the user interface /// more concretely. The building process continues recursively until the /// description of the user interface is fully concrete (e.g., consists /// entirely of [RenderObjectWidget]s, which describe concrete [RenderObject]s).
具體是兩個方法:
- StatelessElement createElement() ——「Stateless Widget」對應(yīng)的 Element 為
StatelessElement
,一般情況下StatelessWidget
子類不必重寫該方法,即子類對應(yīng)的 Element 也是StatelessElement
;
@override StatelessElement createElement() => StatelessElement(this);
- Widget build(BuildContext context) —— 算是 Flutter 體系中的核心方法之一
@protected Widget build(BuildContext context);
以『聲明式 UI』的形式描述了該組合式 Widget 的 UI 層級結(jié)構(gòu)及樣式信息,也是開發(fā) Flutter 應(yīng)用的主要工作『場所』。該方法在 3 種情況下被調(diào)用:
- Widget 第一次被加入到 Widget Tree 中 (更準(zhǔn)確地說是其對應(yīng)的 Element 被加入到 Element Tree 時,即 Element 被掛載『mount』時);
- 「Parent Widget」修改了其配置信息;
- 該 Widget 依賴的「Inherited Widget」發(fā)生變化時。
當(dāng)「Parent Widget」或 依賴的「Inherited Widget」頻繁變化時,build
方法也會頻繁被調(diào)用。因此,提升build
方法的性能就顯得十分重要,F(xiàn)lutter 官方給出了幾點建議:
- *減少不必要的中間節(jié)點,即減少 UI 的層級,*如:對于「Single Child Widget」,沒必要通過組合「Row」、「Column」、「Padding」、「SizedBox」等復(fù)雜的 Widget 達(dá)到某種布局的目標(biāo),或許通過簡單的「Align」、「CustomSingleChildLayout」即可實現(xiàn)。又或者,為了實現(xiàn)某種復(fù)雜精細(xì)的 UI 效果,不一定要通過組合多個「Container」,再附加「Decoration」來實現(xiàn),通過 「CustomPaint」自定義或許是更好的選擇;
- *盡可能使用
const
Widget,*為 Widget 提供const
構(gòu)造方法;
關(guān)于 const constructor 可以看看我這篇文章。
- 必要時,*可以將「Stateless Widget」重構(gòu)成「Stateful Widget」,*以便可以使用「Stateful Widget」中一些特定的優(yōu)化手法,如:緩存「sub trees」的公共部分,并在改變樹結(jié)構(gòu)時使用
GlobalKey
; - *盡量減小 rebuilt 范圍,*如:某個 Widget 因使用了「Inherited Widget」,導(dǎo)致頻繁 rebuilt,可以將真正依賴「Inherited Widget」的部分提取出來,封裝成更小的獨立 Widget,并盡量將該獨立 Widget 推向樹的葉子節(jié)點,以便減小 rebuilt 時受影響的范圍。
StatefulWidget
有狀態(tài)-組合型 Widget,但要注意的是StatefulWidget
本身還是不可變的,其可變狀態(tài)存在于State
中。
/// A widget that has mutable state. /// /// State is information that (1) can be read synchronously when the widget is /// built and (2) might change during the lifetime of the widget. It is the /// responsibility of the widget implementer to ensure that the [State] is /// promptly notified when such state changes, using [State.setState].
具體有兩個方法:
- StatefulElement createElement() ——「Stateful Widget」對應(yīng)的 Element 為
StatefulElement
,一般情況下StatefulWidget
子類不用重寫該方法,即子類對應(yīng)的Element 也是StatefulElement
;
@override StatefulElement createElement() => StatefulElement(this);
- State createState() —— 創(chuàng)建對應(yīng)的 State,該方法在
StatefulElement
的構(gòu)造方法中被調(diào)用。可以簡單地理解為當(dāng)「Stateful Widget」被添加到 Widget Tree 時會調(diào)用該方法。
@protected @factory State createState(); // ignore: no_logic_in_create_state, this is the original sin
StatefulElement(StatefulWidget widget) : _state = widget.createState(), super(widget) { _state._element = this; _state._widget = widget; }
實際上是「Stateful Widget」對應(yīng)的「Stateful Element」被添加到 Element Tree 時,伴隨「Stateful Element」的初始化,createState
方法被調(diào)用。從后文可知一個 Widget 實例可以對應(yīng)多個 Element 實例 (也就是同一份配置信息 (Widget) 可以在 Element Tree 上不同位置配置多個 Element 節(jié)點),因此,createState
方法在「Stateful Widget」生命周期內(nèi)可能會被調(diào)用多次。
另外,需要注意的是配有GlobalKey
的 Widget 對應(yīng)的 Element 在整個 Element Tree 中只有一個實例。
State
有狀態(tài)小部件 的邏輯和Stateful Widget
State 用于處理「Stateful Widget」的業(yè)務(wù)邏輯以及可變狀態(tài)
由于其內(nèi)部狀態(tài)是可變的,故 State 有較復(fù)雜的生命周期:
如上圖,State 的生命周期大致可以分為 8 個階段:
- 在對應(yīng)的「Stateful Element」被掛載 (mount) 到樹上時,通過
StatefulElement.constructor
–>StatefulWidget.createState
創(chuàng)建 State 實例;
從
StatefulElement.constructor
中的_state._element = this;
可知,State._emelent
指向了對應(yīng)的 Element 實例,而我們熟知的State.context
引用的就是這個_element
:BuildContext get context => _element;
。
State
實例與Element
實例間的綁定關(guān)系一經(jīng)確定,在整個生命周期內(nèi)不會再變了 (Element 對應(yīng)的 Widget 可能會變,但對應(yīng)的 State 永遠(yuǎn)不會變),期間,Element
可以在樹上移動,但上述關(guān)系不會變 (即「Stateful Element」是帶著狀態(tài)移動的)。
- StatefulElement 在掛載過程中接著會調(diào)用
State.initState
,子類可以重寫該方法執(zhí)行相關(guān)的初始化操作 (此時可以引用context
、widget
屬性); - 同樣在掛載過程中會調(diào)用
State.didChangeDependencies
,該方法在 State 依賴的對象 (如:「Inherited Widget」) 狀態(tài)發(fā)生變化時也會被調(diào)用,*子類很少需要重寫該方法,*除非有非常耗時不宜在build
中進(jìn)行的操作,因為在依賴有變化時build
方法也會被調(diào)用; - 此時,State 初始化已完成,其
build
方法此后可能會被多次調(diào)用,在狀態(tài)變化時 State 可通過setState
方法來觸發(fā)其子樹的重建; - 此時,「element tree」、「renderobject tree」、「layer tree」已構(gòu)建完成,完整的 UI 應(yīng)該已呈現(xiàn)出來。此后因為變化,「element tree」中「parent element」可能會對樹上該位置的節(jié)點用新配置 (Widget) 進(jìn)行重建,當(dāng)新老配置 (oldWidget、newWidget)具有相同的「runtimeType」&&「key」時,framework 會用 newWidget 替換 oldWidget,并觸發(fā)一系列的更新操作 (在子樹上遞歸進(jìn)行)。同時,
State.didUpdateWidget
方法被調(diào)用,子類重寫該方法去響應(yīng) Widget 的變化;
上述 3 棵樹以及更新流程在后續(xù)文章中會有詳細(xì)介紹
- 在 UI 更新過程中,任何節(jié)點都有被移除的可能,State 也會隨之移除,(如上一步中「runtimeType」||「key」不相等時)。此時會調(diào)用
State.deactivate
方法,由于被移除的節(jié)點可能會被重新插入樹中某個新的位置上,故子類重寫該方法以清理與節(jié)點位置相關(guān)的信息 (如:該 State 對其他 element 的引用)、同時,不應(yīng)在該方法中做資源清理;
重新插入操作必須在當(dāng)前幀動畫結(jié)束之前
- 當(dāng)節(jié)點被重新插入樹中時,
State.build
方法被再次調(diào)用; - 對于在當(dāng)前幀動畫結(jié)束時尚未被重新插入的節(jié)點,
State.dispose
方法被執(zhí)行,State 生命周期隨之結(jié)束,此后再調(diào)用State.setState
方法將報錯。子類重寫該方法以釋放任何占用的資源。
至此,State 中的核心方法基本都已在上述過程中介紹了,下面重點看一下setState
方法:
void setState(VoidCallback fn) { assert(fn != null); assert(() { if (_debugLifecycleState == _StateLifecycle.defunct) { throw FlutterError.fromParts(<DiagnosticsNode>[...]); } if (_debugLifecycleState == _StateLifecycle.created && !mounted) { throw FlutterError.fromParts(<DiagnosticsNode>[...]); } return true; }()); final dynamic result = fn() as dynamic; assert(() { if (result is Future) { throw FlutterError.fromParts(<DiagnosticsNode>[...]); } return true; }()); _element.markNeedsBuild(); }
- 從上述源碼可以看到,關(guān)于
setState
方法有幾點值得關(guān)注: - 在
State.dispose
后不能調(diào)用setState
(第 4 行); - 在 State 的構(gòu)造方法中不能調(diào)用
setState
(第 7 行); setState
方法的回調(diào)函數(shù) (fn
) 不能是異步的 (返回值為Future
),原因很簡單,因為從流程設(shè)計上 framework 需要根據(jù)回調(diào)函數(shù)產(chǎn)生的新狀態(tài)去刷新 UI (第 14 行);- 通過
setState
方法之所以能更新 UI,是在其內(nèi)部調(diào)用_element.markNeedsBuild()
實現(xiàn)的 (具體過程在介紹 Element 時再詳細(xì)分析)。
關(guān)于 State 最后再強(qiáng)調(diào) 2 點:
- 若State.build方法依賴了自身狀態(tài)會變化的對象,如:ChangeNotifier、Stream或其他可以被訂閱的對象,需要確保在initState、didUpdateWidget、dispose
等 3 方法間有正確的訂閱 (subscribe) 與取消訂閱 (unsubscribe) 的操作:
在initState
中執(zhí)行 subscribe;
如果關(guān)聯(lián)的「Stateful Widget」與訂閱有關(guān),在didUpdateWidget
中先取消舊的訂閱,再執(zhí)行新的訂閱;
在dispose
中執(zhí)行 unsubscribe。
- 在
State.initState
方法中不能調(diào)用BuildContext.dependOnInheritedWidgetOfExactType
,但State.didChangeDependencies
會隨之執(zhí)行,在該方法中可以調(diào)用。
ParentDataWidget
ParentDataWidget
以及下面要介紹的InheritedElement
都繼承自ProxyWidget
,由于ProxyWidget
作為抽象基類本身沒有任何功能,故下面直接介紹ParentDataWidget
、InheritedElement
。
/// Base class for widgets that hook [ParentData] information to children of/// [RenderObjectWidget]s.
ParentDataWidget
作為 Proxy 型 Widget,其功能主要是為其他 Widget 提供ParentData
信息。雖然其 child widget 不一定是 RenderObejctWidget 類型,但其提供的ParentData
信息最終都會落地到 RenderObejctWidget 類型子孫 Widget 上。
ParentData 是『parent renderobject』在 layout『child renderobject』時使用的輔助定位信息,詳細(xì)信息會在介紹 RenderObject 時介紹。
void attachRenderObject(dynamic newSlot) { assert(_ancestorRenderObjectElement == null); _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot); final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement(); if (parentDataElement != null) _updateParentData(parentDataElement.widget); } ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() { Element ancestor = _parent; while (ancestor != null && ancestor is! RenderObjectElement) { if (ancestor is ParentDataElement<RenderObjectWidget>) return ancestor; ancestor = ancestor._parent; } return null; } void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) { parentData.applyParentData(renderObject); }
上面這段代碼來自RenderObjectElement
,可以看到在其attachRenderObject
方法第 6 行從祖先節(jié)點找ParentDataElement
,如果找到就用其 Widget(ParentDataWidget) 中的 parentData 信息去設(shè)置 Render Obejct。在查找過程中如查到RenderObjectElement
(第 13 行),說明當(dāng)前 RenderObject 沒有 Parent Data 信息。
最終會調(diào)用到ParentDataWidget.applyParentData(RenderObject renderObject)
,子類需要重寫該方法,以便設(shè)置對應(yīng)RenderObject.parentData
。
來看個例子,通常配合Stack
使用的Positioned
(繼承自ParentDataWidget):
void applyParentData(RenderObject renderObject) { assert(renderObject.parentData is StackParentData); final StackParentData parentData = renderObject.parentData; bool needsLayout = false; if (parentData.left != left) { parentData.left = left; needsLayout = true; } ... if (parentData.width != width) { parentData.width = width; needsLayout = true; } ... if (needsLayout) { final AbstractNode targetParent = renderObject.parent; if (targetParent is RenderObject) targetParent.markNeedsLayout(); } }
可以看到,Positioned
在必要時將自己的屬性賦值給了對應(yīng)的RenderObject.parentData
(此處是StackParentData
),并對「parent render object」調(diào)用markNeedsLayout
(第 19 行),以便重新 layout,畢竟修改了布局相關(guān)的信息。
abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget
如上所示,ParentDataWidget
在定義上使用了泛型<T extends RenderObjectWidget>
,其背后的含義是:
從當(dāng)前ParentDataWidget
節(jié)點向上追溯形成的祖先節(jié)點鏈(『parent widget chain』)上,在 2 個ParentDataWidget
類型的節(jié)點形成的鏈上至少要有一個『RenderObject Widget』類型的節(jié)點。因為一個『RenderObject Widget』不能接受來自 2 個及以上『ParentData Widget』的信息。
/// Base class for widgets that efficiently propagate information down the tree. // To obtain the nearest instance of a particular type of inherited widget from ///a build context, use [BuildContext.dependOnInheritedWidgetOfExactType].
InheritedWidget 用于在樹上向下傳遞數(shù)據(jù)。
通過BuildContext.dependOnInheritedWidgetOfExactType
可以獲取最近的「Inherited Widget」,需要注意的是通過這種方式獲取「Inherited Widget」時,當(dāng)「Inherited Widget」?fàn)顟B(tài)有變化時,會導(dǎo)致該引用方 rebuild。
具體原理在介紹 Element 時會詳細(xì)分析。
通常,為了使用方便會「Inherited Widget」會提供靜態(tài)方法of
,在該方法中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType
。of
方法可以直接返回「Inherited Widget」,也可以是具體的數(shù)據(jù)。
有時,「Inherited Widget」是作為另一個類的實現(xiàn)細(xì)節(jié)而存在的,其本身是私有的(外部不可見),此時of
方法就會放到對外公開的類上。最典型的例子就是Theme
,其本身是StatelessWidget
類型,但其內(nèi)部創(chuàng)建了一個「Inherited Widget」:_InheritedTheme
,of
方法就定義在上Theme
上:
static MediaQueryData of(BuildContext context, { bool nullOk = false }) { final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>(); if (query != null) return query.data; if (nullOk) return null; }
該of
方法返回的是ThemeData
類型的具體數(shù)據(jù),并在其內(nèi)部首先調(diào)用了BuildContext.dependOnInheritedWidgetOfExactType
。
我們經(jīng)常使用的「Inherited Widget」莫過于MediaQuery
,同樣提供了of
方法:
- InheritedElement createElement() ——「Inherited Widget」對應(yīng)的 Element 為
InheritedElement
,一般情況下InheritedElement
子類不用重寫該方法; - bool updateShouldNotify(covariant InheritedWidget oldWidget) —— 在「Inherited Widget」rebuilt 時判斷是否需要 rebuilt 那些依賴它的 Widget;
如下是MediaQuery.updateShouldNotify
的實現(xiàn),在新老Widget.data
不相等時才 rebuilt 那依賴的 Widget。
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
RenderObjectWidget
真正與渲染相關(guān)的 Widget,屬于最核心的類型,一切其他類型的 Widget 要渲染到屏幕上,最終都要回歸到該類型的 Widget 上。
- RenderObjectElement createElement() ——「RenderObject Widget」對應(yīng)的 Element 為
RenderObjectElement
,由于RenderObjectElement
也是抽象類,故子類需要重寫該方法; - RenderObject createRenderObject(BuildContext context) —— 核心方法,創(chuàng)建 Render Widget 對應(yīng)的 Render Object,同樣子類需要重寫該方法。該方法在對應(yīng)的 Element 被掛載到樹上時調(diào)用(
Element.mount
),即在 Element 掛載過程中同步構(gòu)建了「Render Tree」(詳細(xì)過程后續(xù)文章會詳細(xì)分析);
@overrideRenderFlex createRenderObject(BuildContext context) { return RenderFlex( direction: direction, mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, textDirection: getEffectiveTextDirection(context), verticalDirection: verticalDirection, textBaseline: textBaseline, ) ; }
上面是Flex.createRenderObject
的源碼,真實感受一下 (還是代碼更有感覺)。可以看到,用Flex
的信息(配置)初始化了RenderFlex
。
Flex
是Row
、Column
的基類,RenderFlex
繼承自RenderBox
,后者繼續(xù)自RenderObject
。
- void updateRenderObject(BuildContext context, covariant RenderObject renderObject) —— 核心方法,在 Widget 更新后,修改對應(yīng)的 Render Object。該方法在首次 build 以及需要更新 Widget 時都會調(diào)用;
@overridevoid updateRenderObject(BuildContext context, covariant RenderFlex renderObject) { renderObject .. direction = direction ..mainAxisAlignment = mainAxisAlignment .. mainAxisSize = mainAxisSize .. crossAxisAlignment = crossAxisAlignment .. textDirection = getEffectiveTextDirection(context) .. verticalDirection = verticalDirection .. textBaseline = textBaseline; }
Flex.updateRenderObject
的源碼也很簡單,與Flex.createRenderObject
幾乎一一對應(yīng),用當(dāng)前Flex
的信息修改renderObject
。
- void didUnmountRenderObject(covariant RenderObject renderObject) —— 對應(yīng)的「Render Object」從「Render Tree」上移除時調(diào)用該方法。
RenderObjectWidget
的幾個子類:LeafRenderObjectWidget
、SingleChildRenderObjectWidget
、MultiChildRenderObjectWidget
只是重寫了createElement
方法以便返回各自對應(yīng)的具體的 Element 類實例。
小結(jié)
至此,重要的基礎(chǔ)型 Widget 基本介紹完了,總結(jié)一下:
- Widget 本質(zhì)上是 UI 的配置信息 (附加部分業(yè)務(wù)邏輯),并不存在一顆真實的「Widget Tree」(與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比);
- Widget 從功能上可以分為 3 類:「Component Widget」、「Proxy Widget」以及「Renderer Widget」;
- Widget 與 Element 一一對應(yīng),Widget 提供創(chuàng)建 Element 的方法 (
createElement
,本質(zhì)上是一個工廠方法); - 只有「Renderer Widget」才會參與最終的 UI 生成過程(Layout、Paint),只有該類型的 Widget 才有與之對應(yīng)的「Render Object」,同樣由其提供創(chuàng)建方法(
createRenderObject
)。
到此這篇關(guān)于詳解Flutter Widget的文章就介紹到這了,更多相關(guān)Flutter Widget內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android仿知乎懸浮功能按鈕FloatingActionButton效果
前段時間在看屬性動畫,恰巧這個按鈕的效果可以用屬性動畫實現(xiàn),下面通過本文給大家分享adroid仿知乎懸浮功能按鈕FloatingActionButton效果,需要的朋友參考下吧2017-04-04Android?Camera1實現(xiàn)預(yù)覽框顯示
這篇文章主要為大家詳細(xì)介紹了Android?Camera1實現(xiàn)預(yù)覽框顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05Android自定義LinearLayout布局顯示不完整的解決方法
這篇文章主要給大家介紹了關(guān)于Android自定義LinearLayout但布局顯示不完整的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11Android應(yīng)用中圖片瀏覽時實現(xiàn)自動切換功能的方法詳解
這篇文章主要介紹了Android應(yīng)用中圖片瀏覽時實現(xiàn)自動切換功能的方法,文中還講解了一個觸摸大圖進(jìn)行圖片切換的深入功能,需要的朋友可以參考下2016-04-04