導入
第4弾はStatefulWidgetです。
roadmap.shではWidgetsの項目のうち「Personal Recommendation」に該当するものもStatefulWidgetを含めてあと2つになりました。
「Alternative option」の2つも中身は確認しておいて損はないかと思いますが、まずは紫を潰していきます。
StatefulWidget
状態(state)による影響を受けるWidgetです。
状態は、
(1)Widgetが構築される際に同期的に読み取ることができ、
(2)Widgetのライフタイム中に変更される可能性がある情報です。
状態が変化したときにState.setStateを使用してStateに通知する必要があります。
StatefulWidgetは、動的に変化する可能性があるUIの一部を記述する場合に役立ちます。構成情報とBuildContextにのみ依存する構成の場合は、StatelessWidgetの使用を検討した方が良いです。
StatefulWidget自体は不変です。createStateメソッドによって作成される別々のStateオブジェクトにその変更可能な状態を格納するか、そのStateが購読するオブジェクト(例:StreamやChangeNotifierオブジェクト)に格納します。
フレームワークはStatefulWidgetをインフレート(展開?)するたびにcreateStateを呼び出します。
つまり、そのWidgetが複数の場所に挿入された場合、同じStatefulWidgetに複数のStateオブジェクトが関連付けられる可能性があります。
同様に、StatefulWidgetがツリーから削除され、後に再挿入された場合、フレームワークは新しいStateオブジェクトを作成するために再びcreateStateを呼び出します。
これにより、Stateオブジェクトのライフサイクルが簡素化されます。
StatefulWidgetは、GlobalKeyがキーとして使用されている場合、ツリー内の異なる場所に移動しても同じStateオブジェクトを保持します。フレームワークはこの特性を利用して、GlobalKeyを持つWidgetをツリー内の一か所から別の場所に移動する際、新しい場所でサブツリーを再作成するのではなく、古い場所から新しい場所に移植します。Stateオブジェクトもサブツリーと一緒に移植されるため、新しい場所でStateオブジェクトが再利用されます。
パフォーマンスの考慮
StatefulWidgetには主に2つのカテゴリーがあります。
1. State.setStateを呼び出さないWidget
State.initStateでリソースを割り当て、State.disposeでそれらを解放するが、InheritedWidgetsに依存しないWidgetです。
アプリケーションやページのルートで使用され、ChangeNotifiers、Streams、その他のオブジェクトを介してサブウィジェットとやりとりします。
このパターンのStatefulWidgetは、一度構築されると更新されないため、CPUおよびGPUサイクルの観点から比較的安価です。
これにより、複雑で深い構成のメソッドを持つことができます。
2. State.setStateを使用するかInheritedWidgetsに依存するWidget
アプリケーションのライフタイム中に何度も再構築されるため、その影響を最小限に抑えることが重要。
こちらもState.initStateやState.didChangeDependenciesを使用してリソースを割り当てることがありますが、重要なのは再構築です。
再構築の影響を最小限に抑えるテクニック
- 状態をリーフに押し出す
- ページ全体を再構築するのではなく、状態を持つ専用のWidgetを作成してその中で更新するようにする
- buildメソッドおよびそれが作成するWidgetによって生成されるノードの数を抑える
- 理想的には、StatefulWidgetは1つのWidgetのみを作成する
- そのWidgetはRenderObjectWidgetであるべき
- サブツリーが変更されない場合、それを表すWidgetをキャッシュしておき、再利用する
- Widgetをfinalの状態変数に割り当て、buildメソッドで再利用する
- 別のキャッシュ戦略として、Widgetの変更可能な部分をchildパラメータを受け取るStatefulWidgetに抽出する場合もある
- 可能な限りconstウィジェットを使用する
- 作成されたサブツリーの深さやサブツリー内のWidgetのタイプを変更しない
- 例
- childか、IgnorePointerでラップされたchildを返す場合、その代わりに、常にchildウィジェットをIgnorePointerでラップし、IgnorePointer.ignoringプロパティをコントロールする
- これは、サブツリーの深さを変更すると、サブツリー全体の再構築等が行われるが、プロパティの変更だけであれば、レンダーツリーへの変更が最小限に抑えられる
- childか、IgnorePointerでラップされたchildを返す場合、その代わりに、常にchildウィジェットをIgnorePointerでラップし、IgnorePointer.ignoringプロパティをコントロールする
- 例
- 深さの変更が必要な場合、サブツリーの共通部分をWidgetにラップし、そのWidgetにStatefulWidgetのライフタイム全体で一貫したGlobalKeyを持たせる
- 再利用可能なUIを作成する場合、ヘルパーメソッドよりもWidgetを使用する
- 例えば、関数でWidgetを生成する場合、State.setStateの呼び出しは、返却されるラッピングウィジェット全体を再構築する必要がある
- Widgetの場合、Flutterは本当に更新が必要な部分だけを効率的に再レンダリングできる
- 作成されたウィジェットがconstであれば、Flutterは再構築作業のほとんどを省略できる
StatefulWidgetの例
Stateに実際の状態がないパターン
状態は通常、プライベートメンバーフィールドとして表される。
また、通常、Widgetにはより多くのコンストラクタ引数があり、それぞれがfinalプロパティに対応する。
class YellowBird extends StatefulWidget {
const YellowBird({ super.key });
@override
State<YellowBird> createState() => _YellowBirdState();
}
class _YellowBirdState extends State<YellowBird> {
@override
Widget build(BuildContext context) {
return Container(color: const Color(0xFFFFE306));
}
}
状態を持つ場合
内部状態を持ち、それを変えるためのメソッドがある例です。
class Bird extends StatefulWidget {
const Bird({
super.key,
this.color = const Color(0xFFFFE306),
this.child,
});
final Color color;
final Widget? child;
@override
State<Bird> createState() => _BirdState();
}
class _BirdState extends State<Bird> {
double _size = 1.0;
void grow() {
setState(() { _size += 0.1; });
}
@override
Widget build(BuildContext context) {
return Container(
color: widget.color,
transform: Matrix4.diagonal3Values(_size, _size, 1.0),
child: widget.child,
);
}
}
Widgetのコンストラクタは名前付き引数のみを使用する。また、最初の引数はkeyであり、最後の引数はchild、children、またはそれに相当するものである。
終わりに
StatefulWidgetは、Flutterアプリケーションにおいて動的に変化するUIを構築するのに大変有益です。状態管理の概念や再構築の影響を理解し、パフォーマンスを最適化するためのテクニックを活用することで、より効率的でレスポンシブなアプリを作成していけたらと思います。