前回記事(FlutterのUI更新方法)の続きです。
FlutterKaigi 2023の登壇動画を参考に今回は状態管理方法についてまとめます。
参考動画
FlutterKaigi 2023 我々にはなぜ Riverpod が必要なのか - InheritedWidget から始まる app state 管理手法の課題 by ちゅーやん(中條 剛)
状態(State)の種類
まず、Flutterには管理するStateの種類が2種類存在します。
参照できる範囲によって判別され、それぞれ管理するWidgetが用意されています。
特徴と関係性は以下の通りです。
名前 | 参照できる範囲 | 管理用Widget | 使用例 |
---|---|---|---|
ephemeral state | 単一のWidget内 | StatefulWidget | 入力フォーム、アニメーション、カウンターなど |
app state | 複数のWidget | InhetitedWidget | テーマ、ユーザー情報など |
入力フォームの文字列等の単一のWidget(画面)のみで使うWidget固有のephemeral state
はStatefulWidget
で管理し、ユーザー情報等のアプリ全体で共有したいapp state
はInheritedWidget
というWidgetを使って管理します。
次はそれぞれのWidgetがどのように状態を変更し、UIを更新しているのかを見ていきます。
StatefulWidgetの場合
StatefulWidgetでは前回記事で説明したように、変更したいStateのsetState()
を呼ぶことによって自動的にUIを更新してくれます。流れは以下の通りです。
- 変更したいStateの
setState()
を呼び出す - StateオブジェクトがElementに定義されている
markNeedsBuild()
を呼び出す - Elementから
build()
が呼び出され、UIが更新される
InheritedWidgetの場合
InheritedWidgetの場合、参照されている他Elementの情報を保持しており、Stateが更新されると自身のStateを参照しているすべてのElementのmarkNeedsBuild()
を呼び出します。InheritedWidgetを参照している他WidgetはStateの更新に伴って自動的にUIが更新されることになります。
- 参照したいStateを持つInheritedWidgetの型を指定して
dependOnInheritedWidgetOfExactType()
を呼び出す - 参照したInheritedWidgetのStateが変更された場合、
markNeedsBuild()
が呼び出される -
build()
が呼び出され、UIが更新される
ElementはdependOnInheritedWidgetOfExactType()
が呼び出されると、指定した型のInheritedWidgetが見つかるまでツリーを遡って検索します。
例)MyInheritedWidgetというInheritedWidgetを参照したい場合
final inheritedWidget =
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
参照を取得したWidgetでは、MyInheritedWidgetの持つStateが更新されると上図の流れで自動的にUIが更新されます。
InheritedWidgetのStateの更新方法
InheritedWidgetを参照しているWidgetがStateの更新と共にUIが更新されることは説明しました。では、そもそもInheritedWidgetのStateを更新するためにはどのような手順を踏む必要があるのでしょうか?
私が愛用しているPerplexityに聞いてみたところ、一般的には以下の手順で運用することが多いようです。
- InheritedWidgetをStatefulWidgetでラップする
- StatefulWidgetの状態を変更するメソッドを提供する
- そのメソッド内で
setState()
を呼び出し、InheritedWidgetを再構築する
ボタン押下時にInheritedWiidgetのStateを更新する場合、以下のようなコードになります。
// InheritedWidgetをラップするStatefulWidget
class MyInheritedWidgetWrapper extends StatefulWidget {
final Widget child;
MyInheritedWidgetWrapper({required this.child});
static MyInheritedWidgetWrapperState of(BuildContext context) {
return context.findAncestorStateOfType<MyInheritedWidgetWrapperState>()!;
}
@override
MyInheritedWidgetWrapperState createState() => MyInheritedWidgetWrapperState();
}
// StatefulWidgetと対で生成されるStateオブジェクト
class MyInheritedWidgetWrapperState extends State<MyInheritedWidgetWrapper> {
int value = 0;
// 2. StatefulWidgetの状態を変更するメソッドを提供する
void updateValue(int newValue) {
// 3. そのメソッド内でsetState()を呼び出し、InheritedWidgetを再構築する
setState(() {
value = newValue;
});
}
// 1. InheritedWidgetをStatefulWidgetでラップする
@override
Widget build(BuildContext context) {
return MyInheritedWidget(
value: value ,
child: widget.child,
);
}
}
// 対象のInheritedWidget
class MyInheritedWidget extends InheritedWidget {
final int value;
MyInheritedWidget({required this.value, required Widget child}) : super(child: child);
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return value!= oldWidget.value;
}
static MyInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
}
}
// Stateとボタンを表示するWidget
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// MyInheritedWidgetを参照を取得
final myInheritedWidget = MyInheritedWidget.of(context);
return Column(
children: [
Text('Value: ${myInheritedWidget.value}'),
ElevatedButton(
onPressed: () {
// 値を更新 (自動的にMyWidgetがリビルドされる)
MyInheritedWidgetWrapper.of(context).updateValue(myInheritedWidget.value + 1);
},
child: Text('Increment'),
),
],
);
}
}
まとめ
- Flutterにはephemeral stateとapp stateの2種類の状態が存在し、それぞれStatefulWidgetとInheritedWidgetで管理する
- StatefulWidgetでは
setState()
が呼ばれ、状態が更新されると自動的にUIが更新される - InheritedWidgetでは参照されているWidgetを記憶しておき、状態が更新されるとそのすべてのWidgetのUIを自動更新する