LoginSignup
228
141

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-05

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

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

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

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

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

一番言いたい結論は

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

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

BuildContext型のcontext変数

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

BuildContextクラスのドキュメント

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

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

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

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

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

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

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

のように使いますね。

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

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

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

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

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

Flutterの話に戻ります。

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

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

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

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

  • builder
  • of

一つずつ説明します。

builder

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

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

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

自分でWidgetを作る場合は

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

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

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

では次。

of

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

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

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

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

つまりHoge.of(context)

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

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

を調べているわけです。

Themeの例

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

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

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か挟まってて見にくいですが、重要なのはここだけです。

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

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

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

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

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

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

これを

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

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

Screen Shot 2019-09-05 at 11.16.08.png

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

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

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

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

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

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

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

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

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')),
                        ),
                      ),
                    ),
                  )
                ],
              )
            ],
          ),
        ));
  }
}

重要なところだけ版

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

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

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

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

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

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

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

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

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

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

自作Widgetを挟む

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

Builderを使う

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

BuilderというWidgetがそれです。

Builder Class

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

参考

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

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

FlutterのBuildContextとは何か

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

InheritedWidgetの目的と使い方【Flutter】

228
141
2

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
228
141