Posted at

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

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

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

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

これを

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

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

それぞれの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')),
// 閉じカッコ略
}

実行結果がこちら

ThemeCardの位置関係はまったく変わっていないのに、Cardが自分の先祖にいるThemeにアクセスしてくれません!!

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

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

そこからスタートしてさかのぼっても、もう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とは何か