Flutterの状態管理方法

前回記事([FlutterのUI更新方法](https://qiita.com/sebayashi-tomoya/items/433ba00431d96d993d87))の続きです。

FlutterKaigi 2023の登壇動画を参考に今回は状態管理方法についてまとめます。

# 参考動画
[FlutterKaigi 2023 我々にはなぜ Riverpod が必要なのか - InheritedWidget から始まる app state 管理手法の課題 by ちゅーやん（中條 剛）](https://youtu.be/2SnTNFAzmY0?si=dm7rxfZqAI8Sh08E)

# 状態（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を更新してくれます。流れは以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3889500/b7685cb0-0855-8b60-d990-730b8b107fcb.png)
1. 変更したいStateの`setState()`を呼び出す
1. StateオブジェクトがElementに定義されている`markNeedsBuild()`を呼び出す
1. Elementから`build()`が呼び出され、UIが更新される

# InheritedWidgetの場合
InheritedWidgetの場合、参照されている他Elementの情報を保持しており、Stateが更新されると自身のStateを参照しているすべてのElementの`markNeedsBuild()`を呼び出します。InheritedWidgetを参照している他WidgetはStateの更新に伴って自動的にUIが更新されることになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3889500/126831da-8d21-d702-88d7-04a3e9f520b9.png)
1. 参照したいStateを持つInheritedWidgetの型を指定して`dependOnInheritedWidgetOfExactType()`を呼び出す 
1. 参照したInheritedWidgetのStateが変更された場合、`markNeedsBuild()`が呼び出される
1. `build()`が呼び出され、UIが更新される

 Elementは`dependOnInheritedWidgetOfExactType()`が呼び出されると、指定した型のInheritedWidgetが見つかるまでツリーを遡って検索します。
 
例）MyInheritedWidgetというInheritedWidgetを参照したい場合
```dart
final inheritedWidget = 
    context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
```
参照を取得したWidgetでは、MyInheritedWidgetの持つStateが更新されると上図の流れで自動的にUIが更新されます。

## InheritedWidgetのStateの更新方法
InheritedWidgetを参照しているWidgetがStateの更新と共にUIが更新されることは説明しました。では、そもそもInheritedWidgetのStateを更新するためにはどのような手順を踏む必要があるのでしょうか？

私が愛用している[Perplexity](https://www.perplexity.ai/)に聞いてみたところ、一般的には以下の手順で運用することが多いようです。

1. InheritedWidgetをStatefulWidgetでラップする
1. StatefulWidgetの状態を変更するメソッドを提供する
1. そのメソッド内で`setState()`を呼び出し、InheritedWidgetを再構築する

ボタン押下時にInheritedWiidgetのStateを更新する場合、以下のようなコードになります。
```dart
// 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'),
				),
			],
		);
	}
}
```
それぞれの関係性を図で表すとこんな感じですかね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3889500/294a3259-62a0-3b7b-e282-2d0593e22a39.png)

# まとめ
* Flutterにはephemeral stateとapp stateの2種類の状態が存在し、それぞれStatefulWidgetとInheritedWidgetで管理する
* StatefulWidgetでは`setState()`が呼ばれ、状態が更新されると自動的にUIが更新される
* InheritedWidgetでは参照されているWidgetを記憶しておき、状態が更新されるとそのすべてのWidgetのUIを自動更新する