この記事では、 Widget の概要と、StatefulWidget, StatelessWidget, InheritedWidget について解説します。
Flutter 初学者の方の学習の助けになれば幸いです。
この記事では、 Widget の概要と、StatefulWidget, StatelessWidget, InheritedWidget について解説します。
Flutter 初学者の方の学習の助けになれば幸いです。
こんにちは、アプリエンジニアのさとしです。
普段は Android アプリ開発を行なっています。
自社開発で、マルチプラットフォーム開発の出来る Flutter が採用されることになりました。
そこで今回は、Flutter の仕組みについて調べたので、Flutter で利用される Widget の概要と、Widgetの中から StatelessWidget, StatefulWidget, InheritedWidgetについての概要、使い分けについて説明したいと思います。
Flutter では、ほぼ全ての UI が Widget によって構成されており、Text や Button といったものから、中央寄せに利用する Center, 余白の設定に用いられる Padding, 更にはアプリそのものも Widget となっています。
Flutter では、Widget をツリー状に組み合わせることによって、画面を構築していきます。
例えば、Flutter プロジェクトを作成した際に実装されているカウンターアプリは、下記の図のようなツリー構造を持っています。
図1. カウンターアプリのツリー構造
この Widget Tree は IDE 上で確認することができます。
AndroidStudio の場合は、Flutter Inspector を利用することで確認ができます。
VSCode でも、Widget Inspector を利用することで、確認することができます。
図2. Flutter Inspector で見るツリー構造
ビルドすると、以下のようなおなじみのカウンターアプリが起動します。
図3. iOSシミュレータで起動したカウンターアプリ
先ほどの説明のように、Flutter では UI を Widget を利用して構築していきます。Widget は UI に表示する文字列や、色などの状態を保持することができます。
通常の Widget は、一度状態が決まるとその状態が変更されることはありません。
しかし、ボタン押下などのイベントに反応して Widget の状態を動的に変更したいという状況はよくあるかと思います。
そのような時に、StatefulWidget を利用します。
StatefulWidget は、変更可能な状態 (State) を持つ Widget です。
StatefulWidget 自体は他の Widget と同様に自身を変更することはできません。
ただし、StatefulWidget が持つ createState メソッドを通じて変更可能な State を生成することで、自身を変更することが可能な状態を持つことができます。
State クラスでは、ボタン押下などのイベントによって変化させたい変数や、変化させるためのメソッドを定義します。
ここからは、Flutter プロジェクト作成時のデフォルト実装であるカウンターアプリのソースコードを見ながら解説していきます。
ソースコードは下記リンクからも確認することができます。
https://github.com/EMoshU/blog-counter/blob/default_counter_app/lib/main.dart
カウンターアプリの中には、MyHomePage という StatefulWidget と、その State クラスである _MyHomePageState クラスがあります。
_MyHomePageState クラスを見てみると、_counter というint型の変数、 _incrementCounter メソッドが定義されていることがわかります。
なお、Dart 言語では変数、クラス名の前に _(アンダースコア) を置くことで、プライベートな変数、クラスであることを表現します。
さらに、_incrementCounter メソッドの中を見てみると、setState というメソッドが呼ばれていることがわかります。
setState メソッドとは、StatefulWidget が持つライフサイクルメソッドの一つで、この中で実際に State に対する更新を行います。
ソースコードを確認すると、先ほど定義した _counter をインクリメントする処理が行われていることがわかります。
setState が呼ばれると内部で再描画が必要であることを通知し、画面が更新されるタイミングで再描画処理が行われます。
上記の説明を図で表すと以下のようになります。
図4. setState が呼び出されてから、更新された値が設定されるまでの処理
StatefulWidget は、自身が保持するStateが更新された場合、ツリー下位全体に対してリビルド処理を実行します。
結果として、Text(カウンター) が再描画され、カウンターの値が更新されます。
StatelessWidget は、StatefulWidget と異なり、変更可能な状態を持たない Widget です。
テキストやアイコンなど、画面の持つ状態によって変化しない Widget に利用されることが多いです。
図1 のカウンターアプリのツリーにおける、AppBar の子 Widget である Text や、FloatingActionButton の子 Widget である Icon が StatelessWidget に当たります。
StatefulWidget では、setState が呼ばれる度にビルドが実行されましたが、StatelessWidget では最初に描画する時と親 Widget が再描画された時にビルドが実行されます
カウンターアプリでは、図4 で説明した形で StatefulWidget の MyHomePage がリビルドされ、下位 Widget にリビルドが波及していく形で、インクリメントされた数値を表示する Text が再描画されます。
ここまでは、画面を構築するための基本的な Widget について説明してきました。
これらの Widget は親子関係を持ち、Widget ツリーを構築します。
ここで、画面に表示するための、フォントや色などの静的なデータを各々の Widget に持たせると、データを利用するたびに同じコードを記述する必要があり、冗長になります。
StatefulWidget が持つ状態を、子 Widget で参照したくなることがあります。
その際に、Widget のツリー構造が深くなってしまうと、データを取りに行ったり、受け取るために多くの Widget を経由することになり、データを渡すための処理の効率が悪くなります。
更に、静的なデータや状態を依存関係のない Widget にも渡す必要があるため、実装が増え、コードも見通しが悪くなってしまいます。
このような場合に利用される Widget が、InheritedWidget になります。
InheritedWidget は、自身のデータを参照している Widget に直接アクセスすることのできる Widget です。
先ほどのカウンターアプリを、InheritedWidget を利用する形にしたソースコードを参考に解説します。
ソースコードはこちらになります。
https://github.com/EMoshU/blog-counter
アプリを起動後は、先ほどの画像と同じようなカウンターアプリが起動します。
修正後のカウンターアプリが持つ Widget ツリーは以下のようになっています。
図5. 修正後のカウンターアプリのツリー構造
カウンターアプリ上では、Scaffold の下位 Widget である FloatingActionButton のクリックイベントで、MyApp の子 Widget である MyCounter が持つデータを更新しようとしています。
また、更新されたデータは Text(カウンター) に反映させる仕様になっています。
InheritedWidget であれば、データを保有している Widget の状態に依存することで、Widget に直接アクセスすることが出来ます。
また、InheritedWidget が保有しているデータが更新された場合に、状態に依存している Widget に対し即再描画処理を実行することができます。
このアクセス性の良さから、InheritedWidget は主に StatefulWidget と組み合わせて状態管理に利用されます。
次の説明ではシーケンス図を見ながら、MyHomePage での dependOnInheritedWidgetOfExactType 呼び出しから、呼び出した Widget の再描画までの流れを解説します。
まず初めに図6を基に、MyHomePage が、描画に必要なデータを _InheritedWidget から取得するまでの説明をします。
1-1〜1-3 で MyHomePage(Widget) は、_InheritedCounter の状態に依存することを通知します。
そして 1-4.setDependencies で _InheritedCounter は MyHomePage のことを、自身の状態に依存する Widget として記憶します。
最終的に、MyHomePage(Widget) が _InheritedCounter(Widget) の data プロパティにアクセスして、MyCounterState を取得します。
突然 Element という言葉が出てきましたが、Element とは、Widget と対になるオブジェクトであり、Widget 間の親子関係を保持している要素になります。
図6. MyHomePage が _InheritedCounter の持つデータを取得するまでの流れ
次に、FloatingActionButton の押下によってデータの更新が行われ、MyCounter がリビルド対象となる流れについて、図7 を基に説明します。
FloatingActionButton の押下により、2-1.increment メソッドが呼び出されると、increment メソッドの中で 2-2.setState が呼ばれます。
setState メソッドは、内部で 2-3.markNeedsBuild メソッドを呼び出し、自身をリビルド対象とします。
リビルド対象となった MyCounter(Element) は、BuildOwner の 2-4.scheduleBuildFor メソッドを呼び出し、リビルド待ちの状態になります。
リビルドについて簡単に説明すると、画面のフレームレートによって画面が書き換えられるタイミングで、再描画が必要な Widget を覚えている BuildOwner が、再描画対象の Element の rebuild メソッドを呼び出します。
2-4 の scheduleBuildFor から先の説明はしませんが、具体的な描画処理の仕組みについては、scheduleBuildFor メソッドの内部をご覧いただくか、Flutter の描画の仕組みについては、以下のリンクの記事に詳しく記載されていますので、参照してください。
図7. MyCounter がリビルド対象となるまでのシーケンス図
リビルド対象となった MyCounter が実際にリビルドされるのに続いて子 Widget である _InheritedCounter がリビルドされ、_InheritedCounter の更新に伴い記憶されていた Widget をリビルド対象とする流れは以下の図で表されます。
3-1〜3-2 で、まず MyCounter(Element) がリビルドされ、対応する MyCounterState に 3-3.build を指示します。
MyCounter がリビルドされると、3-4.updateChild 〜 3-5.update メソッドで子 Widget である _InheritedCounter の Element へリビルドが伝播します。
_InheritedCounter(Element) は 3-6. updated の中で _InheritedCounter(Widget) の 3-7updateShouldNotify メソッドを呼ぶことで、状態を参照している Widget の更新を行うか確認します。
カウンターアプリでは、固定で true を返していますので、必ず更新が行われます。
_InheritedCounter はその後 3-8.notifyClient, 3-9. notifyDependencies を呼び出した後、MyHomePage(Element) の 3-10.didChangeDependencies メソッドを呼び出し、その中で MyHomePage(Element) は自身をリビルド対象とします。
そこからの流れは図7 で解説した MyCounter のリビルドの流れと同様のため、説明は割愛します。
図8. MyCounter がリビルドされてから、MyHomePage がリビルド対象となるまでのシーケンス図
最後に、BuildOwner が MyHomePage に対してリビルドを実行し、MyHomePage が再描画されるまでの流れを図9 で説明します。
リビルドの流れは、図8 と同様なので詳しい解説は割愛しますが、MyHomePage は、4-3.build メソッドによりリビルドされた後、自身の子 Widget に対し updateChild メソッドを呼び出し、ビルドの伝播が行われます。
図9. MyHomePage がリビルドされる流れのシーケンス図
以上が、InheritedWidget を利用した状態管理、状態更新の説明となります。
現在は状態管理の手法として、InheritedWidget を使いやすくしてくれる Provider 、その Provider を更に改良した Riverpod というパッケージを利用するのが一般的です。
次回以降のブログでは、Provider や Riverpod といった状態管理パッケージについて説明していこうと思います。
この記事では、Widget の概要と、StatefulWidget, StatelessWidget, InheritedWidget について説明しました。
StatefulWidget は動的に変化する状態を持つ Widget で、状態に応じて変化する UI を構築したい時に利用します。
StatelessWidget は状態を持たない Widget で、静的なデータの表示を行いたい時に利用します。
InheritedWidget は、自身のデータを参照している Widget に直接アクセスすることのできる Widget で、状態管理に利用することができます。
EMoshU は、「仲間」を強く意識できる会社です。
自社開発では、サーバー・フロント・アプリの垣根を越え、より良いプロダクトを創り上げるため、日々協力、切磋琢磨しています。
・今の会社・環境では、現場が変わると人間関係がリセットされてしまい、新しく人間関係を築き続けるのに疲れる。寂しい
・一緒にワイワイ開発出来る仲間が欲しい!
・チームを強くする経験を積みたい!
・自社プロダクトでガンガン Flutter を使っていきたい方
という方、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております。
Flutter を使ったアプリ開発で世の中に貢献したい方、ぜひご応募ください!!
募集要項
まずは話を聞いてみたいという方へ