Help us understand the problem. What is going on with this article?

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

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

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

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

私が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とは何か

agajo
あんなに勉強して、親に高い予備校代も出してもらって東大に入り、卒業したのに、今では家と食事を親に頼りながら、年金と住民税を払うためにトイレ掃除をしている者です。
https://portal.oka-ryunoske.work/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした