從原始碼看Flutter BuildContext的祕密

語言: CN / TW / HK

我們每次在寫Flutter程式碼的時候,都會看到這個引數——BuildContext,在Android開發中,也經常看見一個類似的東西——Context,它們是不是一樣的呢?其實說一樣也對,它們都是上下文的關鍵承載者,但是卻也不一樣,因為它們本質上是兩個不同的概念。在Flutter中,BuildContext的原始碼如下。
image.png
從註釋中我們就可以看出,[BuildContext]物件實際上是[Element]物件。[BuildContext]介面是用來阻止對[Element]物件的直接操作,它就是為了避免直接操縱Element類而建立的。

Element是Flutter UI中的一個非常重要的組成,Flutter UI在建立時,會通過Widget的createElement方法建立Element,然後Framework會呼叫Element例項的mount方法,在這個方法中,根據需要建立RenderObject,並掛載到Element的renderObject屬性上,實際的佈局和繪製,通常都是通過RenderObject的實現類RenderBox來實現的。

所以,我們甚至可以直接把Context強轉為Element,從而呼叫Element的方法,例如下面的程式碼。

```dart var size = ((context as Element).findRenderObject() as RenderBox).size;

(context as Element).markNeedsBuild(); ```

在使用BuildContext的時候,我們最常見的一個誤區就是下面這個例子。
image.png
這段程式碼很簡單,就是在當前頁面上路由到一個新的頁面,但是我們執行後,上面的程式碼會報錯。
image.png
從錯誤原因上我們可以看到,就是Context的問題,也就是of(context)這個方法。類似的程式碼風格,我們在Flutter中可以找到很多,例如下面這些。

dart Navigator.of(context) Scaffold.of(context).openDrawer() Theme.of(context).copyWith() ……

就以Navigator.of(context)為例,我們來看下of方法的實現。
image.png
可以看到,關鍵程式碼就是通過context.findRootAncestorStateOfType()或者是context.findAncestorStateOfType()方法,向上遍歷Element tree,並找到最近匹配的NavigatorState。也就是說of實際上是對context跨元件獲取資料的一個封裝方法。

而我們的Navigator的push操作就是通過找到的NavigatorState來完成的。

那麼我們現在來看下上面的那個錯誤具體是怎麼產生的。當我們在build函式中使用Navigator.of(context)的時候,這個context實際上是通過MyApp這個widget創建出來的Element物件,而of方法向上尋找祖先節點的時候(MyApp的祖先節點),其實並不存在MaterialApp,也就沒有它所提供的Navigator,所以就出錯了。

那麼當我們把Scaffold的部分拆成另外一個widget的時候,我們在FirstPage的build函式中,獲取的就是FirstPage的BuildContext,然後向上尋找發現了MaterialApp,並找到它提供的Navigator,於是就可以愉快進行頁面跳轉了。所以要解決這個問題,一般有兩個方法,一個就是抽取出一個新的Widget元件,或者是通過Builder元件,為後續Widget建立一個新的BuildContext環境。

所以,看到這裡,你可以認為,BuildContext,實際上就是當前Widget在Element樹上的控制代碼。

下面我們就從原始碼角度,來看下BuildContext的建立與載入的過程。

我們以StatelessWidget為例,在建立StatelessWidget的時候,首先會去createElement,並將當前widget傳給Element,即StatelessElement。
image.png
在這個Element中,我們發現它的build方法,實際上就是呼叫了Widget的build方法,同時,傳入了this,這個this,實際上就是我們在Widget的build方法中看到的BuildContext,到處,我們終於理清了,為什麼BuildContext就是Element了。

同時,正是由於Flutter檢視的樹形結構,可以讓我們很方便的在樹上游走,這就需要我們用到它的一些遊走的方法。

| dependOnInheritedElement | InheritedWidget | working on the base of ancestor's widget and rebuilding when ancestors change | | --------------------------------------- | ----------------- | --------------------------------------------------------------------------------- | | dependOnInheritedWidgetOfExactType | T? | runtime type of widgets or functions or model | | describeElement | DiagnosticsNode | descriptions of elements | | describeMissingAncestor | List | List of missing ancestors | | describeOwnershipChain | DiagnosticsNode | describes the ownership chain to the error report | | describeWidget | DiagnosticsNode | describe the details and working features of the widget | | dispatchNotification | void | bubble notification indicator at the context | | findAncestorRenderObjectOfType | T? | runtime type of RenderObjectOfType | | findAncestorStateOfType | T? | runtime type of StateOfType | | findAncestorWidgetOfExactType | T? | runtime type of WidgetOfExactType | | findRenderObject | T? | current render object which is created by itSelf | | findRootAncestorStateOfType | T? | runtime type ancestor's of given T type stateful widgets instance | | getElementForInheritedWidgetOfExactType | InheritedElement? | Type of concrete inheritedWidget subclass | | noSuchMethod | dynamic | dynamic type of method | | toString | string | converting the date to string through .toString methods provided by the framework | | visitAncestorElements | void | works for the call back return function, call back can not be null | | visitChildElements | void | children of the widgets or visitor |

這些方法也不用死記硬背,你只需要時刻記得「那幾棵樹」即可。

歡迎大家關注我的公眾號——【群英傳】,專注於「Android」「Flutter」「Kotlin」
我的語雀知識庫——https://www.yuque.com/xuyisheng