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
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index d80a29a..1f6fafd 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -15,7 +15,10 @@
## 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
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index 4f6e95c..a06c2ef 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -3,6 +3,10 @@
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`.
diff --git a/packages/devtools_app_shared/lib/src/ui/split_pane.dart b/packages/devtools_app_shared/lib/src/ui/split_pane.dart
index 59e5ef5..4bda05e 100644
--- a/packages/devtools_app_shared/lib/src/ui/split_pane.dart
+++ b/packages/devtools_app_shared/lib/src/ui/split_pane.dart
@@ -88,7 +88,7 @@
}
final class _SplitPaneState extends State<SplitPane> {
- late final List<double> fractions;
+ late List<double> fractions;
bool get isHorizontal => widget.axis == Axis.horizontal;
@@ -99,6 +99,18 @@
}
@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);
}
diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml
index 375fd00..cce911d 100644
--- a/packages/devtools_app_shared/pubspec.yaml
+++ b/packages/devtools_app_shared/pubspec.yaml
@@ -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:
diff --git a/packages/devtools_app_shared/test/ui/split_pane_test.dart b/packages/devtools_app_shared/test/ui/split_pane_test.dart
index 326d3c4..068f9ed 100644
--- a/packages/devtools_app_shared/test/ui/split_pane_test.dart
+++ b/packages/devtools_app_shared/test/ui/split_pane_test.dart
@@ -1154,6 +1154,67 @@
);
});
+ 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',