Skip to content

Commit 42dcc71

Browse files
Handle non-event on_ fields in diff and protocol logic (#5742)
* Handle non-event 'on_' fields in diff and protocol logic Updates diffing and protocol encoding logic to distinguish 'on_' fields that are not events using metadata. Adds integration tests for color scheme, new test cases for 'on_' fields, and updates ColorScheme to mark non-event 'on_' fields. This improves accuracy of change detection and serialization for controls with 'on_' prefixed fields. * Refine event attribute detection in Jinja templates Updated logic in material Jinja templates to more accurately classify event attributes by checking config.extra.events. Also updated colorscheme.md to pass additional options for documentation rendering. * Refactor event field handling and update color scheme test Simplifies checks for 'event' metadata in object_patch.py and protocol.py by using get() with a default value. Updates color scheme integration test to use a more comprehensive set of ColorScheme properties and renames screenshot references for clarity. * Improve scrollable controls and spacing in views Added wrapIntoScrollableView option to ScrollableControl and enabled it for Column, Row, and View controls to ensure proper scroll behavior. Enhanced ListViewControl to handle spacing and dividers between children more flexibly. Refined Scrollbar visibility logic and removed unnecessary exception in ViewControl. * Revamp color scheme integration test and screenshots Expanded the color scheme integration test to cover palettes, surface roles, accents, buttons, themed card, and error banner. Replaced the single color_scheme.png screenshot with multiple targeted screenshots and updated the test logic and golden images accordingly. * Rename dismissable files and add declarative example Renamed 'dismissable_list_tiles.py' and its media file to 'dismissible_list_tiles.py' for consistency. Added a new example 'remove_on_dismiss_declarative.py' demonstrating proper use of keys with Dismissible in declarative components. Updated documentation to reflect these changes and provide guidance on key usage. * Improve ListView prototype item handling Enhanced logic for determining the prototype item in ListViewControl (Dart) to use either a provided prototype or the first control when appropriate. Updated Python docstrings to clarify when prototype_item and first_item_prototype properties take effect. * Update integration test suite and doc formatting Limits macOS integration tests to the 'controls/theme' suite for focused testing. Improves docstring formatting in ListView by splitting 'Note:' onto its own line for better readability. * Refactor color scheme test UI and update goldens Reorganized color palette and button rows in test_color_scheme.py, added ScrollKey to palette rows, removed redundant spacing and text labels, and adjusted error banner border radius. Updated corresponding golden images to reflect UI changes for macOS color scheme integration tests. * Update macOS color scheme golden images and test Regenerated golden images for macOS color scheme integration tests and adjusted test window height from 600 to 300 to match new screenshot dimensions. * Refactor color scheme test to use Screenshot widgets Updated the color scheme integration test to wrap palette, button, card, and banner controls in Screenshot widgets for more precise screenshot capturing. Adjusted window height and consolidated button rows. Updated screenshot assertions to use the new capture method for each control. * Enable all test suites in macOS integration workflow Restores the full test suite matrix in the macOS integration tests workflow, running tests for apps, examples, and all controls. Previously, only the 'controls/theme' suite was enabled. * Fix doc filter regex and marker doc typo Corrected a typo in the Marker class docstring in marker_layer.py and updated the filter regex in mkdocs.yml to properly match method names for documentation exclusion.
1 parent cb71135 commit 42dcc71

File tree

30 files changed

+671
-60
lines changed

30 files changed

+671
-60
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class ColumnControl extends StatelessWidget {
5757
child = ScrollableControl(
5858
control: control,
5959
scrollDirection: wrap ? Axis.horizontal : Axis.vertical,
60+
wrapIntoScrollableView: true,
6061
child: child,
6162
);
6263

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

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ class _ListViewControlState extends State<ListViewControl> {
5656
widget.control.getBool("build_controls_on_demand", true)!;
5757
var firstItemPrototype =
5858
widget.control.getBool("first_item_prototype", false)!;
59-
var prototypeItem = firstItemPrototype
60-
? widget.control.buildWidget("prototype_item")
61-
: null;
6259
var controls = widget.control.children("controls");
60+
var prototypeItem = widget.control.buildWidget("prototype_item") ??
61+
(firstItemPrototype && controls.isNotEmpty
62+
? ControlWidget(control: controls.first)
63+
: null);
6364

6465
Widget listView = LayoutBuilder(
6566
builder: (BuildContext context, BoxConstraints constraints) {
@@ -79,14 +80,31 @@ class _ListViewControlState extends State<ListViewControl> {
7980
shrinkWrap: shrinkWrap,
8081
padding: padding,
8182
semanticChildCount: semanticChildCount,
82-
itemExtent: itemExtent,
83-
prototypeItem: prototypeItem,
84-
children: controls
85-
.map((item) => ControlWidget(
86-
key: ValueKey(item.getKey("key")?.value ?? item.id),
87-
control: item,
88-
))
89-
.toList(),
83+
itemExtent: spacing > 0 ? null : itemExtent,
84+
prototypeItem: spacing > 0 ? null : prototypeItem,
85+
children: () {
86+
final childWidgets = <Widget>[];
87+
for (var index = 0; index < controls.length; index++) {
88+
final item = controls[index];
89+
childWidgets.add(ControlWidget(
90+
key: ValueKey(item.getKey("key")?.value ?? item.id),
91+
control: item,
92+
));
93+
if (spacing > 0 && index < controls.length - 1) {
94+
childWidgets.add(horizontal
95+
? dividerThickness == 0
96+
? SizedBox(width: spacing)
97+
: VerticalDivider(
98+
width: spacing, thickness: dividerThickness)
99+
: dividerThickness == 0
100+
? SizedBox(height: spacing)
101+
: Divider(
102+
height: spacing,
103+
thickness: dividerThickness));
104+
}
105+
}
106+
return childWidgets;
107+
}(),
90108
)
91109
: spacing > 0
92110
? ListView.separated(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class RowControl extends StatelessWidget {
5555
child = ScrollableControl(
5656
control: control,
5757
scrollDirection: wrap ? Axis.vertical : Axis.horizontal,
58+
wrapIntoScrollableView: true,
5859
child: child);
5960

6061
if (control.getBool("on_scroll", false)!) {

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ class ScrollableControl extends StatefulWidget {
88
final Widget child;
99
final Axis scrollDirection;
1010
final ScrollController? scrollController;
11+
final bool wrapIntoScrollableView;
1112

1213
ScrollableControl(
1314
{Key? key,
1415
required this.control,
1516
required this.child,
1617
required this.scrollDirection,
17-
this.scrollController})
18+
this.scrollController,
19+
this.wrapIntoScrollableView = false})
1820
: super(key: key ?? ValueKey("control_${control.id}"));
1921

2022
@override
@@ -102,22 +104,26 @@ class _ScrollableControlState extends State<ScrollableControl>
102104
return scrollMode != ScrollMode.none
103105
? Scrollbar(
104106
// todo: create class ScrollBarConfiguration on Py end, for more customizability
105-
thumbVisibility: scrollMode == ScrollMode.always ||
106-
(scrollMode == ScrollMode.adaptive && !isMobilePlatform())
107-
? true
108-
: false,
109-
trackVisibility: scrollMode == ScrollMode.hidden ? false : null,
107+
thumbVisibility: (scrollMode == ScrollMode.always ||
108+
(scrollMode == ScrollMode.adaptive &&
109+
!isMobilePlatform())) &&
110+
scrollMode != ScrollMode.hidden,
110111
thickness: scrollMode == ScrollMode.hidden
111112
? 0
112113
: isMobilePlatform()
113114
? 4.0
114115
: null,
115-
//interactive: true,
116116
controller: _controller,
117-
child: SingleChildScrollView(
118-
controller: _controller,
119-
scrollDirection: widget.scrollDirection,
120-
child: widget.child,
117+
child: ScrollConfiguration(
118+
behavior:
119+
ScrollConfiguration.of(context).copyWith(scrollbars: false),
120+
child: widget.wrapIntoScrollableView
121+
? SingleChildScrollView(
122+
controller: _controller,
123+
scrollDirection: widget.scrollDirection,
124+
child: widget.child,
125+
)
126+
: widget.child,
121127
))
122128
: widget.child;
123129
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ class _ViewControlState extends State<ViewControl> {
7676
if (_popCompleter != null && !_popCompleter!.isCompleted) {
7777
_popCompleter?.complete(args["should_pop"]);
7878
}
79-
default:
80-
throw Exception("Unknown View method: $name");
8179
}
8280
}
8381

@@ -130,7 +128,11 @@ class _ViewControlState extends State<ViewControl> {
130128
.toList());
131129

132130
Widget child = ScrollableControl(
133-
control: control, scrollDirection: Axis.vertical, child: column);
131+
control: control,
132+
scrollDirection: Axis.vertical,
133+
wrapIntoScrollableView: true,
134+
child: column,
135+
);
134136

135137
if (control.getBool("on_scroll", false)!) {
136138
child = ScrollNotificationControl(control: control, child: child);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import flet as ft
2+
3+
4+
@ft.component
5+
def App():
6+
items, set_items = ft.use_state(list(range(5)))
7+
8+
return ft.ListView(
9+
controls=[
10+
ft.Dismissible(
11+
key=i,
12+
content=ft.ListTile(title=ft.Text(f"Item {i}")),
13+
dismiss_direction=ft.DismissDirection.HORIZONTAL,
14+
background=ft.Container(bgcolor=ft.Colors.GREEN),
15+
secondary_background=ft.Container(bgcolor=ft.Colors.RED),
16+
on_dismiss=lambda e, index=i: set_items(
17+
[item for item in items if item != index]
18+
),
19+
dismiss_thresholds={
20+
ft.DismissDirection.HORIZONTAL: 0.1,
21+
ft.DismissDirection.START_TO_END: 0.1,
22+
},
23+
)
24+
for i in items
25+
],
26+
)
27+
28+
29+
ft.run(lambda page: page.render(App))

sdk/python/packages/flet/docs/controls/dismissible.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,39 @@ example_images: ../examples/controls/dismissible/media
1010

1111
[Live example](https://flet-controls-gallery.fly.dev/layout/dismissible)
1212

13-
### Dismissable `ListTile`s
13+
### Dismissible `ListTile`s
1414

1515
```python
16-
--8<-- "{{ examples }}/dismissable_list_tiles.py"
16+
--8<-- "{{ examples }}/dismissible_list_tiles.py"
1717
```
1818

19-
{{ image(example_images + "/dismissable_list_tiles.gif", alt="dismissable-list-tiles", width="80%") }}
19+
{{ image(example_images + "/dismissible_list_tiles.gif", alt="dismissible-list-tiles", width="80%") }}
2020

21+
### Remove Dismissible `on_dismiss` inside component
22+
23+
/// admonition | Important
24+
type: warning
25+
Always specify a key for `Dismissible` when using inside Flet component.
26+
///
27+
28+
The issue you may encounter here is specific to the `Dismissible` control used inside Flet component (declarative UI).
29+
30+
When a user swipes (dismisses) an item, that widget is marked as “dismissed” on the Flutter side and effectively removed from the UI.
31+
However, when Flet recalculates the UI diff on the Python side, it may attempt to reuse widgets in the list based on their order rather than their identity.
32+
33+
If no key is provided, Flet’s diffing algorithm can’t tell that a particular `Dismissible` corresponds to a specific item — so it assumes the items have merely shifted.
34+
That leads to update commands like:
35+
36+
> “Update text in items 0…N-1, then delete the last item (N).”
37+
38+
On Flutter’s side, though, the already-dismissed `Dismissible` widget in the middle of the list can’t be updated — it’s gone — causing runtime errors.
39+
40+
**Always assign a stable, unique key to each `Dismissible`, typically based on the item’s identifier or index.**
41+
42+
Example:
43+
44+
```python
45+
--8<-- "{{ examples }}/remove_on_dismiss_declarative.py"
46+
```
2147

2248
{{ class_members(class_name) }}

sdk/python/packages/flet/docs/templates/python_xref/material/attribute.html.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Context:
1818
{{ log.debug("Rendering " + attribute.path) }}
1919
{% endblock logs %}
2020

21-
{% set attr_class_name = "doc-symbol-event" if attribute.name.startswith('on_') else "doc-symbol-attribute" %}
21+
{% set attr_class_name = "doc-symbol-event" if attribute.name.startswith('on_') and (config.extra.events is not defined or (config.extra.events and attribute.name in config.extra.events)) else "doc-symbol-attribute" %}
2222

2323
<div class="doc doc-object doc-attribute">
2424
{% with obj = attribute, html_id = attribute.path %}

0 commit comments

Comments
 (0)