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
18 changes: 15 additions & 3 deletions lib/features/connections/sdui_tree_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {
final Set<String> _loaded = {};
final Set<String> _expanded = {};
final Map<String, String> _expandErrors = {};
List<_VisibleRow>? _cachedVisibleRows;

@material.visibleForTesting
List<Object?>? get debugVisibleRows => _cachedVisibleRows;

static const double _rowExtent = 28;

Expand All @@ -83,13 +87,15 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {
_loaded.clear();
_expanded.clear();
_expandErrors.clear();
_cachedVisibleRows = null;
}
}

Future<void> _onExpand(SduiTreeNode node) async {
setState(() {
_expanded.add(node.id);
_expandErrors.remove(node.id);
_cachedVisibleRows = null;
});
if (!node.expandable || _loaded.contains(node.id) || node.hasChildren) {
return;
Expand All @@ -112,18 +118,23 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {
if (children.isEmpty) {
_expandErrors[node.id] = 'No child objects found.';
}
_cachedVisibleRows = null;
});
} catch (e) {
if (!mounted) return;
setState(() {
_loading.remove(node.id);
_expandErrors[node.id] = e.toString();
_cachedVisibleRows = null;
});
}
}

void _onCollapse(SduiTreeNode node) {
setState(() => _expanded.remove(node.id));
setState(() {
_expanded.remove(node.id);
_cachedVisibleRows = null;
});
}

void _toggleExpand(SduiTreeNode node) {
Expand All @@ -143,6 +154,7 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {
node.id,
(n) => n.copyWith(children: const []),
);
_cachedVisibleRows = null;
});
await _onExpand(node);
}
Expand Down Expand Up @@ -185,13 +197,13 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {

@override
material.Widget build(material.BuildContext context) {
final rows = _flattenVisible();
final rows = _cachedVisibleRows ??= _flattenVisible();
final list = material.ListView.builder(
shrinkWrap: widget.maxHeight == null,
physics: widget.maxHeight == null
? const material.NeverScrollableScrollPhysics()
: const material.ClampingScrollPhysics(),
itemExtent: rows.any((r) => r.isError) ? null : _rowExtent,
itemExtent: _expandErrors.isNotEmpty ? null : _rowExtent,
itemCount: rows.length,
itemBuilder: (context, index) {
final row = rows[index];
Expand Down
62 changes: 62 additions & 0 deletions test/core/sdui/sdui_builders_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -518,5 +518,67 @@ void main() {
final text = tester.widget<material.Text>(find.text('users'));
expect(text.style?.fontWeight, isNot(material.FontWeight.w600));
});

testWidgets(
'caches visible rows and preserves itemExtent without scanning list on selection changes',
(tester) async {
final schema = SduiTreeSchema.fromJson(const {
'roots': [
{
'id': 'folder.tables',
'label': 'Tables',
'expandable': true,
'children': [
{
'id': 'table.users',
'label': 'users',
'node_type': 'table',
},
],
},
],
});

var selected = 'table.users';
late material.StateSetter parentSetState;

await tester.pumpWidget(
queryaThemeTestShell(
child: material.StatefulBuilder(
builder: (context, setState) {
parentSetState = setState;
return material.Scaffold(
body: SduiTreeBuilder(
schema: schema,
selectedNodeId: selected,
),
);
},
),
),
);
await tester.pumpAndSettle();

final state = tester.state<SduiTreeBuilderState>(
find.byType(SduiTreeBuilder),
);
final initialRows = state.debugVisibleRows;
expect(initialRows, isNotNull);
expect(initialRows!.length, 1);

// Verify itemExtent is set to 28 when there are no errors
final listView = tester.widget<material.ListView>(
find.byType(material.ListView),
);
expect(listView.itemExtent, 28);

// Change selection in parent: rebuilds SduiTreeBuilder without changing roots/expansion
parentSetState(() => selected = 'other');
await tester.pump();

// Cached rows must be reused by identity
expect(state.debugVisibleRows, same(initialRows));
});
});
}

Loading