LoginSignup
277
184

More than 3 years have passed since last update.

FlutterのBuildContextとは何か

Last updated at Posted at 2019-01-21

Flutterで実装していると割と最初の方に疑問が出てくるのが、BuildContextだと思います。
これは、何も考えずにSnackbarを出そうとしたら、エラーになるので、ほとんどの人はここでつまずくと思います。

Widgetは表面的にはわかりやすいのですが、BuildContextは少しわかりにくいかもしれません。

この記事では、Flutterで実装する上でとても重要なBuildContextについて書いていきます。

BuildContextとは

結論をいうとBuildContextはElementのことです。
ドキュメントに書いてあります。

BuildContext objects are actually Element objects. The BuildContext interface is used to discourage direct manipulation of Element objects.

訳)BuildContextオブジェクトは実際にはElementオブジェクトです。 BuildContextインタフェースは、Elementオブジェクトの直接操作を妨げるために使用されます。

はい。 Elementが何かわからないですね。

Elementとは

まずはソースのほんの一部分だけをみてみましょう。
コンストラクタは以下になっています。

framework.dart
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

ElementはWidgetの参照を保持してますね。
この実装で、一つのBuildContextは一つのWidgetを参照していることがわかります。

次にElement classのドキュメントを見てみましょう。

An instantiation of a Widget at a particular location in the tree.

と書いてあります。
Widgetは直感的にtree構造になっていることは理解できますが、ElementもWidgetと全く同じ構造になっています。

次にElementの実装クラスには何があるか見てみましょう。

Implementers
ComponentElement, RenderObjectElement

Elementには、二種類あるのがわかります。

  1. ComponentElement
  2. RenderObjectElement

ComponentElement

一つ目のElement、ComponentElement classのドキュメントには

An Element that composes other Elements.

Rather than creating a RenderObject directly, a ComponentElement creates >RenderObjects indirectly by creating other Elements.

訳)
他のElementを構成するElement。
直接RenderObjectを作成するのではなく、ComponentElementは他のElementを作成することによって間接的にRenderObjectを作成します。

また、ComponentElementの子クラスは3種類あることがわかります。

Implementers
ProxyElement, StatefulElement, StatelessElement

これらは、それぞれProxyWidget, StatefulWidget, StatelessWidgetに対応していて、他のWidgetとは扱いが違うようです。
ちなみにProxyWidgetには、直接の子クラスにInheritedWidgetがあります。
これらのクラスはつまり、レンダリングを他のWidgetに移譲しています。

他のWidgetとは、RenderObjectWidgetのことです。
他のすべてのWidgetの親にはRenderObjectWidgetがいます。

RenderObjectElement

もう一つのElement、RenderObjectElement classでは

An Element that uses a RenderObjectWidget as its configuration.

RenderObjectElement objects have an associated RenderObject widget in the render tree, which handles concrete operations like laying out, painting, and hit testing.

どうやら、WidgetやElement同様にtree構造なRenderObjectの参照を持っているようです。
さらに、RenderObjectがレイアウトやら描画やら具体的な重い処理を担っているクラスということがわかります。

Elementの端的な説明

つまり、Elementとは、

  • Widgetと同じツリー構造になっている
  • 一つのWidgetを保持している
  • ComponentElementとRenderObjectElementの二種類ある
  • RenderObjectElementはレンダリングなどを行うRenderObjectを作成し、保持している

buildメソッドのBuildContext

build(BuildContext) のBuildContextには何が入っているのか。
BuildContextのドキュメントを見てみましょう。

Each widget has its own BuildContext, which becomes the parent of the widget returned by the StatelessWidget.build or State.build function. (And similarly, the parent of any children for RenderObjectWidgets.)

訳) 各ウィジェットはそれ自身のBuildContextを持っています。そのBuildContextは、StatelessWidgetのbuild()、またはStateのbuild()によって返されるウィジェットの親になります。 (そして同様に、RenderObjectWidgetsの子Widgetの親です。)

つまり、buildメソッドの引数のBuildContextは、そのメソッドで返すWidget群の親のElementということです。
ややこしく言っていますが、単純にbuildメソッドを実装しているWidgetのElementのことです。
(例えば、StatelessWidgetのbuild()メソッドのBuildContext == StatelessWidget自身を保持しているElement)

次にStatelessWidgetのbuildメソッドStateのbuildメソッドのドキュメントをみてみます。
二つのクラスで以下の部分が全く同じ内容です。

The given BuildContext contains information about the location in the tree at which this widget is being built. For example, the context provides the set of inherited widgets for this location in the tree.

訳)指定されたBuildContextには、このウィジェットが構築されているツリー内の場所に関する情報が含まれています。たとえば、コンテキストはツリー内のこの場所に継承されたウィジェットのセットを提供します。

本当にBuildContextがbuildメソッドを実装しているWidgetのElementなのかとツリー構造の情報が含まれているかをdebugしてみてみましょう。

main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp( // ⑤
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) { // ④
    return Scaffold( // ③
      body: Center( // ②
        child: Text( // ① 
          '$_counter',
          style: Theme.of(context).textTheme.display1,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

例えば、
①のText(=StatelessWidget)クラスのbuildメソッドの引数BuildContextが保持しているWidgetはText('0')であり、親は②のCenterになっています。
②のCenter(=RenderObjectWidget)のcreateRenderObjectメソッドのBuildContextの直接の親はMediaQaueryですがその先祖を辿っていくと③のScaffoldになります。
④のBuildContextの先祖は辿っていくと⑤のMaterialAppになります。

ちなみに最初の実行ではBuildContextのchildはnullです。
それは当然buildメソッドが終わらないとchildが設定できないからです。
BuildContextは、先祖を取得するメソッドはたくさんあります。メソッド名にancestorやinheritが付くものがそれです。
逆に子孫を取得するメソッドは、visitChildElements だけになります。当然、buildメソッドが終わらないと利用できないメソッドになります。

なぜshowSnackBarをするとエラーが起きる場合があるのか

上記の説明をわかっていると簡単です。
例えば、以下の実装ではエラーになります。

class _MyHomePageState extends State<MyHomePage> {

  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          '$_counter',
          style: Theme.of(context).textTheme.display1,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Scaffold.of(context).showSnackBar(SnackBar(content: Text('message')));
        },
        child: Icon(Icons.add),
      ),
    );
  }

showSnackBar メソッドは、ScaffoldState クラスのものです。
Scaffold.of(context) は、ancestorStateOfType() で先祖を辿ってScaffoldStateを探します。
ここで指定しているcontextのwidgetは、_MyHomePageState なので、いくら先祖を辿っても ScaffoldState が当然ないのです。

解決方法は、利用するScaffoldにid(key)を付けて、そのcurrentStateを使用することです。

+  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

   @override
   Widget build(BuildContext context) {
    return Scaffold(
+     key: _scaffoldKey,
      body: Center(
        child: Text(
          '$_counter',
          style: Theme.of(context).textTheme.display1,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
-         Scaffold.of(context).showSnackBar(SnackBar(content: Text('message')));
+         _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text('message')));
            _incrementCounter();
        },
        child: Icon(Icons.add),
      ),
    );
  }

もう一つの解決方法は、Builder widgetを使うことです。

   @override
   Widget build(BuildContext context) {
    return Scaffold(
      body: ・・・ ,
      floatingActionButton: Builder(builder: (context) {
        return FloatingActionButton(
          onPressed: () {
            Scaffold.of(context).showSnackBar(SnackBar(content: Text('message')));
            _incrementCounter();
          },
          child: Icon(Icons.add),
        );
      }),

この場合、Builder widgetは当然 Scaffold の子孫になります。
Builder の引数のクロージャーのcontextは、BuilderのbuildメソッドのBuildContextが渡されます。
このBuildContextは当然、Builderのwidgetを保持しているので、その先祖にはScaffoldがいるのです。

277
184
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
277
184