【Flutter】initState,build,disposeの順番を整理する

# はじめに
状態管理していて、**状態変わっているはずなのに更新されないとか、これが初期値になるはずなのに反映されてない**とか、業務で困ったので整理したいと思います。
業務のコードは、サーバーからデータを取ってくるので、非同期に実行されたり、〇〇の場合だけ初期値を××にするだったり、整理しないまま書いていると複雑になりがちです。
なので、**単純化したコードで、順番を整理、理解して、複雑な業務のコードを書く際に整理しながら書けたら、バグの防止や作業効率アップ**につながるんじゃないかなーと思いました！

### 子Widgetがそれぞれ独立しているパターン

「それぞれ独立しているパターン」というのは、親Widgetの状態に子Widgetが影響を受けないという意味です。具体的にいうと、子が親から何も引数を受け取らないということです。（後述するコードを見た方が理解しやすいと思います）

Widgetは下記のようなツリーになってます。
```
ParentWidget - ChildWidgetA - GrandChildWidgetA
```

検証するコードは下記です。

```dart
// 親Widget
class ParentWidget extends StatelessWidget {
  const ParentWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('title'),
      ),
      body: const SafeArea(
        child: Center(
          child: Column(
            children: [
              // 子WidgetAを表示
              ChildWidgetA(),
            ],
          ),
        ),
      ),
    );
  }
}

// 子WidgetA
class ChildWidgetA extends StatefulWidget {
  const ChildWidgetA({super.key});

  @override
  State<ChildWidgetA> createState() => _ChildWidgetAState();
}

class _ChildWidgetAState extends State<ChildWidgetA> {
  // 自分の状態としてlabelを持っていて、表示する
  String label = label1;
  static const String label1 = "!!ChildWidgetA!!";
  static const String label2 = "??ChildWidgetA??";

  @override
  void initState() {
    super.initState();
    print("ChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("ChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("ChildWidgetA: build");
    return Container(
      color: Colors.amber,
      child: Column(
        children: [
          // 自分の状態であるlabelを表示
          Text(
            label,
            style: const TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              // ボタンを押下するごとに状態を更新
              setState(() {
                label = label == label1 ? label2 : label1;
              });
            },
            child: const Text("Change ChildWidgetA"),
          ),
          // 孫WidgetAを表示
          const GrandChildWidgetA(),
        ],
      ),
    );
  }
}

// 孫WidgetA
class GrandChildWidgetA extends StatefulWidget {
  const GrandChildWidgetA({super.key});

  @override
  State<GrandChildWidgetA> createState() => _GrandChildWidgetAState();
}

class _GrandChildWidgetAState extends State<GrandChildWidgetA> {
  // 子Widgetと同じく、自分の状態としてlabelを持つ
  String label = label1;
  static const String label1 = "!!GrandChildWidgetA!!";
  static const String label2 = "??GrandChildWidgetA??";

  @override
  void initState() {
    super.initState();
    print("GrandChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("GrandChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("GrandChildWidgetA: build");
    return Container(
      color: Colors.green,
      child: Column(
        children: [
          Text(
            label,
            style: const TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              // ボタン押下ごとにlabelを更新
              setState(() {
                label = label == label1 ? label2 : label1;
              });
            },
            child: const Text("Change GrandChildWidgetA"),
          ),
        ],
      ),
    );
  }
}
```

#### 初回表示時

初回表示時のログは以下のようになります。
```
ChildWidgetA: initState
ChildWidgetA: build
GrandChildWidgetA: initState
GrandChildWidgetA: build
```

特に説明することはないので次へ

#### ChildWidgetAを更新
ChildWidgetAのボタンを押すとChildWidgetAを更新するのでボタンを押下します

![updateChild.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/ce069834-eefb-3290-efc6-6dc0ec6d8f29.gif)

ログは以下になります。

```
ChildWidgetA: build
```

着目点は以下です
・ChildWidgetAの状態を更新すると、buildが実行される。
・ChildWidgetAのinitState,disposeは呼ばれない（状態遷移について知りたい人は[こちら](https://qiita.com/kurun_pan/items/116288b8ab2c409d2ee5)を読むとわかりやすいです）
・GrandChildAは更新されない（[こちら](https://pryogram.com/flutter/const-widget-mean/)を参照）


```
parentWidget - 更新されない
  ┗  ChildWidgetA - 更新
       ┗  GrandChildWidgetA - 更新されない
```

GrandChildAを更新した場合も同様にGrandChildAのみが更新されるので割愛します。

### 親Widgetが子Widgetの状態を持つパターン
親Widgetから引数が渡ってきて、それを表示なり、その引数に合わせて表示を変えたりするパターンです。


[子Widgetがそれぞれ独立しているパターン](#子Widgetがそれぞれ独立しているパターン)と少しコードを変えてます。
概要としては`ChildWidgetA`から`GrandChildWidgetA`に引数を渡しているだけです。

```dart
class ChildWidgetA extends StatefulWidget {
  const ChildWidgetA({super.key});

  @override
  State<ChildWidgetA> createState() => _ChildWidgetAState();
}

class _ChildWidgetAState extends State<ChildWidgetA> {
  String fruits = apple;
  static const String widgetName = "ChildWidgetA";
  static const String apple = "りんご";
  static const String orange = "オレンジ";

  @override
  void initState() {
    super.initState();
    print("ChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("ChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("ChildWidgetA: build");
    return Container(
      color: Colors.amber,
      child: Column(
        children: [
          Text(
            "$widgetName is $fruits",
            style: const TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                fruits == apple ? orange : apple;
              });
            },
            child: const Text("Change ChildWidgetA"),
          ),
          GrandChildWidgetA(
            fruits: fruits,
          ),
        ],
      ),
    );
  }
}

class GrandChildWidgetA extends StatefulWidget {
  const GrandChildWidgetA({
    super.key,
    required this.fruits,
  });

  final String fruits;

  @override
  State<GrandChildWidgetA> createState() => _GrandChildWidgetAState();
}

class _GrandChildWidgetAState extends State<GrandChildWidgetA> {
  String label = label1;
  static const label1 = "AAAA";
  static const label2 = "BBBB";

  @override
  void initState() {
    super.initState();
    print("GrandChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("GrandChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("GrandChildWidgetA: build");
    return Container(
      color: Colors.green,
      child: Column(
        children: [
          Text(
            "Child: ${widget.fruits} GrandChild: $label",
            style: const TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                label = label == label1 ? label2 : label1;
              });
            },
            child: const Text("Change GrandChildWidgetA"),
          ),
        ],
      ),
    );
  }
}
```

#### 初回表示時
こちらは特に変わり無いので割愛します

#### ChildWidgetAを更新
ChildWidgetAのボタンを押下して、ChildWidgetAを更新します。
![WidgetA.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/0d93743c-37d0-fa83-b109-9261a88b59bc.gif)

ログは下記になります。
```
ChildWidgetA: build
GrandChildWidgetA: build
```

着目点としては、以下になります。
・ChildWidgetAを更新するとGrandChildAも更新されていること
・GrandChildAのinitStateは呼ばれないこと

親Widgetのbuildが走っても、子WidgetはinitStateから再生成されるわけではなく、buildが走り状態が更新されるだけ。
つまり、initStateは初回表示時のみ動作するということになります。
したがって親Widgetの引数によって状態を変えたい場合、initStateで引数を使い状態を設定するのではなく、build内で引数を使い状態を設定するのが正しい実装になります。

### 表示・非表示が切り替わった場合

ボタンの押下でGrandChildAの表示・非表示を切り替えます。

![visible.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/7588a086-1830-3c9f-61de-9d233b6a12a9.gif)


```
class ChildWidgetA extends StatefulWidget {
  const ChildWidgetA({super.key});

  @override
  State<ChildWidgetA> createState() => _ChildWidgetAState();
}

class _ChildWidgetAState extends State<ChildWidgetA> {
  static const String widgetName = "ChildWidgetA";
  bool isGrandChildVisible = true;

  @override
  void initState() {
    super.initState();
    print("ChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("ChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("ChildWidgetA: build");
    return Container(
      color: Colors.amber,
      child: Column(
        children: [
          const Text(
            widgetName,
            style: TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                isGrandChildVisible = !isGrandChildVisible;
              });
            },
            child: const Text("Change ChildWidgetA"),
          ),
          if (isGrandChildVisible) const GrandChildWidgetA(),
        ],
      ),
    );
  }
}

class GrandChildWidgetA extends StatefulWidget {
  const GrandChildWidgetA({
    super.key,
  });

  @override
  State<GrandChildWidgetA> createState() => _GrandChildWidgetAState();
}

class _GrandChildWidgetAState extends State<GrandChildWidgetA> {
  String label = label1;
  static const label1 = "AAAA";
  static const label2 = "BBBB";

  @override
  void initState() {
    super.initState();
    print("GrandChildWidgetA: initState");
  }

  @override
  void dispose() {
    super.dispose();
    print("GrandChildWidgetA: dispose");
  }

  @override
  Widget build(BuildContext context) {
    print("GrandChildWidgetA: build");
    return Container(
      color: Colors.green,
      child: Column(
        children: [
          Text(
            "GrandChild: $label",
            style: const TextStyle(
              fontSize: 20,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                label = label == label1 ? label2 : label1;
              });
            },
            child: const Text("Change GrandChildWidgetA"),
          ),
        ],
      ),
    );
  }
}
```

ログは下記です。
（わかりにくかったので一部加工してます。


```
// GrandChildWidgetAを消した時
flutter: ChildWidgetA: build
flutter: GrandChildWidgetA: dispose

// GrandChild WidgetAを表示した時
flutter: ChildWidgetA: build
flutter: GrandChildWidgetA: initState
flutter: GrandChildWidgetA: build
```

表示・非表示を切り替えて、Widgetツリーから削除されるとdisposeが呼ばれ、再びWidgetツリーに追加されるとinitStateが呼ばれるようです。

# 終わりに
状態管理について、ドキュメントや記事で読んでぼんやりと理解していたものの、実際に動作を見てみることで理解が深まりました。