概述
[BuildContext] objects are actually [Element] objects. The [BuildContext] ,interface is used to discourage direct manipulation of [Element] objects.
翻譯過來的意思就是 [BuildContext] 物件實際上是 [Element] 物件。 [BuildContext] 介面用於阻止直接操作 [Element] 物件。
根據官方的註釋,我們可以知道 BuildContext 實際上就是 Element 物件,主要是為了防止開發者直接操作 Element 物件。通過原始碼我們也可以看到 Element 是實現了 BuildContext 這個抽象類
abstract class Element extends DiagnosticableTree implements BuildContext {}
複製程式碼
BuildContext 的作用
在之前的一篇文章中講過 Element 和 Widget 對應的關係,不太清楚的可以看一下
Element 是 Widget 樹中特定位置所對應的例項,Widget 的狀態都會儲存在 Element 當中。
那麼 BuildContext 到底能幹什麼呢?只要是 Element 能做的事情,BuildContext 基本都能做,如:
var size = (context.findRenderObjec var size = (context.findRenderObject() as RenderBox).size;
var local = (context.findRenderObject() as RenderBox).localToGlobal;
var widget = context.widget;t() as RenderBox).size;
複製程式碼
例如上面通過 context 之前獲取到寬高度,距離左上角的偏移,element 對應的 widget 等
因為 Elment 是繼承自 BuildContext ,我們甚至可以通過 context 來直接重新整理 Element 的狀態,如下:
(context as Element).markNeedsBuild();
複製程式碼
這樣就可以直接對當前的 Element 進行重新整理,而不必去通過 SetState,但是這種做法是極其的不推薦的。
其實在 SetState 中,最終也是呼叫的 markNeedsBuild
方法,如下:
void setState(VoidCallback fn) {
assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
///......
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
///......
}
return true;
}());
final dynamic result = fn() as dynamic;
assert(() {
if (result is Future) {
///......
]);
}
return true;
}());
///最終呼叫
_element!.markNeedsBuild();
}
複製程式碼
我們在寫程式碼的過程中還會發現一個問題,就是要更新的狀態不是必須要寫在 setState 裡面,只要寫在 setState 上面 即可,這樣也沒有問題,例如有些其他的響應式框架就沒有這個回撥,只提供了一個通知頁面重新整理的方法,早期的 flutter 也是如此。但是最後發現了這個問題的弊端了,如大多數人會在每個方法的後面加一個 setState,導致過度的開銷,並且在刪除的時候也是不知道這個這個 setState 到底有沒有實際的意義,這就會造成一些不必要的麻煩。
所以 Flutter 在 setState 中加了一個回撥,我們可以需要更新的狀態直接放在回撥裡面,和狀態沒關係的放在外邊即可。
常見的一些方法
-
(context as Element).findAncestorStateOfType()
沿著當前的 Element 向上尋找,直到直到一個特定的型別之後,將他的 State 返回
-
(context as Element).findRenderObject()
獲取 Element 渲染的物件
-
(context as Element).findAncestorRenderObjectOfType()
向上遍歷,獲取與泛型對應的渲染物件
-
(context as Element).findAncestorWidgetOfExactType()
遍歷,獲取與 T 對應的 Widget
上面這些方法在原始碼中還是有一些使用的栗子的,例如:
- Scaffold.of(context).showSnackBar()
在 Scaffold 的底部顯示一個 SnackBar
static ScaffoldState of(BuildContext context) {
assert(context != null);
final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>();
if (result != null)
return result;
//......
}
複製程式碼
檢視 of 方法,可以發現,裡面使用的就是 findAncestorStateOfType 方法來獲取的 Scaffold 的狀態,最終來實現一些操作,
-
Theme.of(context).accentColor
我們可以通過如上的方法來獲取一下主題顏色等,其內部實現如下:
static ThemeData of(BuildContext context) { final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>(); final MaterialLocalizations? localizations = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations); final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike; final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme; return ThemeData.localize(theme, theme.typography.geometryThemeFor(category)); } 複製程式碼
它和上面的一樣,也是找到離你最近的 _InheritedTheme,最後再將它還給你
栗子
寫一個側滑欄,通過點選按鈕來實現開啟 側滑欄
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
drawer: Drawer(),
floatingActionButton: FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
}),
);
}
}
複製程式碼
執行程式碼,就會發現報錯:Scaffold.of() called with a context that does not contain a Scaffold.
意思就是當前的 context 裡面沒有找到 Drawer,所以無法開啟。
為什麼呢? 因為這個 context 是當前 MyHomePage 這個層級的,在他的上層確實沒有 Drawer,所以自然也就沒有辦法開啟了。 那麼如何解決呢?如下:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
drawer: Drawer(),
floatingActionButton: Floating());
}
}
class Floating extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
});
}
}
複製程式碼
修改為如上程式碼即可解決。
在 Floating 中的 context 是 MyHomePage 下面的層級,所以說他的上級時候 Scaffold 的,自然也就不會報錯了。
但是一般這種情況下,我們是不用多建立一個元件的,所以我們還需要一個更好的解決方案,如下:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
drawer: Drawer(),
floatingActionButton: Builder(
builder: (context) {
return FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
});
},
));
}
}
複製程式碼
我們可以通過 Builder 來建立一個匿名的元件就可以了。
參考文獻
B站王叔不禿
如果本文有幫助到你的地方,不勝榮幸,如有文章中有錯誤和疑問,歡迎大家提出!