From 13cb8822bc4089a15bfd39447a61c32dadfb3fed Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 9 Sep 2025 11:48:38 -0700 Subject: [PATCH 1/3] 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. --- client/pubspec.lock | 8 +++---- packages/flet/lib/src/controls/grid_view.dart | 14 +++++++++--- packages/flet/lib/src/controls/list_view.dart | 20 +++++++++++++---- .../src/controls/reorderable_list_view.dart | 22 ++++++++++++------- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/client/pubspec.lock b/client/pubspec.lock index 2ef3a23aa7..a6032bd936 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: audioplayers - sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de + sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.5.1" audioplayers_android: dependency: transitive description: @@ -1108,10 +1108,10 @@ packages: dependency: transitive description: name: screen_brightness_android - sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed + sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" screen_brightness_platform_interface: dependency: transitive description: diff --git a/packages/flet/lib/src/controls/grid_view.dart b/packages/flet/lib/src/controls/grid_view.dart index 85f67791f1..27ca8240fc 100644 --- a/packages/flet/lib/src/controls/grid_view.dart +++ b/packages/flet/lib/src/controls/grid_view.dart @@ -51,6 +51,7 @@ class _GridViewControlState extends State { final childAspectRatio = widget.control.getDouble("child_aspect_ratio", 1)!; final reverse = widget.control.getBool("reverse", false)!; final cacheExtent = widget.control.getDouble("cache_extent"); + final controls = widget.control.children("controls"); var clipBehavior = widget.control.getClipBehavior("clip_behavior", Clip.hardEdge)!; @@ -89,7 +90,12 @@ class _GridViewControlState extends State { shrinkWrap: shrinkWrap, padding: padding, gridDelegate: gridDelegate, - children: widget.control.buildWidgets("controls"), + children: controls + .map((item) => ControlWidget( + key: ValueKey(item.id), + control: item, + )) + .toList(), ) : GridView.builder( scrollDirection: horizontal ? Axis.horizontal : Axis.vertical, @@ -101,10 +107,12 @@ class _GridViewControlState extends State { shrinkWrap: shrinkWrap, padding: padding, gridDelegate: gridDelegate, - itemCount: widget.control.children("controls").length, + itemCount: controls.length, itemBuilder: (context, index) { return ControlWidget( - control: widget.control.children("controls")[index]); + key: ValueKey(controls[index].id), + control: controls[index], + ); }, ); diff --git a/packages/flet/lib/src/controls/list_view.dart b/packages/flet/lib/src/controls/list_view.dart index 4fed71b539..d28e3e12c2 100644 --- a/packages/flet/lib/src/controls/list_view.dart +++ b/packages/flet/lib/src/controls/list_view.dart @@ -8,6 +8,7 @@ import '../utils/misc.dart'; import '../utils/numbers.dart'; import '../widgets/error.dart'; import 'base_controls.dart'; +import 'control_widget.dart'; import 'scroll_notification_control.dart'; import 'scrollable_control.dart'; @@ -57,7 +58,7 @@ class _ListViewControlState extends State { var prototypeItem = firstItemPrototype ? widget.control.buildWidget("prototype_item") : null; - List controls = widget.control.buildWidgets("controls"); + var controls = widget.control.children("controls"); Widget listView = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -79,7 +80,12 @@ class _ListViewControlState extends State { semanticChildCount: semanticChildCount, itemExtent: itemExtent, prototypeItem: prototypeItem, - children: controls, + children: controls + .map((item) => ControlWidget( + key: ValueKey(item.id), + control: item, + )) + .toList(), ) : spacing > 0 ? ListView.separated( @@ -92,7 +98,10 @@ class _ListViewControlState extends State { padding: padding, itemCount: controls.length, itemBuilder: (context, index) { - return controls[index]; + return ControlWidget( + key: ValueKey(controls[index].id), + control: controls[index], + ); }, separatorBuilder: (context, index) { return horizontal @@ -118,7 +127,10 @@ class _ListViewControlState extends State { itemCount: controls.length, itemExtent: itemExtent, itemBuilder: (context, index) { - return controls[index]; + return ControlWidget( + key: ValueKey(controls[index].id), + control: controls[index], + ); }, prototypeItem: prototypeItem, ); diff --git a/packages/flet/lib/src/controls/reorderable_list_view.dart b/packages/flet/lib/src/controls/reorderable_list_view.dart index 24ac158c1d..48a9d3935f 100644 --- a/packages/flet/lib/src/controls/reorderable_list_view.dart +++ b/packages/flet/lib/src/controls/reorderable_list_view.dart @@ -62,14 +62,12 @@ class _ListViewControlState extends State { var anchor = widget.control.getDouble("anchor", 0.0)!; var clipBehavior = widget.control.getClipBehavior("clip_behavior", Clip.hardEdge)!; - var controls = _controls - .map((child) => ControlWidget(key: ValueKey(child.id), control: child)) - .toList(); var scrollDirection = horizontal ? Axis.horizontal : Axis.vertical; var header = widget.control.buildWidget("header"); var footer = widget.control.buildWidget("footer"); - var prototypeItem = - firstItemPrototype && controls.isNotEmpty ? controls[0] : null; + var prototypeItem = firstItemPrototype && _controls.isNotEmpty + ? ControlWidget(key: ValueKey(_controls[0].id), control: _controls[0]) + : null; var autoScrollerVelocityScalar = widget.control.getDouble("auto_scroller_velocity_scalar"); var mouseCursor = widget.control.getMouseCursor("mouse_cursor"); @@ -110,7 +108,7 @@ class _ListViewControlState extends State { scrollDirection: scrollDirection, shrinkWrap: shrinkWrap, padding: padding, - itemCount: controls.length, + itemCount: _controls.length, itemExtent: itemExtent, mouseCursor: mouseCursor, anchor: anchor, @@ -122,7 +120,10 @@ class _ListViewControlState extends State { onReorderEnd: onReorderEnd, onReorderStart: onReorderStart, itemBuilder: (context, index) { - return controls[index]; + return ControlWidget( + key: ValueKey(_controls[index].id), + control: _controls[index], + ); }, ) : ReorderableListView( @@ -144,7 +145,12 @@ class _ListViewControlState extends State { onReorder: onReorder, onReorderEnd: onReorderEnd, onReorderStart: onReorderStart, - children: controls, + children: _controls + .map((item) => ControlWidget( + key: ValueKey(item.id), + control: item, + )) + .toList(), ); child = ScrollableControl( From fd46c2e792c26d84d509476ae2b1a6d310247096 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 9 Sep 2025 13:44:02 -0700 Subject: [PATCH 2/3] 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 --- packages/flet/lib/src/controls/checkbox.dart | 12 ++++++----- .../lib/src/controls/cupertino_checkbox.dart | 12 ++++++----- .../lib/src/controls/cupertino_radio.dart | 20 ++++++++++++++----- .../lib/src/controls/cupertino_switch.dart | 10 ++++++---- packages/flet/lib/src/controls/radio.dart | 19 ++++++++++++++---- packages/flet/lib/src/controls/switch.dart | 12 ++++++----- 6 files changed, 57 insertions(+), 28 deletions(-) diff --git a/packages/flet/lib/src/controls/checkbox.dart b/packages/flet/lib/src/controls/checkbox.dart index 12a9420688..71082a3cc4 100644 --- a/packages/flet/lib/src/controls/checkbox.dart +++ b/packages/flet/lib/src/controls/checkbox.dart @@ -34,6 +34,12 @@ class _CheckboxControlState extends State { _focusNode.addListener(_onFocusChange); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + ListTileClicks.of(context)?.notifier.addListener(_toggleValue); + } + void _onFocusChange() { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } @@ -41,6 +47,7 @@ class _CheckboxControlState extends State { @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleValue); _focusNode.dispose(); super.dispose(); } @@ -98,11 +105,6 @@ class _CheckboxControlState extends State { ? (bool? value) => _onChange(value) : null); - // Add listener to ListTile clicks - ListTileClicks.of(context)?.notifier.addListener(() { - _toggleValue(); - }); - Widget result = checkbox; var labelStyle = diff --git a/packages/flet/lib/src/controls/cupertino_checkbox.dart b/packages/flet/lib/src/controls/cupertino_checkbox.dart index 3589ddcebd..2b56470c9e 100644 --- a/packages/flet/lib/src/controls/cupertino_checkbox.dart +++ b/packages/flet/lib/src/controls/cupertino_checkbox.dart @@ -34,6 +34,12 @@ class _CheckboxControlState extends State { _focusNode.addListener(_onFocusChange); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + ListTileClicks.of(context)?.notifier.addListener(_toggleValue); + } + void _onFocusChange() { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } @@ -41,6 +47,7 @@ class _CheckboxControlState extends State { @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleValue); _focusNode.dispose(); super.dispose(); } @@ -92,11 +99,6 @@ class _CheckboxControlState extends State { ? (bool? value) => _onChange(value) : null); - // Add listener to ListTile clicks - ListTileClicks.of(context)?.notifier.addListener(() { - _toggleValue(); - }); - Widget result = cupertinoCheckbox; var labelStyle = diff --git a/packages/flet/lib/src/controls/cupertino_radio.dart b/packages/flet/lib/src/controls/cupertino_radio.dart index 0970fb8107..20d04d19c2 100644 --- a/packages/flet/lib/src/controls/cupertino_radio.dart +++ b/packages/flet/lib/src/controls/cupertino_radio.dart @@ -33,6 +33,12 @@ class _CupertinoRadioControlState extends State _focusNode.addListener(_onFocusChange); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + ListTileClicks.of(context)?.notifier.addListener(_toggleRadio); + } + void _onFocusChange() { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } @@ -40,10 +46,19 @@ class _CupertinoRadioControlState extends State @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleRadio); _focusNode.dispose(); super.dispose(); } + void _toggleRadio() { + var radioGroup = RadioGroupProvider.of(context); + if (radioGroup != null) { + String value = widget.control.getString("value", "")!; + _onChange(radioGroup, value); + } + } + void _onChange(Control radioGroup, String? value) { radioGroup.updateProperties({"value": value}, notify: true); radioGroup.triggerEvent("change", value); @@ -62,7 +77,6 @@ class _CupertinoRadioControlState extends State debugPrint("CupertinoRadio build: ${widget.control.id}"); var radioGroup = RadioGroupProvider.of(context); - if (radioGroup == null) { return const ErrorControl( "CupertinoRadio must be enclosed within RadioGroup"); @@ -88,10 +102,6 @@ class _CupertinoRadioControlState extends State ? (String? value) => _onChange(radioGroup, value) : null); - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(radioGroup, value); - }); - Widget result = cupertinoRadio; if (label != "") { var labelWidget = widget.control.disabled diff --git a/packages/flet/lib/src/controls/cupertino_switch.dart b/packages/flet/lib/src/controls/cupertino_switch.dart index ecb065b855..01572807af 100644 --- a/packages/flet/lib/src/controls/cupertino_switch.dart +++ b/packages/flet/lib/src/controls/cupertino_switch.dart @@ -29,15 +29,21 @@ class _CupertinoSwitchControlState extends State { super.initState(); _focusNode = FocusNode(); _focusNode.addListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.addListener(_toggleValue); } @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleValue); _focusNode.dispose(); super.dispose(); } + void _toggleValue() { + _onChange(!_value); + } + void _onChange(bool value) { _value = value; var props = {"value": value}; @@ -114,10 +120,6 @@ class _CupertinoSwitchControlState extends State { } : null); - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(!_value); - }); - Widget result = swtch; if (label != "") { var labelWidget = widget.control.disabled diff --git a/packages/flet/lib/src/controls/radio.dart b/packages/flet/lib/src/controls/radio.dart index 3c571fa566..21d14c861a 100644 --- a/packages/flet/lib/src/controls/radio.dart +++ b/packages/flet/lib/src/controls/radio.dart @@ -32,6 +32,12 @@ class _RadioControlState extends State { _focusNode.addListener(_onFocusChange); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + ListTileClicks.of(context)?.notifier.addListener(_toggleRadio); + } + void _onFocusChange() { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } @@ -39,10 +45,19 @@ class _RadioControlState extends State { @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleRadio); _focusNode.dispose(); super.dispose(); } + void _toggleRadio() { + var radioGroup = RadioGroupProvider.of(context); + if (radioGroup != null) { + String value = widget.control.getString("value", "")!; + _onChange(radioGroup, value); + } + } + void _onChange(Control radioGroup, String? value) { radioGroup.updateProperties({"value": value}, notify: true); radioGroup.triggerEvent("change", value); @@ -95,10 +110,6 @@ class _RadioControlState extends State { ? (String? value) => _onChange(radioGroup, value) : null); - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(radioGroup, value); - }); - Widget result = radio; if (label != "") { var labelWidget = widget.control.disabled diff --git a/packages/flet/lib/src/controls/switch.dart b/packages/flet/lib/src/controls/switch.dart index 2e29b5eedc..42d45abdd0 100644 --- a/packages/flet/lib/src/controls/switch.dart +++ b/packages/flet/lib/src/controls/switch.dart @@ -31,15 +31,21 @@ class _SwitchControlState extends State { super.initState(); _focusNode = FocusNode(); _focusNode.addListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.addListener(_toggleValue); } @override void dispose() { _focusNode.removeListener(_onFocusChange); + ListTileClicks.of(context)?.notifier.removeListener(_toggleValue); _focusNode.dispose(); super.dispose(); } + void _toggleValue() { + _onChange(!_value); + } + void _onChange(bool value) { _value = value; var props = {"value": value}; @@ -78,7 +84,7 @@ class _SwitchControlState extends State { autofocus: autofocus, padding: widget.control.getPadding("padding"), focusNode: _focusNode, - activeColor: widget.control.getColor("active_color", context), + activeThumbColor: widget.control.getColor("active_color", context), activeTrackColor: widget.control.getColor("active_track_color", context), inactiveThumbColor: @@ -105,10 +111,6 @@ class _SwitchControlState extends State { } : null); - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(!_value); - }); - Widget result = s; if (label is Control || (label is String)) { From 9df6e5f092e5a550313fae4b8f19b20d411d01af Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 9 Sep 2025 16:45:53 -0700 Subject: [PATCH 3/3] 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. --- packages/flet/lib/src/controls/grid_view.dart | 7 ++++--- packages/flet/lib/src/controls/list_view.dart | 11 +++++++---- .../flet/lib/src/controls/reorderable_list_view.dart | 6 ++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/flet/lib/src/controls/grid_view.dart b/packages/flet/lib/src/controls/grid_view.dart index 27ca8240fc..481f5732a3 100644 --- a/packages/flet/lib/src/controls/grid_view.dart +++ b/packages/flet/lib/src/controls/grid_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/widgets.dart'; import '../controls/control_widget.dart'; -import '../extensions/control.dart'; import '../models/control.dart'; import '../utils/edge_insets.dart'; +import '../utils/keys.dart'; import '../utils/layout.dart'; import '../utils/misc.dart'; import '../utils/numbers.dart'; @@ -92,7 +92,7 @@ class _GridViewControlState extends State { gridDelegate: gridDelegate, children: controls .map((item) => ControlWidget( - key: ValueKey(item.id), + key: ValueKey(item.getKey("key")?.value ?? item.id), control: item, )) .toList(), @@ -110,7 +110,8 @@ class _GridViewControlState extends State { itemCount: controls.length, itemBuilder: (context, index) { return ControlWidget( - key: ValueKey(controls[index].id), + key: ValueKey(controls[index].getKey("key")?.value ?? + controls[index].id), control: controls[index], ); }, diff --git a/packages/flet/lib/src/controls/list_view.dart b/packages/flet/lib/src/controls/list_view.dart index d28e3e12c2..b3dabf12ae 100644 --- a/packages/flet/lib/src/controls/list_view.dart +++ b/packages/flet/lib/src/controls/list_view.dart @@ -1,9 +1,10 @@ -import 'package:flet/src/utils/layout.dart'; import 'package:flutter/material.dart'; import '../extensions/control.dart'; import '../models/control.dart'; import '../utils/edge_insets.dart'; +import '../utils/keys.dart'; +import '../utils/layout.dart'; import '../utils/misc.dart'; import '../utils/numbers.dart'; import '../widgets/error.dart'; @@ -82,7 +83,7 @@ class _ListViewControlState extends State { prototypeItem: prototypeItem, children: controls .map((item) => ControlWidget( - key: ValueKey(item.id), + key: ValueKey(item.getKey("key")?.value ?? item.id), control: item, )) .toList(), @@ -99,7 +100,8 @@ class _ListViewControlState extends State { itemCount: controls.length, itemBuilder: (context, index) { return ControlWidget( - key: ValueKey(controls[index].id), + key: ValueKey(controls[index].getKey("key")?.value ?? + controls[index].id), control: controls[index], ); }, @@ -128,7 +130,8 @@ class _ListViewControlState extends State { itemExtent: itemExtent, itemBuilder: (context, index) { return ControlWidget( - key: ValueKey(controls[index].id), + key: ValueKey(controls[index].getKey("key")?.value ?? + controls[index].id), control: controls[index], ); }, diff --git a/packages/flet/lib/src/controls/reorderable_list_view.dart b/packages/flet/lib/src/controls/reorderable_list_view.dart index 48a9d3935f..596b7fe25b 100644 --- a/packages/flet/lib/src/controls/reorderable_list_view.dart +++ b/packages/flet/lib/src/controls/reorderable_list_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../extensions/control.dart'; import '../models/control.dart'; import '../utils/edge_insets.dart'; +import '../utils/keys.dart'; import '../utils/misc.dart'; import '../utils/mouse.dart'; import '../utils/numbers.dart'; @@ -121,7 +122,8 @@ class _ListViewControlState extends State { onReorderStart: onReorderStart, itemBuilder: (context, index) { return ControlWidget( - key: ValueKey(_controls[index].id), + key: ValueKey(_controls[index].getKey("key")?.value ?? + _controls[index].id), control: _controls[index], ); }, @@ -147,7 +149,7 @@ class _ListViewControlState extends State { onReorderStart: onReorderStart, children: _controls .map((item) => ControlWidget( - key: ValueKey(item.id), + key: ValueKey(item.getKey("key")?.value ?? item.id), control: item, )) .toList(),