BuildContextとは
ここまでのContextに関する理解があれば、BuildContextはFlutter、もしくはUIフレームワークに共通する特徴について理解すれば問題ないはずです。そこで、Flutterでの重要なContextであるBuildContextについて説明します。
画面を構成するWidgetと定義としてのWidgetの関係
Flutterでは各部品はすべてWidgetというクラスのインスタンスとして実行時に構築されますが、それらのWidgetを作成するためのコードは図3のように再利用されます。
そして、それらのコードからは実際のインスタンスを意識したコードが記述しにくくなっています。なぜなら、実際に画面上に表示されるWidgetの制御は表示上の最適化のために多くの部分でFlutterのフレーム側が制御しており、プログラマが想像している状況と異なる場合もあるからです。
しかし、実際には画面表示だけではなく制御にかかわる部分では、このツリー構造やその場所を意識し制御する必要もあります。特にStatefulWidgetの場合には部品の状態とデータなどを保持するState情報なども管理しており、それらのデータや機能に他のWidgetからアクセスできる必要があります。
そこで、Flutterでは図4のようにそれぞれの部品自体の情報を管理するBuildContextをもち、プログラマはBuildContextを通じて制御を行います。BuildContextはWidgetのbuildメソッド(リスト1)でアクセスできます。
class WidgetA1 extends StatelessWidget { // (1) Widgetクラス // 省略 @override Widget build(BuildContext context) { // (2) MyAppのBuildContext // 省略 } // 省略 }
(1)ではWidgetA1という名前でWidgetを定義しています。そして、(2)では部品ツリー上のWidgetA1を識別するBuildContextが引数として渡されます。StatefulWidgetでもほぼ同様であり、Widgetを構築する前にそのWidgetを作成するためのBuildContextが用意されます。
Elementとは
BuildContextの説明から少々離れますが、読者がReactやVueJSのような他のUIフレームワークを使ったことがあるのであれば、BuildContextの実装であるElementクラスを理解すると、これまでの経験から理解を助けることもできます。
例えば、BuildContextの実装は、リスト2のようにElementクラスでもあります。
abstract class Element extends DiagnosticableTree implements BuildContext { Widget get widget; // 省略 }
そして、このElementはWidgetライフサイクルを図5のように管理しています。
ここで示すメソッドや役割などをみると、ReactやVueJSのようなものと似ていると感じる方もいると思います。また他のUIフレームワークを使ったことがない読者の場合にはここまでの理解を思い出していただければ共通する部分があるので役立つはずです。
BuildContextを引数にするofメソッドについて
Flutterを利用していると、Navigator.ofのようなBuildContextを引数にとるofメソッドをたびたび見かけます。これらのメソッドが何をやっているかをもう少し詳しく見ていきます。
また、先ほどのより汎用的なContextの図に当てはめてofメソッドを考えると、図6の各Contextで管理している各データやインスタンスの実体にアクセスするためのメソッドと理解することもできます。
ofメソッドの仕組み
BuildContextを引数にするofメソッドには、リスト3のようにいろいろあります。
MediaQuery.of(context) Navigator.of(context) ScaffoldMessenger.of(context)
いずれのメソッドを使う場合でも、エラーなく動作している場合にはあまり意味を意識していなくても問題になることはありません。それぞれのofメソッドでやっていることは単純で、これまでのBuildContextのツリー構造がイメージできていれば理解は簡単です。
これは図7のように自分自身のBuildContextから親からその親へと探索し、対象のStateオブジェクトが見つかるとそのオブジェクトを返します。
このofメソッドで注意すべきなのは、引数で指定したBuildContextの親から探索するという点です。例えば、間違えたBuildContextを指定した場合には、リスト4のようなエラーが生じます。その場合には、この制限を思い出してもらえれば問題を解決できるはずです。
======== Exception caught by gesture =============================================================== The following assertion was thrown while handling a gesture: Scaffold.of() called with a context that does not contain a Scaffold.
利用したいBuildContextを用意する
ofメソッドを使いたいときに、引数として利用したいBuildContextがコード上存在しない場合があります。または、もっとも近くに存在するBuildContextを使えればよいと理解している方もいるかもしれません。
しかし、リスト5のようなコードを実行した際には、必要なScaffoldのステート(ScaffoldState)が見つからず先ほどのようなエラーが生じます。
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("タイトル"), leading: IconButton( icon: Icon(Icons.menu), onPressed: (){ Scaffold.of(context).openDrawer(); // (1) 正常に動作しないコード } ), // 省略 ))}
(1)のコード自体には記述上の問題はありませんが、引数として利用しているcontextが正しくなくエラーが発生します。
なぜならこのコードの場合には、図8のような探索を行います。従って、このような場合にはScaffoldとIconButtonのWidgetの間に存在するBuildContextを作る必要があります。
そのような場合に利用するのがBuilderウィジェットで、問題ないように変更したコードがリスト6です。
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("タイトル"), leading: Builder( builder: (context) => IconButton( // (1) 必要なBuildContextを用意する icon: Icon(Icons.menu), onPressed: (){ Scaffold.of(context).openDrawer(); // (2) 正常に動作するコード } ) ), // 省略 ))}
(1)のbuilderプロパティの指定部分で、AppBarウィジェットとIconButtonウィジェットの間にBuildContextが存在するようにします。
そのため、先ほどとコードは同じにもかかわらず(2)のコードは問題なく動作するようになります。この動作を先ほどと同じような図で示したのが、図9です。
Builderウィジェットを使わなくても、必要なBuildContextをコード上に存在させたいだけなので、自分で定義したStatefulWidgetやStatelessWidgetではこのようなBuilderウィジェットは必要ありません。しかし、BuildContextを使いたいだけの場合が多いので、このようにBuilderウィジェットが多用されます。