Skip to content

Commit be950ac

Browse files
Fix SplitPane RangeError when child count changes between rebuilds (#9822)
* Fix SplitPane RangeError when child count changes between rebuilds Fixes #9648. SplitPane cached its fractions list in initState only. When the parent rebuilt the widget with a different number of children (for example, toggling a panel via a collection-if), fractions.length stayed at the old value while widget.minSizes and widget.children shrank, causing minSizeForIndex to read past the end of widget.minSizes and throw 'RangeError (index): Index out of range: index should be less than 2: 2' from the layout pass. This adds didUpdateWidget to _SplitPaneState. When the child count changes, fractions is reset to List.of(widget.initialFractions) so it stays in sync with the new children and minSizes. The existing constructor assertion already guarantees children.length matches initialFractions.length. Bumps devtools_app_shared to 0.5.2 with a CHANGELOG entry, and adds regression tests that pump a 3-child SplitPane and then a 2-child SplitPane (and vice versa) and assert no exception is thrown. * Add release note entry for SplitPane RangeError fix (#9822) * Address review: bump devtools_app_shared CHANGELOG to 0.5.2-wip * Address review: bump devtools_app_shared pubspec to 0.5.2-wip
1 parent 55c61f4 commit be950ac

5 files changed

Lines changed: 83 additions & 3 deletions

File tree

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ To learn more about DevTools, check out the
1515

1616
## General updates
1717

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

2023
## Inspector updates
2124

packages/devtools_app_shared/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Copyright 2025 The Flutter Authors
33
Use of this source code is governed by a BSD-style license that can be
44
found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
55
-->
6+
## 0.5.2-wip
7+
* Fix a `RangeError` thrown by `SplitPane` when the number of children
8+
changes between rebuilds.
9+
610
## 0.5.1
711
* Add DevTools-styled text field `DevToolsTextField`.
812
* Updates `devtools_shared` constraint to `^13.0.0`.

packages/devtools_app_shared/lib/src/ui/split_pane.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ final class SplitPane extends StatefulWidget {
8888
}
8989

9090
final class _SplitPaneState extends State<SplitPane> {
91-
late final List<double> fractions;
91+
late List<double> fractions;
9292

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

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

101+
@override
102+
void didUpdateWidget(SplitPane oldWidget) {
103+
super.didUpdateWidget(oldWidget);
104+
// When the number of children changes, the previously stored [fractions]
105+
// list will be out of sync with [widget.minSizes] and [widget.children],
106+
// which causes a RangeError during layout. Reset to the new
107+
// [initialFractions] when the child count changes.
108+
if (oldWidget.children.length != widget.children.length) {
109+
fractions = List.of(widget.initialFractions);
110+
}
111+
}
112+
101113
@override
102114
Widget build(BuildContext context) {
103115
return LayoutBuilder(builder: _buildLayout);

packages/devtools_app_shared/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44
name: devtools_app_shared
55
description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions.
6-
version: 0.5.1
6+
version: 0.5.2-wip
77
repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared
88

99
environment:

packages/devtools_app_shared/test/ui/split_pane_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,67 @@ void main() {
11541154
);
11551155
});
11561156

1157+
group('rebuilds with a different number of children', () {
1158+
testWidgets(
1159+
'does not throw a RangeError when child count shrinks',
1160+
(WidgetTester tester) async {
1161+
final threeChildSplit = buildSplitPane(
1162+
Axis.horizontal,
1163+
children: const [_w1, _w2, _w3],
1164+
initialFractions: const [0.2, 0.4, 0.4],
1165+
minSizes: const [50.0, 50.0, 50.0],
1166+
);
1167+
await tester.pumpWidget(wrap(threeChildSplit));
1168+
expect(find.byKey(_k1), findsOneWidget);
1169+
expect(find.byKey(_k2), findsOneWidget);
1170+
expect(find.byKey(_k3), findsOneWidget);
1171+
1172+
final twoChildSplit = buildSplitPane(
1173+
Axis.horizontal,
1174+
children: const [_w1, _w2],
1175+
initialFractions: const [0.5, 0.5],
1176+
minSizes: const [50.0, 50.0],
1177+
);
1178+
await tester.pumpWidget(wrap(twoChildSplit));
1179+
await tester.pumpAndSettle();
1180+
1181+
expect(tester.takeException(), isNull);
1182+
expect(find.byKey(_k1), findsOneWidget);
1183+
expect(find.byKey(_k2), findsOneWidget);
1184+
expect(find.byKey(_k3), findsNothing);
1185+
},
1186+
);
1187+
1188+
testWidgets(
1189+
'does not throw a RangeError when child count grows',
1190+
(WidgetTester tester) async {
1191+
final twoChildSplit = buildSplitPane(
1192+
Axis.horizontal,
1193+
children: const [_w1, _w2],
1194+
initialFractions: const [0.5, 0.5],
1195+
minSizes: const [50.0, 50.0],
1196+
);
1197+
await tester.pumpWidget(wrap(twoChildSplit));
1198+
expect(find.byKey(_k1), findsOneWidget);
1199+
expect(find.byKey(_k2), findsOneWidget);
1200+
1201+
final threeChildSplit = buildSplitPane(
1202+
Axis.horizontal,
1203+
children: const [_w1, _w2, _w3],
1204+
initialFractions: const [0.2, 0.4, 0.4],
1205+
minSizes: const [50.0, 50.0, 50.0],
1206+
);
1207+
await tester.pumpWidget(wrap(threeChildSplit));
1208+
await tester.pumpAndSettle();
1209+
1210+
expect(tester.takeException(), isNull);
1211+
expect(find.byKey(_k1), findsOneWidget);
1212+
expect(find.byKey(_k2), findsOneWidget);
1213+
expect(find.byKey(_k3), findsOneWidget);
1214+
},
1215+
);
1216+
});
1217+
11571218
group('axisFor', () {
11581219
testWidgetsWithWindowSize(
11591220
'return Axis.horizontal',

0 commit comments

Comments
 (0)