Flutterのcontextの「お気持ち」を理解する

ここでいう「お気持ち」というのは、「定義」とか「正体」とかではなく、

「何のためにあるのか」とか「どう使うことが想定されているのか」とか

そういうことを意味します。「作った人がどういうつもりだったか」という意味で「お気持ち」と言っています。

私がFlutterを始めてすぐの頃、`context`とか`BuildContext`なるものが何なのか、どう使うのか、さっぱり理解できなくて困ったので、その辺を助ける記事です。

「実体はなんなのか」とか「正体はなんなのか」といった説明はしませんのでご了承ください。

一番言いたい結論は

**Hoge.of(context) で「引数に与えた文脈におけるHoge」**


を意味するということです。

#BuildContext型のcontext変数

`context`とサラッと言ってますが、より具体的には`BuildContext`というクラスですね。

[BuildContextクラスのドキュメント](https://api.flutter.dev/flutter/widgets/BuildContext-class.html)

こちらをよく読むと実は「お気持ち」も書いてあるのですが、簡潔に終わってますので、この記事で改めて説明します。


#"context"は「文脈」という意味

そもそも何で"context"と呼ばれているのか、を考えてみることにしましょう。そこには命名者の意図があるはずです。

"context"という英単語は「文脈」という意味です。


「この**文脈**における『適当』は『雑』ではなく『ちょうどいい』という意味です。」

「この**文脈**における『押すなよ！』というのは『押せ！』という意味です。」

「この**文脈**においては、お茶漬けを勧めているのではなく『帰れ』と言っています。」

のように使いますね。

##「文脈」は「環境」や「状況」と言い換えることもできます。

「この**状況**における『適当』は『雑』ではなく『ちょうどいい』という意味です。」

「この**環境**における『押すなよ！』というのは『押せ！』という意味です。」

「この**状況**においては、お茶漬けを勧めているのではなく『帰れ』と言っています。」


#Hoge.of(context)で「この文脈におけるHoge」

Flutterの話に戻ります。

Flutterの`context`もやはり「文脈」「状況」「環境」を表します。

「今、どんな場所にいるのか」を表しているわけです。

もっと言うと、「**Widgetツリー内のどこにいるか**」を表しています。

Flutterのコードで`context`が出てくるのは大体次の２つの場合です。

 - builder
 - of

一つずつ説明します。

##builder

```dart
builder:(BuildContext context){
  return // (何かしらのWidget);
}
```



builderは、Widget生成してWidgetツリー内にセットする時に呼ばれる関数です。

引数の`context`は、Flutterが自動的に与えます。我々はbuilder関数が`context`を受け取れるようにさえしておけば良いです。

自分でWidgetを作る場合は

```dart
@override
Widget build(BuildContext context){
  return // (何かしらのWidget);
}
```

となりますが、`BuildContext`を受け取って`Widget`を返す関数という意味で同じです。

これらのコードに出てくる`context`は、受け取り口として用意されているものであって、使ってるわけじゃないですね。


では次。

##of

```dart
Hoge.of(context).// (何かしらのプロパティやメソッド);
```

こちらは、すでに持っている`context`という変数を実際に使っています。

`Hoge.of(context)`というのは、`context`の場所からWidgetツリーを先祖に向かってさかのぼって、最初に見つかる`Hoge`を返します。

実際には`Hoge`は`Theme`だったり`DefaultTextStyle`だったり自分で作った何かだったりします。

つまり`Hoge.of(context)`は

「引数に与えた`context`はどの`Hoge`の傘下なのか」

「引数に与えた`context`にはどんな`Hoge`が設定されているか」

を調べているわけです。

##Themeの例

実際のコードで見てみましょう。

次のようなWidgetAを作ります。

```dart
class WidgetA extends StatelessWidget {
  const WidgetA(this.text);
  final String text;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        height: 100,
        width: 200,
        child: Card(
          color: Theme.of(context).primaryColor,
          child: Center(child: Text(text)),
        ),
      ),
    );
  }
}
```


レイアウト用の`Widget`か挟まってて見にくいですが、重要なのはここだけです。

```dart
class WidgetA extends StatelessWidget {
  @override
  // contextはここで引数として与えられる
  Widget build(BuildContext context) {
    return
      Card(
          // 自分が置かれた文脈に設定されているThemeを調べている
          color: Theme.of(context).primaryColor,
      ),
    );
  }
}
```

`Theme.of(context)`の部分で、自分が置かれた文脈におけるThemeを探しに行き、自分自身の色として適用しています。

この`WidgetA`を、次のように配置してみましょう。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(primaryColor: Colors.pink),
        home: Scaffold(
          appBar: AppBar(),
          body: Column(
            children: <Widget>[
              Theme(
                  data: ThemeData(primaryColor: Colors.amber),
                  child: const WidgetA('Fuga')),
              Column(
                children: <Widget>[
                  const WidgetA('Foo'),
                  Theme(
                      data: ThemeData(
                        primaryColor: Colors.teal,
                      ),
                      child: const WidgetA('Bar'))
                ],
              )
            ],
          ),
        ));
  }
}
```

ツリー図にするとこんな感じです。

![Qiita用Widgetツリー図 context.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85139/295572ce-b72e-9a95-0390-9991db562305.png)

それぞれのWidgetが何色の`Theme`の傘下になっているかによって色分けしてあります。

`MaterialApp`に色が設定してありますが、実は`MaterialApp`にはデフォルトの`Theme`を設定する機能がありまして、そこにピンク色を設定してあります。

これを


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

として実行すると次のようになります。

![Screen Shot 2019-09-05 at 11.16.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85139/a324471a-f2f8-1cb7-19f0-ae8bdd2aa2d6.png)

それぞれのWidgetが、自分の先祖にいるThemeを見つけてその色に染まっているのがわかると思います。

`Foo`と書いてある`WidgetA`は、先祖に`Theme`がいないので、`MaterialApp`に設定されているピンク色になっていますね。

また、よく見ると最上段の`AppBar`もピンク色になっています。(時間や電波状況が表示されている部分)

これは`Scaffold`Widgetの一部なのですが、Scaffoldも、すぐ親に`MaterialApp`がいるのでピンク色になっています。


#【注意】contextの位置が高すぎる場合




いちいちWidgetAなんて作らずに、単にズラズラとWidgetを配置していったらどうなるでしょうか？

つまりこういうことです。

レイアウト用のコードも挟まってて見にくいので、重要な部分だけ取り出したコードも下に載せておきます。

```dart
class MyApp extends StatelessWidget {
  @override
  // contextはここで与えられる
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(primaryColor: Colors.pink),
        home: Scaffold(
          appBar: AppBar(),
          body: Column(
            children: <Widget>[
              Theme(
                  data: ThemeData(primaryColor: Colors.blue),
                  child: Center(
                    child: SizedBox(
                      height: 100,
                      width: 200,
                      child: Card(
                        color: Theme.of(context).primaryColor,
                        child: Center(child: Text('Fuga')),
                      ),
                    ),
                  )),
              Column(
                children: <Widget>[
                  Center(
                    child: SizedBox(
                      height: 100,
                      width: 200,
                      child: Card(
                        color: Theme.of(context).primaryColor,
                        child: Center(child: Text('Foo')),
                      ),
                    ),
                  ),
                  Theme(
                    data: ThemeData(
                      primaryColor: Colors.green,
                    ),
                    child: Center(
                      child: SizedBox(
                        height: 100,
                        width: 200,
                        child: Card(
                          color: Theme.of(context).primaryColor,
                          child: Center(child: Text('Bar')),
                        ),
                      ),
                    ),
                  )
                ],
              )
            ],
          ),
        ));
  }
}

```

重要なところだけ版

```dart
class MyApp extends StatelessWidget {
  @override
  // contextはここで与えられる
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(primaryColor: Colors.pink),
        home: Column(
            children: <Widget>[
              Theme(
                  data: ThemeData(primaryColor: Colors.blue),
                      child: Card(
                        color: Theme.of(context).primaryColor,
                        child: Center(child: Text('Fuga')),
                      ),
                    ),
              Column(
                children: <Widget>[
                      child: Card(
                        color: Theme.of(context).primaryColor,
                        child: Center(child: Text('Foo')),
                      ),
                  Theme(
                    data: ThemeData(
                      primaryColor: Colors.green,
                    ),
                        child: Card(
                          color: Theme.of(context).primaryColor,
                          child: Center(child: Text('Bar')),
 // 閉じカッコ略
}

```

実行結果がこちら

![Screen Shot 2019-09-05 at 11.29.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85139/9572a9d0-9e6d-587e-09b0-e36cb09c10c0.png)



ウィジェットツリー内の`Theme`と`Card`の位置関係はまったく変わっていないのに、Cardが自分の先祖にいる`Theme`にアクセスしてくれません！！

実はコードをよく見ると、引数`context`が与えられているのは`MyApp`のbuild関数だけです。これは`MaterialApp`よりもさらに親です。

`Theme.of(context)`は与えられた`context`の地点からスタートしてツリーをさかのぼり、`Theme`を探しに行くことになりますが

そこ(`MaterialApp`よりも親)からスタートしてさかのぼっても、もうWidgetはありません。もちろん`Theme`もありません。

ということで、何も見つからないので、３つの`Card`はFlutterのデフォルトカラーである青色になってしまいました。

`Hoge.of(context)`でさかのぼりを開始するスタート地点は、そのコードを記述した位置ではなく、**引数contextが与えられた位置から**なので注意してください。




#contextを途中で置き換える方法

このように`context`の与えられる位置が高すぎる場合は、もっと下層で`context`を**「発生」**させなければなりません。

その方法を２つ紹介します。

##自作Widgetを挟む

はじめのコードでやっていた方法です。`WidgetA`のような自作Widgetを用意すれば、自動的にその中にbuild関数があるはずですから、そこで`context`を受け取ることになります。

##Builderを使う

実はFlutterにはそういう時に使えるWidgetが用意されています。

`Builder`というWidgetがそれです。

[Builder Class](https://api.flutter.dev/flutter/widgets/Builder-class.html)

これを挟み込むと、そこで`context`を受け取れます。


#参考

ということで「お気持ち」の説明は以上です。

より詳しく知りたい方はこちらの記事がおすすめです。

[FlutterのBuildContextとは何か](https://qiita.com/ko2ic/items/f7bf98b4a30049027470)


また、私の記事ですが、`BuildContext`と密接な関わりのある`InheritedWidget`についても書きましたのでぜひご覧ください。

[InheritedWidgetの目的と使い方【Flutter】](https://qiita.com/agajo/items/375d5415cb79689a925c)