Skip to content
Merged
5 changes: 4 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ To learn more about DevTools, check out the

## General updates

TODO: Remove this section if there are not any updates.
* Fixed a `RangeError` thrown by `SplitPane` when the parent rebuilt the
widget with a different number of children, for example when toggling a
panel in or out of the layout. -
[#9822](https://github.com/flutter/devtools/pull/9822)

## Inspector updates

Expand Down
4 changes: 4 additions & 0 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Copyright 2025 The Flutter Authors
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
-->
## 0.5.2-wip
* Fix a `RangeError` thrown by `SplitPane` when the number of children
changes between rebuilds.

## 0.5.1
* Add DevTools-styled text field `DevToolsTextField`.
* Updates `devtools_shared` constraint to `^13.0.0`.
Expand Down
14 changes: 13 additions & 1 deletion packages/devtools_app_shared/lib/src/ui/split_pane.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ final class SplitPane extends StatefulWidget {
}

final class _SplitPaneState extends State<SplitPane> {
late final List<double> fractions;
late List<double> fractions;

bool get isHorizontal => widget.axis == Axis.horizontal;

Expand All @@ -98,6 +98,18 @@ final class _SplitPaneState extends State<SplitPane> {
fractions = List.of(widget.initialFractions);
}

@override
void didUpdateWidget(SplitPane oldWidget) {
super.didUpdateWidget(oldWidget);
// When the number of children changes, the previously stored [fractions]
// list will be out of sync with [widget.minSizes] and [widget.children],
// which causes a RangeError during layout. Reset to the new
// [initialFractions] when the child count changes.
if (oldWidget.children.length != widget.children.length) {
fractions = List.of(widget.initialFractions);
}
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: _buildLayout);
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools_app_shared/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
name: devtools_app_shared
description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions.
version: 0.5.1
version: 0.5.2-wip
repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared

environment:
Expand Down
61 changes: 61 additions & 0 deletions packages/devtools_app_shared/test/ui/split_pane_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,67 @@ void main() {
);
});

group('rebuilds with a different number of children', () {
testWidgets(
'does not throw a RangeError when child count shrinks',
(WidgetTester tester) async {
final threeChildSplit = buildSplitPane(
Axis.horizontal,
children: const [_w1, _w2, _w3],
initialFractions: const [0.2, 0.4, 0.4],
minSizes: const [50.0, 50.0, 50.0],
);
await tester.pumpWidget(wrap(threeChildSplit));
expect(find.byKey(_k1), findsOneWidget);
expect(find.byKey(_k2), findsOneWidget);
expect(find.byKey(_k3), findsOneWidget);

final twoChildSplit = buildSplitPane(
Axis.horizontal,
children: const [_w1, _w2],
initialFractions: const [0.5, 0.5],
minSizes: const [50.0, 50.0],
);
await tester.pumpWidget(wrap(twoChildSplit));
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
expect(find.byKey(_k1), findsOneWidget);
expect(find.byKey(_k2), findsOneWidget);
expect(find.byKey(_k3), findsNothing);
},
);

testWidgets(
'does not throw a RangeError when child count grows',
(WidgetTester tester) async {
final twoChildSplit = buildSplitPane(
Axis.horizontal,
children: const [_w1, _w2],
initialFractions: const [0.5, 0.5],
minSizes: const [50.0, 50.0],
);
await tester.pumpWidget(wrap(twoChildSplit));
expect(find.byKey(_k1), findsOneWidget);
expect(find.byKey(_k2), findsOneWidget);

final threeChildSplit = buildSplitPane(
Axis.horizontal,
children: const [_w1, _w2, _w3],
initialFractions: const [0.2, 0.4, 0.4],
minSizes: const [50.0, 50.0, 50.0],
);
await tester.pumpWidget(wrap(threeChildSplit));
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
expect(find.byKey(_k1), findsOneWidget);
expect(find.byKey(_k2), findsOneWidget);
expect(find.byKey(_k3), findsOneWidget);
},
);
});

group('axisFor', () {
testWidgetsWithWindowSize(
'return Axis.horizontal',
Expand Down
Loading