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',