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
16 changes: 16 additions & 0 deletions lib/features/workspace/result_grid_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
/// Last widget built per visual row, reused when a rebuild would produce the
/// same row (see [_DataRow.sameAs]).
final Map<int, _DataRow> _rowWidgets = {};

@material.visibleForTesting
int get rowWidgetsCacheCount => _rowWidgets.length;

List<List<String>> _sortedRows = const [];
List<int> _sortedToModelIndices = const [];

Expand Down Expand Up @@ -806,6 +810,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
(i) => rows[order[i]],
growable: false,
);
_rowWidgets.removeWhere((idx, _) => idx >= _sortedRows.length);
}

@override
Expand All @@ -820,6 +825,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
if (oldWidget.stagingBuffer != widget.stagingBuffer) {
oldWidget.stagingBuffer?.removeListener(_onStagingBufferChanged);
widget.stagingBuffer?.addListener(_onStagingBufferChanged);
_rowWidgets.clear();
}
if (oldWidget.columns != widget.columns ||
oldWidget.rows != widget.rows ||
Expand All @@ -834,6 +840,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
_selectionFocus = null;
_selection = null;
_editingCell = null;
_rowWidgets.clear();
widget.onRowSelected?.call(null);
}
// With a staging buffer and no filter mapping the displayed rows come from
Expand All @@ -853,6 +860,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
@override
void dispose() {
_stopAutoScroll();
_rowWidgets.clear();
widget.stagingBuffer?.removeListener(_onStagingBufferChanged);
_horizontalController.removeListener(_onHorizontalScroll);
_horizontalController.dispose();
Expand Down Expand Up @@ -1122,6 +1130,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
}

void _updateSortedRows({bool asyncIfLarge = true}) {
_rowWidgets.clear();
final rows = _baseRows;
if (_sortColumnIndex == null || _sortOrder == null) {
_sortedRows = rows;
Expand Down Expand Up @@ -1172,6 +1181,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {

void _applySortedData(
SortedResultGridData sortedData, List<List<String>> rows) {
_rowWidgets.clear();
_sortedRows = sortedData.rows;
if (widget.rowIndicesMapping != null) {
final mapping = widget.rowIndicesMapping!;
Expand Down Expand Up @@ -2285,6 +2295,12 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
return previous;
}
_rowWidgets[rowIndex] = candidate;
if (_rowWidgets.length > 200) {
_rowWidgets.removeWhere(
(idx, _) =>
(idx - rowIndex).abs() > 100,
);
}
return candidate;
},
),
Expand Down
144 changes: 144 additions & 0 deletions test/features/workspace/result_grid_row_cache_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart';
import 'package:querya_desktop/features/workspace/result_grid_view.dart';

import '../../support/querya_theme_test_shell.dart';

material.Widget _testShell({required material.Widget child}) {
return queryaThemeTestShell(
child: material.Scaffold(
body: child,
),
);
}

void main() {
group('VirtualResultGrid row memo cache eviction (#1010)', () {
testWidgets(
'caches built rows and clears cache when stagingBuffer is swapped',
(tester) async {
final buffer1 = DataGridStagingBuffer(
columns: const ['id', 'name'],
rows: List.generate(20, (i) => ['$i', 'Item $i']),
);
final buffer2 = DataGridStagingBuffer(
columns: const ['id', 'name'],
rows: List.generate(10, (i) => ['new_$i', 'New $i']),
);

await tester.pumpWidget(
_testShell(
child: material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: const ['id', 'name'],
rows: buffer1.effectiveRows,
stagingBuffer: buffer1,
),
),
),
);
await tester.pumpAndSettle();

final dynamic state = tester.state(find.byType(VirtualResultGrid));
expect(state.rowWidgetsCacheCount, greaterThan(0));

// Swap to buffer2
await tester.pumpWidget(
_testShell(
child: material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: const ['id', 'name'],
rows: buffer2.effectiveRows,
stagingBuffer: buffer2,
),
),
),
);
await tester.pumpAndSettle();

expect(state.rowWidgetsCacheCount, greaterThan(0));
expect(state.rowWidgetsCacheCount, lessThanOrEqualTo(10));
});

testWidgets('clears cache when columns change', (tester) async {
await tester.pumpWidget(
_testShell(
child: const material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: ['id', 'name'],
rows: [
['1', 'Alpha'],
['2', 'Beta'],
],
),
),
),
);
await tester.pumpAndSettle();

final dynamic state = tester.state(find.byType(VirtualResultGrid));
expect(state.rowWidgetsCacheCount, 2);

await tester.pumpWidget(
_testShell(
child: const material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: ['col_a', 'col_b', 'col_c'],
rows: [
['1', 'Alpha', 'Extra'],
['2', 'Beta', 'Extra'],
],
),
),
),
);
await tester.pumpAndSettle();

expect(state.rowWidgetsCacheCount, 2);
});

testWidgets(
'bounds cache size and evicts far rows during heavy vertical scroll',
(tester) async {
final rows = List.generate(350, (i) => ['$i', 'Value $i']);
await tester.pumpWidget(
_testShell(
child: material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: const ['id', 'value'],
rows: rows,
),
),
),
);
await tester.pumpAndSettle();

final dynamic state = tester.state(find.byType(VirtualResultGrid));
expect(state.rowWidgetsCacheCount, lessThan(100));

// Scroll down across hundreds of rows
for (int i = 0; i < 15; i++) {
await tester.drag(
find.byType(VirtualResultGrid),
const material.Offset(0, -600),
);
await tester.pump();
}
await tester.pumpAndSettle();

// Even after scrolling through 350 rows, the cache must remain bounded (<= 200)
expect(state.rowWidgetsCacheCount, lessThanOrEqualTo(200));
});
});
}
Loading