From 4e9df2a3050a7c009999343aba4cc8087360f944 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 00:28:36 +0300 Subject: [PATCH] perf(mongodb): replace unvirtualized shrinkWrap ListView in _FieldList with Column or virtualized sliver --- .../mongodb/mongo_documents_view.dart | 161 +++++++++++------- .../mongodb/mongo_field_list_perf_test.dart | 91 ++++++++++ 2 files changed, 186 insertions(+), 66 deletions(-) create mode 100644 test/features/mongodb/mongo_field_list_perf_test.dart diff --git a/lib/features/mongodb/mongo_documents_view.dart b/lib/features/mongodb/mongo_documents_view.dart index 6b84db7..b15a66d 100644 --- a/lib/features/mongodb/mongo_documents_view.dart +++ b/lib/features/mongodb/mongo_documents_view.dart @@ -554,7 +554,7 @@ class _DocumentCardState extends State<_DocumentCard> { padding: const material.EdgeInsets.only( left: 16, right: 16, bottom: 10), child: _expanded - ? _FieldList( + ? MongoFieldList( document: widget.document, colorScheme: cs, shadcnCs: scs, @@ -584,8 +584,10 @@ class _DocumentCardState extends State<_DocumentCard> { } } -class _FieldList extends StatelessWidget { - const _FieldList({ +@material.visibleForTesting +class MongoFieldList extends StatelessWidget { + const MongoFieldList({ + super.key, required this.document, required this.colorScheme, required this.shadcnCs, @@ -597,77 +599,104 @@ class _FieldList extends StatelessWidget { final shadcn.ColorScheme shadcnCs; final ValueChanged onInspectField; + Widget _buildFieldRow(String key) { + final canEdit = key != '_id'; + final display = mongoFieldToDisplay(document[key]); + final oneLine = display.replaceAll('\n', ' '); + return material.InkWell( + onTap: canEdit ? () => onInspectField(key) : null, + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + child: Row( + children: [ + material.SizedBox( + width: 120, + child: Text( + key, + overflow: TextOverflow.ellipsis, + style: material.TextStyle( + fontSize: 12, + fontFamily: 'monospace', + fontWeight: material.FontWeight.w600, + color: colorScheme.foreground, + ), + ), + ), + const Gap(8), + material.Expanded( + child: Text( + oneLine, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: shadcnCs.mutedForeground, + ), + ), + ), + if (canEdit) + material.Icon( + material.Icons.edit_note_rounded, + size: 16, + color: shadcnCs.mutedForeground, + ), + ], + ), + ), + ); + } + + Widget _buildDivider() => material.Divider( + height: 1, + color: colorScheme.border.withValues(alpha: 0.2), + ); + @override Widget build(BuildContext context) { final fields = document.keys.toList(); + if (fields.isEmpty) return const material.SizedBox.shrink(); + + final decoration = material.BoxDecoration( + color: colorScheme.muted.withValues(alpha: 0.15), + borderRadius: material.BorderRadius.circular(6), + border: material.Border.all( + color: colorScheme.border.withValues(alpha: 0.3), + ), + ); + + // For documents with <= 10 fields, render a lightweight Column with zero + // scroll machinery or shrinkWrap measurement passes. + if (fields.length <= 10) { + final children = []; + for (var i = 0; i < fields.length; i++) { + if (i > 0) children.add(_buildDivider()); + children.add(_buildFieldRow(fields[i])); + } + return material.Container( + decoration: decoration, + padding: const material.EdgeInsets.symmetric(vertical: 4), + child: material.Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: children, + ), + ); + } + + // For wide documents (> 10 fields), render a bounded virtualized list + // without shrinkWrap to avoid layout recalculation spikes on scroll. return material.Container( constraints: const material.BoxConstraints(maxHeight: 320), - decoration: material.BoxDecoration( - color: colorScheme.muted.withValues(alpha: 0.15), - borderRadius: material.BorderRadius.circular(6), - border: material.Border.all( - color: colorScheme.border.withValues(alpha: 0.3), - ), - ), + decoration: decoration, child: ListView.separated( - shrinkWrap: true, padding: const material.EdgeInsets.symmetric(vertical: 4), itemCount: fields.length, - separatorBuilder: (_, __) => material.Divider( - height: 1, - color: colorScheme.border.withValues(alpha: 0.2), - ), - itemBuilder: (context, i) { - final key = fields[i]; - final canEdit = key != '_id'; - final display = mongoFieldToDisplay(document[key]); - final oneLine = display.replaceAll('\n', ' '); - return material.InkWell( - onTap: canEdit ? () => onInspectField(key) : null, - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - child: Row( - children: [ - material.SizedBox( - width: 120, - child: Text( - key, - overflow: TextOverflow.ellipsis, - style: material.TextStyle( - fontSize: 12, - fontFamily: 'monospace', - fontWeight: material.FontWeight.w600, - color: colorScheme.foreground, - ), - ), - ), - const Gap(8), - material.Expanded( - child: Text( - oneLine, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: shadcnCs.mutedForeground, - ), - ), - ), - if (canEdit) - material.Icon( - material.Icons.edit_note_rounded, - size: 16, - color: shadcnCs.mutedForeground, - ), - ], - ), - ), - ); - }, + separatorBuilder: (_, __) => _buildDivider(), + itemBuilder: (context, i) => _buildFieldRow(fields[i]), ), ); } diff --git a/test/features/mongodb/mongo_field_list_perf_test.dart b/test/features/mongodb/mongo_field_list_perf_test.dart new file mode 100644 index 0000000..498c16f --- /dev/null +++ b/test/features/mongodb/mongo_field_list_perf_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/features/mongodb/mongo_documents_view.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' as shadcn; + +import '../../support/querya_theme_test_shell.dart'; + +void main() { + group('MongoFieldList virtualization and layout optimization', () { + testWidgets('renders lightweight Column and no ListView when <= 10 fields', + (tester) async { + final doc = { + '_id': '64b1f2a3c9e77a1234567890', + 'title': 'Test Item', + 'count': 42, + 'active': true, + }; + + String? inspectedKey; + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Builder( + builder: (context) { + final scs = shadcn.Theme.of(context).colorScheme; + return material.Scaffold( + body: MongoFieldList( + document: doc, + colorScheme: scs, + shadcnCs: scs, + onInspectField: (k) => inspectedKey = k, + ), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + // Should render as Column to eliminate scrollable/shrinkWrap overhead + expect(find.byType(material.Column), findsOneWidget); + expect(find.byType(material.ListView), findsNothing); + + // Verify fields are present + expect(find.text('title'), findsOneWidget); + expect(find.text('Test Item'), findsOneWidget); + + // Inspecting field + await tester.tap(find.text('title')); + await tester.pumpAndSettle(); + expect(inspectedKey, 'title'); + }); + + testWidgets('renders virtualized ListView without shrinkWrap when > 10 fields', + (tester) async { + // 15 fields + final doc = { + '_id': '64b1f2a3c9e77a1234567890', + }; + for (var i = 1; i <= 14; i++) { + doc['field_$i'] = 'value_$i'; + } + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Builder( + builder: (context) { + final scs = shadcn.Theme.of(context).colorScheme; + return material.Scaffold( + body: MongoFieldList( + document: doc, + colorScheme: scs, + shadcnCs: scs, + onInspectField: (_) {}, + ), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + // Must render ListView without shrinkWrap + final listViewFinder = find.byType(material.ListView); + expect(listViewFinder, findsOneWidget); + + final listView = tester.widget(listViewFinder); + expect(listView.shrinkWrap, isFalse); + }); + }); +}