From 2e5ddda40423723b70c9a278971a0ac22ecf15c8 Mon Sep 17 00:00:00 2001 From: Ishaq Hassan Date: Wed, 6 May 2026 17:51:31 +0000 Subject: [PATCH 1/4] 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. --- packages/devtools_app_shared/CHANGELOG.md | 4 ++ .../lib/src/ui/split_pane.dart | 14 ++++- packages/devtools_app_shared/pubspec.yaml | 2 +- .../test/ui/split_pane_test.dart | 61 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 4f6e95c4104..ebf4451d066 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -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 +* 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 59e5ef576dc..4bda05e1d38 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 SplitPane extends StatefulWidget { } final class _SplitPaneState extends State { - late final List fractions; + late List fractions; bool get isHorizontal => widget.axis == Axis.horizontal; @@ -98,6 +98,18 @@ final class _SplitPaneState extends State { 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); diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml index 375fd0009d3..a27056d5686 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 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 326d3c477e9..068f9ed3392 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 @@ 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', From e1aa36ce4127f8564e7742a542f693ada021e728 Mon Sep 17 00:00:00 2001 From: Ishaq Hassan Date: Fri, 8 May 2026 00:25:28 +0500 Subject: [PATCH 2/4] Add release note entry for SplitPane RangeError fix (#9822) --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index de0d1ce7f5a..66c275a26e3 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 @@ 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 From 87f09a6cad408ec9ebf55b005ae71e82747c3ca0 Mon Sep 17 00:00:00 2001 From: Ishaq Hassan Date: Fri, 8 May 2026 02:43:25 +0500 Subject: [PATCH 3/4] Address review: bump devtools_app_shared CHANGELOG to 0.5.2-wip --- packages/devtools_app_shared/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index ebf4451d066..a06c2efb934 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -3,7 +3,7 @@ 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 +## 0.5.2-wip * Fix a `RangeError` thrown by `SplitPane` when the number of children changes between rebuilds. From 71c6c0a29c00b037d7227244299c1229e88aca22 Mon Sep 17 00:00:00 2001 From: Ishaq Hassan Date: Fri, 8 May 2026 02:43:39 +0500 Subject: [PATCH 4/4] Address review: bump devtools_app_shared pubspec to 0.5.2-wip --- packages/devtools_app_shared/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml index a27056d5686..cce911d0d4b 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.2 +version: 0.5.2-wip repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared environment: