A First Look at Flutter (2): State Management

1. Three common State Management

  • Widget manages its own state.
  • Widget manages child Widget status.
  • Mixed management (both parent and child widgets manage state).

1.1 Widget manages its own status

1.1.1 Applicable scenarios

If the state is related to the appearance of the interface, such as color, animation, then the state is best managed by the Widget itself.

1.1.2 Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class TapboxA extends StatefulWidget {
TapboxA({Key? key}) : super(key: key);

@override
_TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
bool _active = false;

void _handleTap() {
setState(() {
_active = !_active;
});
}

Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}

1.2 Widget manages sub-Widget status

1.2.1 Applicable scenarios

If the state is user data, such as the selected state of a checkbox or the position of a slider, then the state is best managed by the parent Widget.

1.2.2 Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;

void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}

@override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
TapboxB({Key? key, this.active: false, required this.onChanged})
: super(key: key);

final bool active;
final ValueChanged<bool> onChanged;

void _handleTap() {
onChanged(!active);
}

Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}

1.3 Mixed management (both parent and child Widgets manage state)

1.3.1 Applicable scenarios

If a certain state is shared by different Widgets, it is best managed by their common parent Widget.

1.3.2 Example

_ParentWidgetStateC Class:

  • Manage_active status.
  • Implements _handleTapboxChanged(), which is called when the box is clicked.
  • When the box is clicked and the _active status changes, setState() is called to update the UI.

_TapboxCState Object:

  • Manage_highlight status.
  • GestureDetector Listens for all tap events. When the user clicks, it adds a highlight (dark green border); when the user releases, it removes the highlight.
  • Updates the _highlight state when pressed, lifted, or canceled, and calls setState() to update the UI.
  • When clicked, pass state changes to the parent component.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//---------------------------- ParentWidget ----------------------------

class ParentWidgetC extends StatefulWidget {
@override
_ParentWidgetCState createState() => _ParentWidgetCState();
}

class _ParentWidgetCState extends State<ParentWidgetC> {
bool _active = false;

void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}

@override
Widget build(BuildContext context) {
return Container(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
TapboxC({Key? key, this.active: false, required this.onChanged})
: super(key: key);

final bool active;
final ValueChanged<bool> onChanged;

@override
_TapboxCState createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
bool _highlight = false;

void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}

void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}

void _handleTapCancel() {
setState(() {
_highlight = false;
});
}

void _handleTap() {
widget.onChanged(!widget.active);
}

@override
Widget build(BuildContext context) {
// Add green border when pressed, cancel highlighting when lifted
return GestureDetector(
onTapDown: _handleTapDown, // Handle press event
onTapUp: _handleTapUp, // Handle the lift event
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(
widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}

1.4 General selection rules

The encapsulation of state management within the Widget will be better, while the management in the parent Widget will be more flexible.
If you are not sure how to manage the state, the first recommended option is to manage it in the parent Widget (flexibility is more important).

2. Global State Management

Practical example: We have a settings page where you can set the language of the application. In order to make the settings take effect in real time, we expect that the components in the App that depend on the application language can be rebuilt when the language status changes. However, these components that depend on the application language are not together with the settings page, so this situation is difficult to manage using the above method. At this time, the correct approach is to use a global State Manager to handle the communication between such distant components.

  1. Implement a global event bus and represent language-state changes as events. In each app component that depends on the current language, subscribe to the language-change event in initState. When the user changes the language in Settings, publish the event. Subscribers then receive the notification and call setState(...) to rebuild themselves.
  2. Use some packages specifically for State Management, such as Provider and Redux. Readers can view their details on pub.