ここでいう「お気持ち」というのは、「定義」とか「正体」とかではなく、
「何のためにあるのか」とか「どう使うことが想定されているのか」とか
そういうことを意味します。「作った人がどういうつもりだったか」という意味で「お気持ち」と言っています。
私がFlutterを始めてすぐの頃、contextとかBuildContextなるものが何なのか、どう使うのか、さっぱり理解できなくて困ったので、その辺を助ける記事です。
「実体はなんなのか」とか「正体はなんなのか」といった説明はしませんのでご了承ください。
一番言いたい結論は
Hoge.of(context) で「引数に与えた文脈におけるHoge」
を意味するということです。
BuildContext型のcontext変数
contextとサラッと言ってますが、より具体的には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を返します。
実際にはHogeはThemeだったり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')),
// 閉じカッコ略
}
実行結果がこちら
ウィジェットツリー内のThemeとCardの位置関係はまったく変わっていないのに、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がそれです。
これを挟み込むと、そこでcontextを受け取れます。
参考
ということで「お気持ち」の説明は以上です。
より詳しく知りたい方はこちらの記事がおすすめです。
また、私の記事ですが、BuildContextと密接な関わりのあるInheritedWidgetについても書きましたのでぜひご覧ください。


