目錄介紹
- 01.Widget基礎概念
- 1.1 Widget概念
- 1.2 Widget骨架
- 1.3 Widget源碼
- 1.4 Widget不可變
- 02.StatelessWidget源碼
- 03.StatefulWidget源碼
- 04.InheritedWidget源碼
- 05.Context是什麼作用
- 在Flutter中幾乎所有的對象都是一個Widget。
- 與原生開發中“控件”不同的是,Flutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的元件如:用于手勢檢測的
widget、用于APP主題資料傳遞的GestureDetector
等等,而原生開發中的控件通常隻是指UI元素。Theme
- 在描述UI元素時可能會用到“控件”、“元件”這樣的概念,讀者心裡需要知道他們就是widget,隻是在不同場景的不同表述而已。
- 由于Flutter主要就是用于建構使用者界面的,是以,在大多數時候,可以認為widget就是一個控件,不必糾結于概念。
- 與原生開發中“控件”不同的是,Flutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的元件如:用于手勢檢測的
- Widget 的骨架
- 常用的 StatefulWidget、StatelessWidget,再加上 (InheritedWidget) 或 ProxyWidget 和 RenderObjectWidget 都繼承于 Widget 基類,他們整體構成了 Widget 的骨架。
- 有狀态 和 無狀态
- StatelessWidget 無狀态的 Widget,常見的子類如 Text、Container。
- StatefulWidget 有狀态的 Widget,常用的子類有 Image、Navigator。
- ProxyWidget 為代理 Widget,可以快速追溯父節點,通常用來做資料共享,常見的子類 InheritedWidget,各種狀态管理架構,如 provider 等正是基于它實作。
- 什麼叫做“狀态”?Widget 在 Flutter 架構下設計為不可變的,通常情況下每一幀都會重新建構一個新的 Widget 對象,而無法知道之前的狀态。StatefulWidget 通過關聯一個 State 對象實作狀态的儲存。
- Widget源碼如下所示
abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected @factory Element createElement(); @override String toStringShort() { final String type = objectRuntimeType(this, 'Widget'); return key == null ? type : '$type-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } @override @nonVirtual bool operator ==(Object other) => super == other; @override @nonVirtual int get hashCode => super.hashCode; static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } static int _debugConcreteSubtype(Widget widget) { return widget is StatefulWidget ? 1 : widget is StatelessWidget ? 2 : 0; } }
- 主要方法和屬性介紹
-
類繼承自Widget
,DiagnosticableTree
即“診斷樹”,主要作用是提供調試資訊。DiagnosticableTree
-
: 這個Key
屬性類似于React/Vue中的key
,主要的作用是決定是否在下一次key
時複用舊的widget,決定的條件在build
方法中。canUpdate()
-
:正如所述“一個Widget可以對應一個createElement()
”;Flutter Framework在建構UI樹時,會先調用此方法生成對應節點的Element
對象。此方法是Flutter Framework隐式調用的,在我們開發過程中基本不會調用到。Element
-
複寫父類的方法,主要是設定診斷樹的一些特性。debugFillProperties(...)
-
是一個靜态方法,它主要用于在Widget樹重新canUpdate(...)
時複用舊的widget,其實具體來說,應該是:是否用新的Widget對象去更新舊UI樹上所對應的build
對象的配置;通過其源碼我們可以看到,隻要Element
與newWidget
的oldWidget
和runtimeType
同時相等時就會用key
去更新newWidget
對象的配置,否則就會建立新的Element
。Element
-
- 核心方法createElement()
-
類本身是一個抽象類,其中最核心的就是定義了Widget
接口。createElement()
- 在Flutter開發中,我們一般都不用直接繼承
類來實作一個新元件,相反,我們通常會通過繼承Widget
或StatelessWidget
來間接繼承StatefulWidget
類來實作。Widget
-
StatelessWidget
都是直接繼承自StatefulWidget
類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型。Widget
-
- 核心方法canUpdate()
- 實際上就是比較兩個 widget 的 runtimeType 和 key 是否相同。
- runtimeType 也就是類型,如果新老 widget 的類型都變了,顯然需要重新建立 Element。
- key Flutter 中另一個核心的概念,key 的存在影響了 widget 的更新、複用流程,這裡先不展開。
- 預設情況下 widget 建立時不需傳入 key,是以更多情況下隻需要比較二者的類型,如果類型一樣,那麼目前節點的 Element 不需要重建,接下來繼續調用 child.update 更新子樹。
- 實際上就是比較兩個 widget 的 runtimeType 和 key 是否相同。
- Widget 是一個很重要的概念,但是Widget有一個更重重要的特性,就是Widget是immutable(不可變的),這是什麼意思?
- 拿 Opacity 為例給講解,講解之前先看一下Opacity的繼承關系。(在講源碼之前我們先看一下Opacity的職責是什麼,Opacity是一個能讓他的孩子透明的元件,很簡單也很容易了解。)
- Opacity繼承自SingleChildRenderObjectWidget,這類隻包含了一個child的Widget,它繼承自RenderObjectWidget,RenderObjectWidget繼承自Widget。
class Opacity extends SingleChildRenderObjectWidget { } abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { } abstract class RenderObjectWidget extends Widget { }
- 然後看一下 Opacity 簡單源碼
class Opacity extends SingleChildRenderObjectWidget { const Opacity({ Key key, @required this.opacity, Widget child, }) : super(key: key, child: child); final double opacity;//注釋1 @override RenderOpacity createRenderObject(BuildContext context) {//注釋2 return RenderOpacity( opacity: opacity ); } @override void updateRenderObject(BuildContext context, RenderOpacity renderObject) { renderObject ..opacity = opacity } }
- 在注釋1處聲明了一個屬性,這屬性是final,也就除了構造函數能給這個屬性指派之外,沒有其他的辦法讓這個值進行改變。那我們想改變這個值怎麼辦,唯一的辦法就是建立一個新的Opacity。
- StatelessWidget源碼如下所示
abstract class StatelessWidget extends Widget { const StatelessWidget({ Key key }) : super(key: key); @override StatelessElement createElement() => StatelessElement(this); @protected Widget build(BuildContext context); }
-
相對比較簡單,它繼承自StatelessWidget
類,重寫了Widget
方法。createElement()
-
間接繼承自StatelessElement
類,與Element
相對應(作為其配置資料)。StatelessWidget
-
-
用于不需要維護狀态的場景。StatelessWidget
- 它通常在
方法中通過嵌套其它Widget來建構UI,在建構過程中會遞歸的建構其嵌套的Widget。build
- 它通常在
- StatefulWidget源碼如下所示
abstract class StatefulWidget extends Widget { const StatefulWidget({ Key key }) : super(key: key); @override StatefulElement createElement() => StatefulElement(this); @protected @factory State createState(); }
-
StatefulElement
類,與StatefulWidget相對應(作為其配置資料)。Element
-
中可能會多次調用StatefulElement
來建立狀态(State)對象。createState()
-
-
用于建立和Stateful widget相關的狀态,它在Stateful widget的生命周期中可能會被多次調用。createState()
- 例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framework就會調用該方法為每一個位置生成一個獨立的State執行個體,其實,本質上就是一個
對應一個State執行個體。StatefulElement
- 例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framework就會調用該方法為每一個位置生成一個獨立的State執行個體,其實,本質上就是一個
- 了解樹的概念
- 在不同的場景可能指不同的意思,在說“widget樹”時它可以指widget結構樹,但由于widget與Element有對應關系(一可能對多)。
- 在有些場景(Flutter的SDK文檔中)也代指“UI樹”的意思。
- 而在stateful widget中,State對象也和
具有對應關系(一對一),是以在Flutter的SDK文檔中,可以經常看到“從樹中移除State對象”或“插入State對象到樹中”這樣的描述。StatefulElement
- 其實,無論哪種描述,其意思都是在描述“一棵構成使用者界面的節點元素的樹”,如果沒有特别說明,都可抽象的認為它是“一棵構成使用者界面的節點元素的樹”。
4.1 InheritedWidget源碼分析
- InheritedWidget源碼如下所示
abstract class InheritedWidget extends ProxyWidget { const InheritedWidget({ Key key, Widget child }) : super(key: key, child: child); @override InheritedElement createElement() => InheritedElement(this); @protected bool updateShouldNotify(covariant InheritedWidget oldWidget); }
- 如果想自己實作一個類似主題變更後,更新相應 UI 的功能應該怎麼做?
- 大緻思路應該就是一個觀察者模式,凡是使用的主題資料的地方,需要向 主題中心 注冊一個觀察者,當主題資料發生 改變 時,主題中心依次通知各個觀察者進行 UI 更新。
- 這裡有個問題需要解決,如何定義 資料改變 ?事實上,資料是否改變是由業務方決定的,是以這裡需要抽象出相應接口,來看 InheritedWidget 結構。
- 核心就是 updateShouldNotify 方法
- 入參為原始的 widget,傳回值為布爾值,業務方需要實作此方法,判斷是否需要将變化通知到各個觀察者。
4.2 注冊和通知流程
- 舉一個常見例子
- 比如,app設定了theme主題,當修改了theme主題顔色,是怎麼修改全局所有頁面的theme狀态的呢?這個就用到了注冊和通知的功能,接着往下看:
- 注冊流程
- 假設 StudyWidget 是我們業務側的 Widget,其内部使用了 Theme.of(context) 方法擷取任意主題資訊後,會經一系列調用,最終将這個 context——StudyWidget 對應的 Element 對象,注冊到 InheritedElement的成員變量 Map<Element, Object> _dependents 中。
- 另外需要注意,之是以在第二步中,可以找到父 InheritedElement,是因為在 Element 的 mount 過程中,會将父 Widget 中儲存的 Map<Type, InheritedElement> _inheritedWidgets 集合,依次傳遞給子 Widget。如果自身也是 InheritedElement 也會添加到這個集合中。
- 當我們使用 MaterialApp 或 CupertinoApp 作為根節點時,其内部已經幫我們封裝了一個 Theme Widget,是以不需要我們額外的套一層作為注冊了。
- 通知流程
- 當父 InheritedWidget 發生狀态改變時,最終會調用到 InheritedElement 的 update 方法,我們以此作為通知的起點。
- 可以看到,流程最終會将依賴的 Element 标髒,在下一幀重繪時将會更新對應 Widget 的狀态。至此,InheritedWidget 整體的注冊、通知流程結束。
- 什麼是Context
-
方法有一個build
參數,它是context
類的一個執行個體,表示目前widget在widget樹中的上下文,每一個widget都會對應一個context對象(因為每一個widget都是widget樹上的一個節點)。BuildContext
- 實際上,
是目前widget在widget樹中位置中執行”相關操作“的一個句柄,比如它提供了從目前widget開始向上周遊widget樹以及按照widget類型查找父級widget的方法。context
-
- 下面是在子樹中擷取父級widget的一個示例:
class ContextRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Context測試"), ), body: Container( child: Builder(builder: (context) { // 在Widget樹中向上查找最近的父級`Scaffold` widget Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>(); // 直接傳回 AppBar的title, 此處實際上是Text("Context測試") return (scaffold.appBar as AppBar).title; }), ), ); } }