0
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?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

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

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

Widgetは下記のようなツリーになってます。

ParentWidget - ChildWidgetA - GrandChildWidgetA

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

// 親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

ログは以下になります。

ChildWidgetA: build

着目点は以下です
・ChildWidgetAの状態を更新すると、buildが実行される。
・ChildWidgetAのinitState,disposeは呼ばれない(状態遷移について知りたい人はこちらを読むとわかりやすいです)
・GrandChildAは更新されない(こちらを参照)

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

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

親Widgetが子Widgetの状態を持つパターン

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

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

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

ログは下記になります。

ChildWidgetA: build
GrandChildWidgetA: build

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

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

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

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

visible.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が呼ばれるようです。

終わりに

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

0
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
0
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?