【Flutter】BuildContext context【よく理解せずに使っていたものたち】
Flutter を触り始めた頃、contextの正体が分からないまま何となく使っていました。
この記事では自分なりに調べて咀嚼した内容をまとめています。
contextとは
まず、どんな使われ方をしているか、例を挙げておさらいします。
// build関数
@override
Widget build(BuildContext context) {
// ...
}
// Theme.of(context)
ThemeData theme = Theme.of(context);
// MediaQuery.of(context)
double screenWidth = MediaQuery.of(context).size.width;
// Navigator.of(context)
Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewPage()));
build関数はflutter createで作られる雛形でも登場しますね。
まず言葉のイメージを掴むために、一般的な単語としての意味を確認します。
Weblio辞典曰く、contextとは
(文章の)前後関係、文脈、脈絡、コンテキスト、状況、環境
という意味を持ちます。
では早速、contextを深掘っていきましょう。
まず、contextのデータ型を見てみると、BuildContextとなっています。
いきなり新しい単語が出てきました。
BuildContextとは
BuildContextはウィジェットの位置情報を持つオブジェクトで、ツリーを遡って親ウィジェットの情報を取得するのに使われます。
BuildContextの動作を理解する
具体的にTheme.of(context)を例に、BuildContextの挙動を見てみましょう。
まずMaterialAppのthemeでデフォルトのprimaryColorをredにします。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.red, // ここ
useMaterial3: true,
),
home: MyPage(),
);
}
}
次にMyPageというクラスを作成し、画面中央にContainerを配置し、Theme.of(context).primaryColorで塗りつぶします。
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 200,
color: Theme.of(context).primaryColor, // red
),
),
);
}
}
この時、ContainerはMaterialAppのthemeで指定しているredになります。
では次に、特定のウィジェット内ではTheme.of(context).primaryColorをblueにしたいとします。
以下のコードのように、ThemeクラスのThemeDataでprimaryColorをblueに指定すれば、その子ウィジェット以下で呼び出すTheme.of(context).primaryColorはblueになるでしょうか。
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Theme(
data: ThemeData(primaryColor: Colors.blue),
child: Container(
width: 200,
height: 200,
color: Theme.of(context).primaryColor, // blueではなくred
),
),
),
);
}
}
// 注意:このコードでは色を変更できていません
答えはNOです。
なぜblueにならないのか、BuildContextの挙動を段階を追って見ていきます。
-
ContainerウィジェットがTheme.of(context)を参照します。 -
Theme.of(context)がcontext.dependOnInheritedWidgetOfExactType<Theme>()を呼び出します。(dependOnInheritedWidgetOfExactType<T>はInheritedWidgetを検索し、見つかったらInheritedElementに依存関係を登録するメソッドです。) -
BuildContextが現在のウィジェットの位置を特定します。 - ウィジェットツリーを遡り、最も近い
Theme(InheritedWidget)を探します。 - 見つかった
ThemeのThemeDataを返します。
BuildContextがThemeの外で作られているため、Theme.of(context)をTheme内で使っても、外側のThemeを参照してしまうのです。
InheritedWidgetとは
InheritedWidgetはウィジェットツリー内で子ウィジェットにデータを効率的に共有するための仕組みです。
通常、ウィジェット間でデータを共有する場合、親から子にプロパティを通してデータを渡していきます。しかし、アプリ全体で共有するデータ(例: テーマ、画面サイズ、ナビゲーション情報など)を渡す場合、すべてのウィジェットに手動でプロパティを追加するのは非効率です。
InheritedWidgetは、この問題を解決するために設計されており、親ウィジェットが提供するデータをツリー内の任意の子ウィジェットが直接取得できるようにします。
例えばTheme.of(context)やMediaQuery.of(context)は、内部でInheritedWidgetを利用してデータを共有しています。
ではTheme.of(context).primaryColorをblueに上書きするにはどうしたらいいでしょう。
BuildContextが遡った先のThemeがMyPageで呼び出したThemeになるようにすればいいのです。そのためにはThemeの下でBuildContextを新しく作成する必要があります。
BuildContextを新しく作成するには主に2つの方法が挙げられます。
-
Builderウィジェットを使用
Builderウィジェットを使うことで、新しいBuildContextが作成されます。 -
コンポーネント化してbuildを呼ぶ
ウィジェットをコンポーネント化することで、再度build関数が呼ばれ、新しいBuildContextが作成されます。
今回はBuilderウィジェットを使用する方法を例に挙げます。
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Theme(
data: ThemeData(primaryColor: Colors.blue),
child: Builder(
builder: (BuildContext context) {
// ↑ 新しく作られたBuildContext
return Container(
width: 200,
height: 200,
color: Theme.of(context).primaryColor, // blue
// ↑ このcontextはBuilderで新しく作られたBuildContext
);
},
),
),
),
);
}
}
これでContainerの色がblueになりました!
最後に
これまでBuildContextについて深掘りしてきました。
改めて、この記事で学んだことを振り返ると以下のようになります。
-
BuildContextはウィジェットの「位置情報」を持ち、それをもとに親ウィジェットから情報を取得する。 -
Theme.of(context)のような関数は、BuildContextを基準にツリーを遡って情報を探す。 -
Themeを上書きするには、新しいBuildContextを作る必要があり、その方法としてBuilderやコンポーネント化がある。
BuildContextを理解することで、テーマやナビゲーションなどの Flutterの仕組みをより直感的に扱えるようになるでしょう。
Discussion