diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e887c..6cdac41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 7.5.24 + +- 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). +- 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. +- 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. + ## 7.5.23 - Fixed `ThemedTabView` debugPrint statement left in production code. diff --git a/lib/src/table2/src/table.dart b/lib/src/table2/src/table.dart index b125763..036f60f 100644 --- a/lib/src/table2/src/table.dart +++ b/lib/src/table2/src/table.dart @@ -217,15 +217,9 @@ class _ThemedTable2State extends State> { @override void didUpdateWidget(covariant ThemedTable2 oldWidget) { - // Use a fast heuristic instead of O(n) DeepCollectionEquality for large lists. - // Checks referential identity first, then length, then the first element. - final bool c1 = - !identical(oldWidget.items, widget.items) && - (oldWidget.items.length != widget.items.length || - (widget.items.isNotEmpty && !identical(oldWidget.items.first, widget.items.first))); - final bool c2 = - oldWidget.columns.length != widget.columns.length || - (widget.columns.isNotEmpty && oldWidget.columns.first != widget.columns.first); + final eq = const DeepCollectionEquality().equals; + final bool c1 = !eq(oldWidget.items, widget.items); + final bool c2 = !eq(oldWidget.columns, widget.columns); final bool c3 = oldWidget.actionsCount != widget.actionsCount; final bool c4 = oldWidget.canSearch != widget.canSearch; bool c5 = false; @@ -237,8 +231,6 @@ class _ThemedTable2State extends State> { Future _filterAndSort(String source) async { if (_isLoading.value) { - // Queue the update instead of silently dropping it. - // _filterAndSort will re-run once the current operation finishes. _pendingUpdate = true; return; } diff --git a/pubspec.yaml b/pubspec.yaml index c508888..2b45bc4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: layrz_theme description: Layrz standard styling library for Flutter. Widget library following the Material Design 3 guidelines, with a focus on reliavility and functionality. -version: "7.5.23" +version: "7.5.24" homepage: https://theme.layrz.com repository: https://github.com/goldenm-software/layrz_theme diff --git a/test/widgets/table2_test.dart b/test/widgets/table2_test.dart index ef672a0..685779a 100644 --- a/test/widgets/table2_test.dart +++ b/test/widgets/table2_test.dart @@ -19,6 +19,24 @@ class _Item { int get hashCode => id.hashCode; } +// Full value-equality item — simulates Freezed objects (all fields compared). +// Used in the DeepCollectionEquality regression test. +class _ItemFull { + final String id; + final String name; + final String secondary; + + const _ItemFull({required this.id, required this.name, required this.secondary}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ItemFull && id == other.id && name == other.name && secondary == other.secondary; + + @override + int get hashCode => Object.hash(id, name, secondary); +} + // Waits for a _filterAndSort cycle to complete. // // compute() spawns a REAL isolate even in widget tests. pumpAndSettle() cannot @@ -463,6 +481,72 @@ void main() { expect(find.text('First'), findsOneWidget); expect(find.text('Second'), findsOneWidget); }); + + testWidgets( + 'detects edit in middle of same-length list (DeepCollectionEquality regression)', + (tester) async { + // BUG DE PRODUCCIÓN: con la heurística O(1) anterior, si la lista tenía + // el mismo largo y el mismo primer elemento pero un elemento del medio + // cambiaba, c1=false y la tabla NUNCA se actualizaba. + // _ItemFull tiene igualdad por valor completa (simula Freezed). + // DeepCollectionEquality detecta el cambio de 'name' aunque el 'id' sea igual. + List<_ItemFull> items = [ + const _ItemFull(id: '1', name: 'First', secondary: ''), + const _ItemFull(id: '2', name: 'OriginalMiddle', secondary: ''), + const _ItemFull(id: '3', name: 'Last', secondary: ''), + ]; + late StateSetter rebuildState; + + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState) { + rebuildState = setState; + return MaterialApp( + home: Scaffold( + body: SizedBox( + height: 600, + width: 800, + child: ThemedTable2<_ItemFull>( + items: items, + actionsCount: 0, + hasMultiselect: false, + canSearch: false, + populateDelay: Duration.zero, + columns: [ + ThemedColumn2<_ItemFull>( + headerText: 'Name', + valueBuilder: (item) => item.name, + ), + ], + ), + ), + ), + ); + }, + ), + ); + await tester.pump(); + await _waitForCompute(tester); + + expect(find.text('OriginalMiddle'), findsOneWidget); + + // Mismo largo (3), mismo primer elemento ('First'), pero el del medio cambió. + rebuildState(() { + items = [ + const _ItemFull(id: '1', name: 'First', secondary: ''), + const _ItemFull(id: '2', name: 'EditedMiddle', secondary: ''), + const _ItemFull(id: '3', name: 'Last', secondary: ''), + ]; + }); + await tester.pump(); + await _waitForCompute(tester); + + // Con la heurística rota: OriginalMiddle seguía visible (bug). + // Con DeepCollectionEquality: EditedMiddle debe aparecer. + expect(find.text('OriginalMiddle'), findsNothing); + expect(find.text('EditedMiddle'), findsOneWidget); + }, + ); }); }); }