Skip to content

Commit 4afd78a

Browse files
v1: Fix ListView on-demand items building (#5629)
* Fix on-demand building in ListView Updated GridView, ListView, and ReorderableListView controls to wrap child widgets with ControlWidget using ValueKey based on their IDs. This improves widget identity and performance during rebuilds and reordering. * Refactor ListTileClicks listeners in input controls Moved ListTileClicks notifier listeners from build methods to lifecycle methods (didChangeDependencies/initState/dispose) in checkbox, radio, and switch controls. This ensures listeners are properly registered and disposed, preventing duplicate listeners and potential memory leaks. Also, fixed SwitchControl to use activeThumbColor instead of activeColor. Fix #5627 * Use custom keys for list and grid view items Updated GridView, ListView, and ReorderableListView controls to use a custom key from item.getKey('key') if available, falling back to the item id. This improves widget identity and helps prevent unnecessary rebuilds when item order changes.
1 parent 441d828 commit 4afd78a

10 files changed

Lines changed: 110 additions & 49 deletions

File tree

client/pubspec.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ packages:
2929
dependency: transitive
3030
description:
3131
name: audioplayers
32-
sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de
32+
sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
3333
url: "https://pub.dev"
3434
source: hosted
35-
version: "6.5.0"
35+
version: "6.5.1"
3636
audioplayers_android:
3737
dependency: transitive
3838
description:
@@ -1108,10 +1108,10 @@ packages:
11081108
dependency: transitive
11091109
description:
11101110
name: screen_brightness_android
1111-
sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed
1111+
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
11121112
url: "https://pub.dev"
11131113
source: hosted
1114-
version: "2.1.2"
1114+
version: "2.1.3"
11151115
screen_brightness_platform_interface:
11161116
dependency: transitive
11171117
description:

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ class _CheckboxControlState extends State<CheckboxControl> {
3434
_focusNode.addListener(_onFocusChange);
3535
}
3636

37+
@override
38+
void didChangeDependencies() {
39+
super.didChangeDependencies();
40+
ListTileClicks.of(context)?.notifier.addListener(_toggleValue);
41+
}
42+
3743
void _onFocusChange() {
3844
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
3945
}
4046

4147
@override
4248
void dispose() {
4349
_focusNode.removeListener(_onFocusChange);
50+
ListTileClicks.of(context)?.notifier.removeListener(_toggleValue);
4451
_focusNode.dispose();
4552
super.dispose();
4653
}
@@ -98,11 +105,6 @@ class _CheckboxControlState extends State<CheckboxControl> {
98105
? (bool? value) => _onChange(value)
99106
: null);
100107

101-
// Add listener to ListTile clicks
102-
ListTileClicks.of(context)?.notifier.addListener(() {
103-
_toggleValue();
104-
});
105-
106108
Widget result = checkbox;
107109

108110
var labelStyle =

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> {
3434
_focusNode.addListener(_onFocusChange);
3535
}
3636

37+
@override
38+
void didChangeDependencies() {
39+
super.didChangeDependencies();
40+
ListTileClicks.of(context)?.notifier.addListener(_toggleValue);
41+
}
42+
3743
void _onFocusChange() {
3844
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
3945
}
4046

4147
@override
4248
void dispose() {
4349
_focusNode.removeListener(_onFocusChange);
50+
ListTileClicks.of(context)?.notifier.removeListener(_toggleValue);
4451
_focusNode.dispose();
4552
super.dispose();
4653
}
@@ -92,11 +99,6 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> {
9299
? (bool? value) => _onChange(value)
93100
: null);
94101

95-
// Add listener to ListTile clicks
96-
ListTileClicks.of(context)?.notifier.addListener(() {
97-
_toggleValue();
98-
});
99-
100102
Widget result = cupertinoCheckbox;
101103

102104
var labelStyle =

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,32 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl>
3333
_focusNode.addListener(_onFocusChange);
3434
}
3535

36+
@override
37+
void didChangeDependencies() {
38+
super.didChangeDependencies();
39+
ListTileClicks.of(context)?.notifier.addListener(_toggleRadio);
40+
}
41+
3642
void _onFocusChange() {
3743
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
3844
}
3945

4046
@override
4147
void dispose() {
4248
_focusNode.removeListener(_onFocusChange);
49+
ListTileClicks.of(context)?.notifier.removeListener(_toggleRadio);
4350
_focusNode.dispose();
4451
super.dispose();
4552
}
4653

54+
void _toggleRadio() {
55+
var radioGroup = RadioGroupProvider.of(context);
56+
if (radioGroup != null) {
57+
String value = widget.control.getString("value", "")!;
58+
_onChange(radioGroup, value);
59+
}
60+
}
61+
4762
void _onChange(Control radioGroup, String? value) {
4863
radioGroup.updateProperties({"value": value}, notify: true);
4964
radioGroup.triggerEvent("change", value);
@@ -62,7 +77,6 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl>
6277
debugPrint("CupertinoRadio build: ${widget.control.id}");
6378

6479
var radioGroup = RadioGroupProvider.of(context);
65-
6680
if (radioGroup == null) {
6781
return const ErrorControl(
6882
"CupertinoRadio must be enclosed within RadioGroup");
@@ -88,10 +102,6 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl>
88102
? (String? value) => _onChange(radioGroup, value)
89103
: null);
90104

91-
ListTileClicks.of(context)?.notifier.addListener(() {
92-
_onChange(radioGroup, value);
93-
});
94-
95105
Widget result = cupertinoRadio;
96106
if (label != "") {
97107
var labelWidget = widget.control.disabled

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,21 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> {
2929
super.initState();
3030
_focusNode = FocusNode();
3131
_focusNode.addListener(_onFocusChange);
32+
ListTileClicks.of(context)?.notifier.addListener(_toggleValue);
3233
}
3334

3435
@override
3536
void dispose() {
3637
_focusNode.removeListener(_onFocusChange);
38+
ListTileClicks.of(context)?.notifier.removeListener(_toggleValue);
3739
_focusNode.dispose();
3840
super.dispose();
3941
}
4042

43+
void _toggleValue() {
44+
_onChange(!_value);
45+
}
46+
4147
void _onChange(bool value) {
4248
_value = value;
4349
var props = {"value": value};
@@ -114,10 +120,6 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> {
114120
}
115121
: null);
116122

117-
ListTileClicks.of(context)?.notifier.addListener(() {
118-
_onChange(!_value);
119-
});
120-
121123
Widget result = swtch;
122124
if (label != "") {
123125
var labelWidget = widget.control.disabled

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'package:flutter/widgets.dart';
22

33
import '../controls/control_widget.dart';
4-
import '../extensions/control.dart';
54
import '../models/control.dart';
65
import '../utils/edge_insets.dart';
6+
import '../utils/keys.dart';
77
import '../utils/layout.dart';
88
import '../utils/misc.dart';
99
import '../utils/numbers.dart';
@@ -51,6 +51,7 @@ class _GridViewControlState extends State<GridViewControl> {
5151
final childAspectRatio = widget.control.getDouble("child_aspect_ratio", 1)!;
5252
final reverse = widget.control.getBool("reverse", false)!;
5353
final cacheExtent = widget.control.getDouble("cache_extent");
54+
final controls = widget.control.children("controls");
5455

5556
var clipBehavior =
5657
widget.control.getClipBehavior("clip_behavior", Clip.hardEdge)!;
@@ -89,7 +90,12 @@ class _GridViewControlState extends State<GridViewControl> {
8990
shrinkWrap: shrinkWrap,
9091
padding: padding,
9192
gridDelegate: gridDelegate,
92-
children: widget.control.buildWidgets("controls"),
93+
children: controls
94+
.map((item) => ControlWidget(
95+
key: ValueKey(item.getKey("key")?.value ?? item.id),
96+
control: item,
97+
))
98+
.toList(),
9399
)
94100
: GridView.builder(
95101
scrollDirection: horizontal ? Axis.horizontal : Axis.vertical,
@@ -101,10 +107,13 @@ class _GridViewControlState extends State<GridViewControl> {
101107
shrinkWrap: shrinkWrap,
102108
padding: padding,
103109
gridDelegate: gridDelegate,
104-
itemCount: widget.control.children("controls").length,
110+
itemCount: controls.length,
105111
itemBuilder: (context, index) {
106112
return ControlWidget(
107-
control: widget.control.children("controls")[index]);
113+
key: ValueKey(controls[index].getKey("key")?.value ??
114+
controls[index].id),
115+
control: controls[index],
116+
);
108117
},
109118
);
110119

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import 'package:flet/src/utils/layout.dart';
21
import 'package:flutter/material.dart';
32

43
import '../extensions/control.dart';
54
import '../models/control.dart';
65
import '../utils/edge_insets.dart';
6+
import '../utils/keys.dart';
7+
import '../utils/layout.dart';
78
import '../utils/misc.dart';
89
import '../utils/numbers.dart';
910
import '../widgets/error.dart';
1011
import 'base_controls.dart';
12+
import 'control_widget.dart';
1113
import 'scroll_notification_control.dart';
1214
import 'scrollable_control.dart';
1315

@@ -57,7 +59,7 @@ class _ListViewControlState extends State<ListViewControl> {
5759
var prototypeItem = firstItemPrototype
5860
? widget.control.buildWidget("prototype_item")
5961
: null;
60-
List<Widget> controls = widget.control.buildWidgets("controls");
62+
var controls = widget.control.children("controls");
6163

6264
Widget listView = LayoutBuilder(
6365
builder: (BuildContext context, BoxConstraints constraints) {
@@ -79,7 +81,12 @@ class _ListViewControlState extends State<ListViewControl> {
7981
semanticChildCount: semanticChildCount,
8082
itemExtent: itemExtent,
8183
prototypeItem: prototypeItem,
82-
children: controls,
84+
children: controls
85+
.map((item) => ControlWidget(
86+
key: ValueKey(item.getKey("key")?.value ?? item.id),
87+
control: item,
88+
))
89+
.toList(),
8390
)
8491
: spacing > 0
8592
? ListView.separated(
@@ -92,7 +99,11 @@ class _ListViewControlState extends State<ListViewControl> {
9299
padding: padding,
93100
itemCount: controls.length,
94101
itemBuilder: (context, index) {
95-
return controls[index];
102+
return ControlWidget(
103+
key: ValueKey(controls[index].getKey("key")?.value ??
104+
controls[index].id),
105+
control: controls[index],
106+
);
96107
},
97108
separatorBuilder: (context, index) {
98109
return horizontal
@@ -118,7 +129,11 @@ class _ListViewControlState extends State<ListViewControl> {
118129
itemCount: controls.length,
119130
itemExtent: itemExtent,
120131
itemBuilder: (context, index) {
121-
return controls[index];
132+
return ControlWidget(
133+
key: ValueKey(controls[index].getKey("key")?.value ??
134+
controls[index].id),
135+
control: controls[index],
136+
);
122137
},
123138
prototypeItem: prototypeItem,
124139
);

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,32 @@ class _RadioControlState extends State<RadioControl> {
3232
_focusNode.addListener(_onFocusChange);
3333
}
3434

35+
@override
36+
void didChangeDependencies() {
37+
super.didChangeDependencies();
38+
ListTileClicks.of(context)?.notifier.addListener(_toggleRadio);
39+
}
40+
3541
void _onFocusChange() {
3642
widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur");
3743
}
3844

3945
@override
4046
void dispose() {
4147
_focusNode.removeListener(_onFocusChange);
48+
ListTileClicks.of(context)?.notifier.removeListener(_toggleRadio);
4249
_focusNode.dispose();
4350
super.dispose();
4451
}
4552

53+
void _toggleRadio() {
54+
var radioGroup = RadioGroupProvider.of(context);
55+
if (radioGroup != null) {
56+
String value = widget.control.getString("value", "")!;
57+
_onChange(radioGroup, value);
58+
}
59+
}
60+
4661
void _onChange(Control radioGroup, String? value) {
4762
radioGroup.updateProperties({"value": value}, notify: true);
4863
radioGroup.triggerEvent("change", value);
@@ -95,10 +110,6 @@ class _RadioControlState extends State<RadioControl> {
95110
? (String? value) => _onChange(radioGroup, value)
96111
: null);
97112

98-
ListTileClicks.of(context)?.notifier.addListener(() {
99-
_onChange(radioGroup, value);
100-
});
101-
102113
Widget result = radio;
103114
if (label != "") {
104115
var labelWidget = widget.control.disabled

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import '../extensions/control.dart';
44
import '../models/control.dart';
55
import '../utils/edge_insets.dart';
6+
import '../utils/keys.dart';
67
import '../utils/misc.dart';
78
import '../utils/mouse.dart';
89
import '../utils/numbers.dart';
@@ -62,14 +63,12 @@ class _ListViewControlState extends State<ReorderableListViewControl> {
6263
var anchor = widget.control.getDouble("anchor", 0.0)!;
6364
var clipBehavior =
6465
widget.control.getClipBehavior("clip_behavior", Clip.hardEdge)!;
65-
var controls = _controls
66-
.map((child) => ControlWidget(key: ValueKey(child.id), control: child))
67-
.toList();
6866
var scrollDirection = horizontal ? Axis.horizontal : Axis.vertical;
6967
var header = widget.control.buildWidget("header");
7068
var footer = widget.control.buildWidget("footer");
71-
var prototypeItem =
72-
firstItemPrototype && controls.isNotEmpty ? controls[0] : null;
69+
var prototypeItem = firstItemPrototype && _controls.isNotEmpty
70+
? ControlWidget(key: ValueKey(_controls[0].id), control: _controls[0])
71+
: null;
7372
var autoScrollerVelocityScalar =
7473
widget.control.getDouble("auto_scroller_velocity_scalar");
7574
var mouseCursor = widget.control.getMouseCursor("mouse_cursor");
@@ -110,7 +109,7 @@ class _ListViewControlState extends State<ReorderableListViewControl> {
110109
scrollDirection: scrollDirection,
111110
shrinkWrap: shrinkWrap,
112111
padding: padding,
113-
itemCount: controls.length,
112+
itemCount: _controls.length,
114113
itemExtent: itemExtent,
115114
mouseCursor: mouseCursor,
116115
anchor: anchor,
@@ -122,7 +121,11 @@ class _ListViewControlState extends State<ReorderableListViewControl> {
122121
onReorderEnd: onReorderEnd,
123122
onReorderStart: onReorderStart,
124123
itemBuilder: (context, index) {
125-
return controls[index];
124+
return ControlWidget(
125+
key: ValueKey(_controls[index].getKey("key")?.value ??
126+
_controls[index].id),
127+
control: _controls[index],
128+
);
126129
},
127130
)
128131
: ReorderableListView(
@@ -144,7 +147,12 @@ class _ListViewControlState extends State<ReorderableListViewControl> {
144147
onReorder: onReorder,
145148
onReorderEnd: onReorderEnd,
146149
onReorderStart: onReorderStart,
147-
children: controls,
150+
children: _controls
151+
.map((item) => ControlWidget(
152+
key: ValueKey(item.getKey("key")?.value ?? item.id),
153+
control: item,
154+
))
155+
.toList(),
148156
);
149157

150158
child = ScrollableControl(

0 commit comments

Comments
 (0)