はじめに
状態管理していて、状態変わっているはずなのに更新されないとか、これが初期値になるはずなのに反映されてないとか、業務で困ったので整理したいと思います。
業務のコードは、サーバーからデータを取ってくるので、非同期に実行されたり、〇〇の場合だけ初期値を××にするだったり、整理しないまま書いていると複雑になりがちです。
なので、単純化したコードで、順番を整理、理解して、複雑な業務のコードを書く際に整理しながら書けたら、バグの防止や作業効率アップにつながるんじゃないかなーと思いました!
子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を更新するのでボタンを押下します
ログは以下になります。
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を更新します。
ログは下記になります。
ChildWidgetA: build
GrandChildWidgetA: build
着目点としては、以下になります。
・ChildWidgetAを更新するとGrandChildAも更新されていること
・GrandChildAのinitStateは呼ばれないこと
親Widgetのbuildが走っても、子WidgetはinitStateから再生成されるわけではなく、buildが走り状態が更新されるだけ。
つまり、initStateは初回表示時のみ動作するということになります。
したがって親Widgetの引数によって状態を変えたい場合、initStateで引数を使い状態を設定するのではなく、build内で引数を使い状態を設定するのが正しい実装になります。
表示・非表示が切り替わった場合
ボタンの押下でGrandChildAの表示・非表示を切り替えます。
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が呼ばれるようです。
終わりに
状態管理について、ドキュメントや記事で読んでぼんやりと理解していたものの、実際に動作を見てみることで理解が深まりました。