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 diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 4f6e95c4104..a06c2efb934 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-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 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..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.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 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',