Skip to content

Commit 1158595

Browse files
feat(table2): fix mid-list edit detection and remove throttle (#130)
- fix: revert didUpdateWidget to DeepCollectionEquality — heuristic missed edits to non-first elements in same-length lists - fix: remove update throttle — blocked CRUDs up to 30s; root performance issue already solved by DeepCollectionEquality equality check - test: add regression test for mid-list edit detection - chore: bump version to 7.5.24
2 parents 8aa70fa + 02b0692 commit 1158595

4 files changed

Lines changed: 94 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 7.5.24
4+
5+
- Fixed `ThemedTable2` silent update loss for mid-list edits: reverted `didUpdateWidget` heuristic back to `DeepCollectionEquality` — the heuristic (identical + length + first element) missed edits to non-first elements in same-length lists; with Freezed value-equality objects the O(n) cost is negligible in practice (~4 of 170 telemetry updates actually triggered a reload in production profiling).
6+
- Removed `ThemedTable2` update throttle: the throttle introduced in 7.5.22 blocked CRUD operations for up to 30 seconds at trailing-edge; the root performance problem (constant reloads) was already solved by the `DeepCollectionEquality` equality check, making the throttle unnecessary complexity.
7+
- Added `ThemedTable2` regression test: `didUpdateWidget` now verifies that editing a middle element of a same-length list (same length, same first element) correctly triggers a table reload.
8+
39
## 7.5.23
410

511
- Fixed `ThemedTabView` debugPrint statement left in production code.

lib/src/table2/src/table.dart

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,9 @@ class _ThemedTable2State<T> extends State<ThemedTable2<T>> {
217217

218218
@override
219219
void didUpdateWidget(covariant ThemedTable2<T> oldWidget) {
220-
// Use a fast heuristic instead of O(n) DeepCollectionEquality for large lists.
221-
// Checks referential identity first, then length, then the first element.
222-
final bool c1 =
223-
!identical(oldWidget.items, widget.items) &&
224-
(oldWidget.items.length != widget.items.length ||
225-
(widget.items.isNotEmpty && !identical(oldWidget.items.first, widget.items.first)));
226-
final bool c2 =
227-
oldWidget.columns.length != widget.columns.length ||
228-
(widget.columns.isNotEmpty && oldWidget.columns.first != widget.columns.first);
220+
final eq = const DeepCollectionEquality().equals;
221+
final bool c1 = !eq(oldWidget.items, widget.items);
222+
final bool c2 = !eq(oldWidget.columns, widget.columns);
229223
final bool c3 = oldWidget.actionsCount != widget.actionsCount;
230224
final bool c4 = oldWidget.canSearch != widget.canSearch;
231225
bool c5 = false;
@@ -237,8 +231,6 @@ class _ThemedTable2State<T> extends State<ThemedTable2<T>> {
237231

238232
Future<void> _filterAndSort(String source) async {
239233
if (_isLoading.value) {
240-
// Queue the update instead of silently dropping it.
241-
// _filterAndSort will re-run once the current operation finishes.
242234
_pendingUpdate = true;
243235
return;
244236
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: layrz_theme
22
description: Layrz standard styling library for Flutter. Widget library following the Material Design 3 guidelines, with a focus on reliavility and functionality.
3-
version: "7.5.23"
3+
version: "7.5.24"
44
homepage: https://theme.layrz.com
55
repository: https://github.com/goldenm-software/layrz_theme
66

test/widgets/table2_test.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ class _Item {
1919
int get hashCode => id.hashCode;
2020
}
2121

22+
// Full value-equality item — simulates Freezed objects (all fields compared).
23+
// Used in the DeepCollectionEquality regression test.
24+
class _ItemFull {
25+
final String id;
26+
final String name;
27+
final String secondary;
28+
29+
const _ItemFull({required this.id, required this.name, required this.secondary});
30+
31+
@override
32+
bool operator ==(Object other) =>
33+
identical(this, other) ||
34+
other is _ItemFull && id == other.id && name == other.name && secondary == other.secondary;
35+
36+
@override
37+
int get hashCode => Object.hash(id, name, secondary);
38+
}
39+
2240
// Waits for a _filterAndSort cycle to complete.
2341
//
2442
// compute() spawns a REAL isolate even in widget tests. pumpAndSettle() cannot
@@ -463,6 +481,72 @@ void main() {
463481
expect(find.text('First'), findsOneWidget);
464482
expect(find.text('Second'), findsOneWidget);
465483
});
484+
485+
testWidgets(
486+
'detects edit in middle of same-length list (DeepCollectionEquality regression)',
487+
(tester) async {
488+
// BUG DE PRODUCCIÓN: con la heurística O(1) anterior, si la lista tenía
489+
// el mismo largo y el mismo primer elemento pero un elemento del medio
490+
// cambiaba, c1=false y la tabla NUNCA se actualizaba.
491+
// _ItemFull tiene igualdad por valor completa (simula Freezed).
492+
// DeepCollectionEquality detecta el cambio de 'name' aunque el 'id' sea igual.
493+
List<_ItemFull> items = [
494+
const _ItemFull(id: '1', name: 'First', secondary: ''),
495+
const _ItemFull(id: '2', name: 'OriginalMiddle', secondary: ''),
496+
const _ItemFull(id: '3', name: 'Last', secondary: ''),
497+
];
498+
late StateSetter rebuildState;
499+
500+
await tester.pumpWidget(
501+
StatefulBuilder(
502+
builder: (context, setState) {
503+
rebuildState = setState;
504+
return MaterialApp(
505+
home: Scaffold(
506+
body: SizedBox(
507+
height: 600,
508+
width: 800,
509+
child: ThemedTable2<_ItemFull>(
510+
items: items,
511+
actionsCount: 0,
512+
hasMultiselect: false,
513+
canSearch: false,
514+
populateDelay: Duration.zero,
515+
columns: [
516+
ThemedColumn2<_ItemFull>(
517+
headerText: 'Name',
518+
valueBuilder: (item) => item.name,
519+
),
520+
],
521+
),
522+
),
523+
),
524+
);
525+
},
526+
),
527+
);
528+
await tester.pump();
529+
await _waitForCompute(tester);
530+
531+
expect(find.text('OriginalMiddle'), findsOneWidget);
532+
533+
// Mismo largo (3), mismo primer elemento ('First'), pero el del medio cambió.
534+
rebuildState(() {
535+
items = [
536+
const _ItemFull(id: '1', name: 'First', secondary: ''),
537+
const _ItemFull(id: '2', name: 'EditedMiddle', secondary: ''),
538+
const _ItemFull(id: '3', name: 'Last', secondary: ''),
539+
];
540+
});
541+
await tester.pump();
542+
await _waitForCompute(tester);
543+
544+
// Con la heurística rota: OriginalMiddle seguía visible (bug).
545+
// Con DeepCollectionEquality: EditedMiddle debe aparecer.
546+
expect(find.text('OriginalMiddle'), findsNothing);
547+
expect(find.text('EditedMiddle'), findsOneWidget);
548+
},
549+
);
466550
});
467551
});
468552
}

0 commit comments

Comments
 (0)