Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
14 changes: 3 additions & 11 deletions lib/src/table2/src/table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,9 @@ class _ThemedTable2State<T> extends State<ThemedTable2<T>> {

@override
void didUpdateWidget(covariant ThemedTable2<T> 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;
Expand All @@ -237,8 +231,6 @@ class _ThemedTable2State<T> extends State<ThemedTable2<T>> {

Future<void> _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;
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
84 changes: 84 additions & 0 deletions test/widgets/table2_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
},
);
});
});
}
Loading