Flutter | 一文搞懂 BuildContext

345丶發表於2021-08-02

bc602a6bda0e49b7acda73aa54ad6c4f_tplv-k3u1fbpfcp-watermark.jpg

概述

[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站王叔不禿

如果本文有幫助到你的地方,不勝榮幸,如有文章中有錯誤和疑問,歡迎大家提出!

相關文章