2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutterの状態管理方法

Last updated at Posted at 2024-09-22

前回記事(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 stateStatefulWidgetで管理し、ユーザー情報等のアプリ全体で共有したいapp stateInheritedWidgetというWidgetを使って管理します。

次はそれぞれのWidgetがどのように状態を変更し、UIを更新しているのかを見ていきます。

StatefulWidgetの場合

StatefulWidgetでは前回記事で説明したように、変更したいStateのsetState()を呼ぶことによって自動的にUIを更新してくれます。流れは以下の通りです。
image.png

  1. 変更したいStateのsetState()を呼び出す
  2. StateオブジェクトがElementに定義されているmarkNeedsBuild()を呼び出す
  3. Elementからbuild()が呼び出され、UIが更新される

InheritedWidgetの場合

InheritedWidgetの場合、参照されている他Elementの情報を保持しており、Stateが更新されると自身のStateを参照しているすべてのElementのmarkNeedsBuild()を呼び出します。InheritedWidgetを参照している他WidgetはStateの更新に伴って自動的にUIが更新されることになります。
image.png

  1. 参照したいStateを持つInheritedWidgetの型を指定してdependOnInheritedWidgetOfExactType()を呼び出す
  2. 参照したInheritedWidgetのStateが変更された場合、markNeedsBuild()が呼び出される
  3. 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に聞いてみたところ、一般的には以下の手順で運用することが多いようです。

  1. InheritedWidgetをStatefulWidgetでラップする
  2. StatefulWidgetの状態を変更するメソッドを提供する
  3. そのメソッド内で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'),
				),
			],
		);
	}
}

それぞれの関係性を図で表すとこんな感じですかね
image.png

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?