Skip to content

Commit 151309d

Browse files
Store dialogs state in controls to avoid stateful widget state drifting (#5815)
* Refactor dialog and picker controls to use StatelessWidget Converted AlertDialog, Banner, BottomSheet, CupertinoAlertDialog, DatePicker, DateRangePicker, SnackBar, and TimePicker controls from StatefulWidget to StatelessWidget. State management is now handled via control properties and post-frame callbacks, simplifying the widget structure and improving consistency across dialog and picker controls. * Refactor dialog dismissal event types and update picker dates Changed event handler parameter types from specific dialog controls to the generic ft.DialogControl in several examples for consistency. Updated date and date range picker examples to use dynamic date ranges based on the current date.
1 parent 568ac6f commit 151309d

File tree

13 files changed

+353
-515
lines changed

13 files changed

+353
-515
lines changed

packages/flet/lib/src/controls/alert_dialog.dart

Lines changed: 57 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -13,139 +13,98 @@ import '../widgets/control_inherited_notifier.dart';
1313
import '../widgets/error.dart';
1414
import 'control_widget.dart';
1515

16-
class AlertDialogControl extends StatefulWidget {
16+
class AlertDialogControl extends StatelessWidget {
1717
final Control control;
1818

19-
AlertDialogControl({Key? key, required this.control})
20-
: super(key: key ?? ValueKey("control_${control.id}"));
19+
const AlertDialogControl({super.key, required this.control});
2120

22-
@override
23-
State<AlertDialogControl> createState() => _AlertDialogControlState();
24-
}
25-
26-
class _AlertDialogControlState extends State<AlertDialogControl> {
27-
bool _open = false;
28-
NavigatorState? _navigatorState;
29-
String? _error;
30-
31-
@override
32-
void didChangeDependencies() {
33-
super.didChangeDependencies();
34-
_navigatorState = Navigator.of(context);
35-
_toggleDialog();
36-
}
37-
38-
@override
39-
void didUpdateWidget(covariant AlertDialogControl oldWidget) {
40-
super.didUpdateWidget(oldWidget);
41-
_toggleDialog();
42-
}
43-
44-
@override
45-
void dispose() {
46-
debugPrint("AlertDialog.dispose: ${widget.control.id}");
47-
_closeDialog();
48-
super.dispose();
49-
}
50-
51-
Widget _createAlertDialog() {
21+
Widget _createAlertDialog(BuildContext context) {
5222
return ControlInheritedNotifier(
53-
notifier: widget.control,
23+
notifier: control,
5424
child: Builder(builder: (context) {
5525
ControlInheritedNotifier.of(context);
56-
var title = widget.control.get("title");
26+
var title = control.get("title");
5727
return AlertDialog(
5828
title: title is Control
5929
? ControlWidget(control: title)
6030
: title is String
6131
? Text(title)
6232
: null,
63-
titlePadding: widget.control.getPadding("title_padding"),
64-
content: widget.control.buildWidget("content"),
65-
contentPadding: widget.control.getPadding("content_padding",
33+
titlePadding: control.getPadding("title_padding"),
34+
content: control.buildWidget("content"),
35+
contentPadding: control.getPadding("content_padding",
6636
const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0))!,
67-
actions: widget.control.buildWidgets("actions"),
68-
actionsPadding: widget.control.getPadding("actions_padding"),
69-
actionsAlignment:
70-
widget.control.getMainAxisAlignment("actions_alignment"),
71-
shape: widget.control.getShape("shape", Theme.of(context)),
72-
semanticLabel: widget.control.getString("semantics_label"),
73-
insetPadding: widget.control.getPadding("inset_padding",
37+
actions: control.buildWidgets("actions"),
38+
actionsPadding: control.getPadding("actions_padding"),
39+
actionsAlignment: control.getMainAxisAlignment("actions_alignment"),
40+
shape: control.getShape("shape", Theme.of(context)),
41+
semanticLabel: control.getString("semantics_label"),
42+
insetPadding: control.getPadding("inset_padding",
7443
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0))!,
75-
iconPadding: widget.control.getPadding("icon_padding"),
76-
backgroundColor: widget.control.getColor("bgcolor", context),
77-
buttonPadding: widget.control.getPadding("action_button_padding"),
78-
shadowColor: widget.control.getColor("shadow_color", context),
79-
elevation: widget.control.getDouble("elevation"),
44+
iconPadding: control.getPadding("icon_padding"),
45+
backgroundColor: control.getColor("bgcolor", context),
46+
buttonPadding: control.getPadding("action_button_padding"),
47+
shadowColor: control.getColor("shadow_color", context),
48+
elevation: control.getDouble("elevation"),
8049
clipBehavior:
81-
parseClip(widget.control.getString("clip_behavior"), Clip.none)!,
82-
icon: widget.control.buildIconOrWidget("icon"),
83-
iconColor: widget.control.getColor("icon_color", context),
84-
scrollable: widget.control.getBool("scrollable", false)!,
50+
parseClip(control.getString("clip_behavior"), Clip.none)!,
51+
icon: control.buildIconOrWidget("icon"),
52+
iconColor: control.getColor("icon_color", context),
53+
scrollable: control.getBool("scrollable", false)!,
8554
actionsOverflowButtonSpacing:
86-
widget.control.getDouble("actions_overflow_button_spacing"),
87-
alignment: widget.control.getAlignment("alignment"),
88-
contentTextStyle: widget.control
89-
.getTextStyle("content_text_style", Theme.of(context)),
90-
titleTextStyle: widget.control
91-
.getTextStyle("title_text_style", Theme.of(context)),
55+
control.getDouble("actions_overflow_button_spacing"),
56+
alignment: control.getAlignment("alignment"),
57+
contentTextStyle:
58+
control.getTextStyle("content_text_style", Theme.of(context)),
59+
titleTextStyle:
60+
control.getTextStyle("title_text_style", Theme.of(context)),
9261
);
9362
}),
9463
);
9564
}
9665

97-
void _toggleDialog() {
98-
debugPrint("AlertDialog build: ${widget.control.id}");
99-
100-
var open = widget.control.getBool("open", false)!;
101-
var modal = widget.control.getBool("modal", false)!;
102-
103-
if (open && (open != _open)) {
104-
if (widget.control.get("title") == null &&
105-
widget.control.get("content") == null &&
106-
widget.control.children("actions").isEmpty) {
107-
_error =
108-
"AlertDialog has nothing to display. Provide at minimum one of the following: title, content, actions.";
109-
return;
66+
@override
67+
Widget build(BuildContext context) {
68+
debugPrint("AlertDialog build: ${control.id}");
69+
70+
var open = control.getBool("open", false)!;
71+
final lastOpen = control.getBool("_open", false)!;
72+
var modal = control.getBool("modal", false)!;
73+
74+
if (open && (open != lastOpen)) {
75+
if (control.get("title") == null &&
76+
control.get("content") == null &&
77+
control.children("actions").isEmpty) {
78+
return const ErrorControl(
79+
"AlertDialog has nothing to display. Provide at minimum one of the following: title, content, actions.");
11080
}
11181

112-
_open = open;
82+
control.updateProperties({"_open": open}, python: false);
11383

11484
WidgetsBinding.instance.addPostFrameCallback((_) {
11585
showDialog(
11686
barrierDismissible: !modal,
117-
barrierColor: widget.control.getColor("barrier_color", context),
87+
barrierColor: control.getColor("barrier_color", context),
11888
useRootNavigator: false,
11989
context: context,
120-
builder: (context) => _createAlertDialog()).then((value) {
121-
debugPrint("Dismissing AlertDialog(${widget.control.id})");
122-
_open = false;
123-
widget.control.updateProperties({"open": false});
124-
widget.control.triggerEvent("dismiss");
90+
builder: (context) => _createAlertDialog(context)).then((value) {
91+
debugPrint("Dismissing AlertDialog(${control.id})");
92+
control.updateProperties({"_open": false}, python: false);
93+
control.updateProperties({"open": false});
94+
control.triggerEvent("dismiss");
12595
});
12696
});
127-
} else if (!open && _open) {
128-
_closeDialog();
129-
}
130-
}
131-
132-
@override
133-
Widget build(BuildContext context) {
134-
return _error != null ? ErrorControl(_error!) : const SizedBox.shrink();
135-
}
136-
137-
void _closeDialog() {
138-
if (_open) {
139-
if (_navigatorState?.canPop() == true) {
97+
} else if (!open && lastOpen) {
98+
if (Navigator.of(context).canPop() == true) {
14099
debugPrint(
141-
"AlertDialog(${widget.control.id}): Closing dialog managed by this widget.");
142-
_navigatorState?.pop();
143-
_open = false;
144-
_error = null;
100+
"AlertDialog(${control.id}): Closing dialog managed by this widget.");
101+
Navigator.of(context).pop();
102+
control.updateProperties({"_open": false}, python: false);
145103
} else {
146104
debugPrint(
147-
"AlertDialog(${widget.control.id}): Dialog was not opened by this widget, skipping pop.");
105+
"AlertDialog(${control.id}): Dialog was not opened by this widget, skipping pop.");
148106
}
149107
}
108+
return const SizedBox.shrink();
150109
}
151110
}

packages/flet/lib/src/controls/banner.dart

Lines changed: 62 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -8,114 +8,79 @@ import '../utils/numbers.dart';
88
import '../utils/text.dart';
99
import '../widgets/error.dart';
1010

11-
class BannerControl extends StatefulWidget {
11+
class BannerControl extends StatelessWidget {
1212
final Control control;
1313

14-
BannerControl({Key? key, required this.control})
15-
: super(key: key ?? ValueKey("control_${control.id}"));
14+
const BannerControl({super.key, required this.control});
1615

17-
@override
18-
State<BannerControl> createState() => _BannerControlState();
19-
}
20-
21-
class _BannerControlState extends State<BannerControl> {
22-
String? _error;
23-
24-
@override
25-
void initState() {
26-
super.initState();
27-
debugPrint("Banner.initState: ${widget.control.id}");
28-
}
29-
30-
@override
31-
void didChangeDependencies() {
32-
super.didChangeDependencies();
33-
debugPrint("Banner.didChangeDependencies: ${widget.control.id}");
34-
_toggleBanner();
35-
}
36-
37-
@override
38-
void didUpdateWidget(covariant BannerControl oldWidget) {
39-
debugPrint("Banner.didUpdateWidget: ${widget.control.id}");
40-
super.didUpdateWidget(oldWidget);
41-
_toggleBanner();
42-
}
43-
44-
MaterialBanner _createBanner() {
16+
MaterialBanner _createBanner(BuildContext context) {
4517
return MaterialBanner(
46-
leading: widget.control.buildIconOrWidget("leading"),
47-
leadingPadding: widget.control.getPadding("leading_padding"),
48-
content: widget.control.buildTextOrWidget("content")!,
49-
padding: widget.control.getPadding("content_padding"),
50-
actions: widget.control.buildWidgets("actions"),
51-
forceActionsBelow: widget.control.getBool("force_actions_below", false)!,
52-
backgroundColor: widget.control.getColor("bgcolor", context),
18+
leading: control.buildIconOrWidget("leading"),
19+
leadingPadding: control.getPadding("leading_padding"),
20+
content: control.buildTextOrWidget("content")!,
21+
padding: control.getPadding("content_padding"),
22+
actions: control.buildWidgets("actions"),
23+
forceActionsBelow: control.getBool("force_actions_below", false)!,
24+
backgroundColor: control.getColor("bgcolor", context),
5325
contentTextStyle:
54-
widget.control.getTextStyle("content_text_style", Theme.of(context)),
55-
shadowColor: widget.control.getColor("shadow_color", context),
56-
dividerColor: widget.control.getColor("divider_color", context),
57-
elevation: widget.control.getDouble("elevation"),
58-
minActionBarHeight:
59-
widget.control.getDouble("min_action_bar_height", 52.0)!,
60-
margin: widget.control.getMargin("margin"),
26+
control.getTextStyle("content_text_style", Theme.of(context)),
27+
shadowColor: control.getColor("shadow_color", context),
28+
dividerColor: control.getColor("divider_color", context),
29+
elevation: control.getDouble("elevation"),
30+
minActionBarHeight: control.getDouble("min_action_bar_height", 52.0)!,
31+
margin: control.getMargin("margin"),
6132
onVisible: () {
62-
widget.control.triggerEvent("visible");
33+
control.triggerEvent("visible");
6334
},
6435
);
6536
}
6637

67-
void _toggleBanner() {
68-
var dismissed = widget.control.get("_dismissed");
69-
70-
debugPrint("Banner build: ${widget.control.id}, _dismissed=$dismissed");
71-
72-
if (dismissed == true) return;
73-
74-
final lastOpen = widget.control.getBool("_open", false)!;
75-
var open = widget.control.getBool("open", false)!;
76-
77-
if (open && (open != lastOpen)) {
78-
if (widget.control.get("content") == null) {
79-
_error = "Banner.content must be provided and visible";
80-
return;
81-
} else if (widget.control.children("actions").isEmpty) {
82-
_error =
83-
"Banner.actions must be provided and at least one action should be visible";
84-
return;
85-
}
86-
87-
widget.control.updateProperties({"_open": open}, python: false);
88-
89-
WidgetsBinding.instance.addPostFrameCallback((_) {
90-
ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
91-
ScaffoldMessenger.of(context)
92-
.showMaterialBanner(_createBanner())
93-
.closed
94-
.then((reason) {
95-
debugPrint(
96-
"Closing Banner(${widget.control.id}) with reason: $reason");
97-
if (widget.control.get("_dismissed") != true) {
98-
widget.control
99-
.updateProperties({"_dismissed": true}, python: false);
100-
debugPrint(
101-
"Dismissing Banner(${widget.control.id}) with reason: $reason");
102-
//_open = false;
103-
widget.control.updateProperties({"_open": false}, python: false);
104-
widget.control.updateProperties({"open": false});
105-
widget.control.triggerEvent("dismiss");
106-
}
107-
});
108-
});
109-
} else if (!open && lastOpen) {
110-
WidgetsBinding.instance.addPostFrameCallback((_) {
111-
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
112-
widget.control.updateProperties({"_open": false}, python: false);
113-
});
114-
}
115-
}
116-
11738
@override
11839
Widget build(BuildContext context) {
119-
return _error != null ? ErrorControl(_error!) : const SizedBox.shrink();
40+
final dismissed = control.getBool("_dismissed", false)!;
41+
42+
debugPrint("Banner build: ${control.id}, _dismissed=$dismissed");
43+
44+
if (!dismissed) {
45+
final lastOpen = control.getBool("_open", false)!;
46+
var open = control.getBool("open", false)!;
47+
48+
if (open && (open != lastOpen)) {
49+
if (control.get("content") == null) {
50+
return const ErrorControl(
51+
"Banner.content must be provided and visible");
52+
} else if (control.children("actions").isEmpty) {
53+
return const ErrorControl(
54+
"Banner.actions must be provided and at least one action should be visible");
55+
}
56+
57+
control.updateProperties({"_open": open}, python: false);
58+
59+
WidgetsBinding.instance.addPostFrameCallback((_) {
60+
ScaffoldMessenger.of(context).removeCurrentMaterialBanner();
61+
ScaffoldMessenger.of(context)
62+
.showMaterialBanner(_createBanner(context))
63+
.closed
64+
.then((reason) {
65+
debugPrint("Closing Banner(${control.id}) with reason: $reason");
66+
if (control.get("_dismissed") != true) {
67+
control.updateProperties({"_dismissed": true});
68+
debugPrint(
69+
"Dismissing Banner(${control.id}) with reason: $reason");
70+
//_open = false;
71+
control.updateProperties({"_open": false}, python: false);
72+
control.updateProperties({"open": false});
73+
control.triggerEvent("dismiss");
74+
}
75+
});
76+
});
77+
} else if (!open && lastOpen) {
78+
WidgetsBinding.instance.addPostFrameCallback((_) {
79+
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
80+
control.updateProperties({"_open": false}, python: false);
81+
});
82+
}
83+
}
84+
return const SizedBox.shrink();
12085
}
12186
}

0 commit comments

Comments
 (0)