ここでいう「お気持ち」というのは、「定義」とか「正体」とかではなく、
「何のためにあるのか」とか「どう使うことが想定されているのか」とか
そういうことを意味します。「作った人がどういうつもりだったか」という意味で「お気持ち」と言っています。
私が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
もピンク色になっています。(時間や電波状況が表示されている部分)
これはScaffold
Widgetの一部なのですが、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
についても書きましたのでぜひご覧ください。