Skip to content
Open
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
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