Fix error message for unbounded viewports (#123035)
Fix error message for unbounded viewports
diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart
index 2879bac..cff2791 100644
--- a/packages/flutter/lib/src/rendering/debug.dart
+++ b/packages/flutter/lib/src/rendering/debug.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'box.dart';
import 'object.dart';
export 'package:flutter/foundation.dart' show debugPrint;
@@ -311,3 +312,80 @@
}());
return true;
}
+
+/// Returns true if the given [Axis] is bounded within the given
+/// [BoxConstraints] in both the main and cross axis, throwing an exception
+/// otherwise.
+///
+/// This is used by viewports during `performLayout` and `computeDryLayout`
+/// because bounded constraints are required in order to layout their children.
+bool debugCheckHasBoundedAxis(Axis axis, BoxConstraints constraints) {
+ assert(() {
+ if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
+ switch (axis) {
+ case Axis.vertical:
+ if (!constraints.hasBoundedHeight) {
+ throw FlutterError.fromParts(<DiagnosticsNode>[
+ ErrorSummary('Vertical viewport was given unbounded height.'),
+ ErrorDescription(
+ 'Viewports expand in the scrolling direction to fill their container. '
+ 'In this case, a vertical viewport was given an unlimited amount of '
+ 'vertical space in which to expand. This situation typically happens '
+ 'when a scrollable widget is nested inside another scrollable widget.',
+ ),
+ ErrorHint(
+ 'If this widget is always nested in a scrollable widget there '
+ 'is no need to use a viewport because there will always be enough '
+ 'vertical space for the children. In this case, consider using a '
+ 'Column or Wrap instead. Otherwise, consider using a '
+ 'CustomScrollView to concatenate arbitrary slivers into a '
+ 'single scrollable.',
+ ),
+ ]);
+ }
+ if (!constraints.hasBoundedWidth) {
+ throw FlutterError(
+ 'Vertical viewport was given unbounded width.\n'
+ 'Viewports expand in the cross axis to fill their container and '
+ 'constrain their children to match their extent in the cross axis. '
+ 'In this case, a vertical viewport was given an unlimited amount of '
+ 'horizontal space in which to expand.',
+ );
+ }
+ break;
+ case Axis.horizontal:
+ if (!constraints.hasBoundedWidth) {
+ throw FlutterError.fromParts(<DiagnosticsNode>[
+ ErrorSummary('Horizontal viewport was given unbounded width.'),
+ ErrorDescription(
+ 'Viewports expand in the scrolling direction to fill their container. '
+ 'In this case, a horizontal viewport was given an unlimited amount of '
+ 'horizontal space in which to expand. This situation typically happens '
+ 'when a scrollable widget is nested inside another scrollable widget.',
+ ),
+ ErrorHint(
+ 'If this widget is always nested in a scrollable widget there '
+ 'is no need to use a viewport because there will always be enough '
+ 'horizontal space for the children. In this case, consider using a '
+ 'Row or Wrap instead. Otherwise, consider using a '
+ 'CustomScrollView to concatenate arbitrary slivers into a '
+ 'single scrollable.',
+ ),
+ ]);
+ }
+ if (!constraints.hasBoundedHeight) {
+ throw FlutterError(
+ 'Horizontal viewport was given unbounded height.\n'
+ 'Viewports expand in the cross axis to fill their container and '
+ 'constrain their children to match their extent in the cross axis. '
+ 'In this case, a horizontal viewport was given an unlimited amount of '
+ 'vertical space in which to expand.',
+ );
+ }
+ break;
+ }
+ }
+ return true;
+ }());
+ return true;
+}
diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart
index 804fdef4..8622068 100644
--- a/packages/flutter/lib/src/rendering/viewport.dart
+++ b/packages/flutter/lib/src/rendering/viewport.dart
@@ -9,6 +9,7 @@
import 'package:flutter/semantics.dart';
import 'box.dart';
+import 'debug.dart';
import 'layer.dart';
import 'object.dart';
import 'sliver.dart';
@@ -1388,73 +1389,7 @@
@override
Size computeDryLayout(BoxConstraints constraints) {
- assert(() {
- if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
- switch (axis) {
- case Axis.vertical:
- if (!constraints.hasBoundedHeight) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('Vertical viewport was given unbounded height.'),
- ErrorDescription(
- 'Viewports expand in the scrolling direction to fill their container. '
- 'In this case, a vertical viewport was given an unlimited amount of '
- 'vertical space in which to expand. This situation typically happens '
- 'when a scrollable widget is nested inside another scrollable widget.',
- ),
- ErrorHint(
- 'If this widget is always nested in a scrollable widget there '
- 'is no need to use a viewport because there will always be enough '
- 'vertical space for the children. In this case, consider using a '
- 'Column or Wrap instead. Otherwise, consider using a '
- 'CustomScrollView to concatenate arbitrary slivers into a '
- 'single scrollable.',
- ),
- ]);
- }
- if (!constraints.hasBoundedWidth) {
- throw FlutterError(
- 'Vertical viewport was given unbounded width.\n'
- 'Viewports expand in the cross axis to fill their container and '
- 'constrain their children to match their extent in the cross axis. '
- 'In this case, a vertical viewport was given an unlimited amount of '
- 'horizontal space in which to expand.',
- );
- }
- break;
- case Axis.horizontal:
- if (!constraints.hasBoundedWidth) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('Horizontal viewport was given unbounded width.'),
- ErrorDescription(
- 'Viewports expand in the scrolling direction to fill their container. '
- 'In this case, a horizontal viewport was given an unlimited amount of '
- 'horizontal space in which to expand. This situation typically happens '
- 'when a scrollable widget is nested inside another scrollable widget.',
- ),
- ErrorHint(
- 'If this widget is always nested in a scrollable widget there '
- 'is no need to use a viewport because there will always be enough '
- 'horizontal space for the children. In this case, consider using a '
- 'Row or Wrap instead. Otherwise, consider using a '
- 'CustomScrollView to concatenate arbitrary slivers into a '
- 'single scrollable.',
- ),
- ]);
- }
- if (!constraints.hasBoundedHeight) {
- throw FlutterError(
- 'Horizontal viewport was given unbounded height.\n'
- 'Viewports expand in the cross axis to fill their container and '
- 'constrain their children to match their extent in the cross axis. '
- 'In this case, a horizontal viewport was given an unlimited amount of '
- 'vertical space in which to expand.',
- );
- }
- break;
- }
- }
- return true;
- }());
+ assert(debugCheckHasBoundedAxis(axis, constraints));
return constraints.biggest;
}
@@ -1858,17 +1793,48 @@
late double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
+ bool _debugCheckHasBoundedCrossAxis() {
+ assert(() {
+ switch (axis) {
+ case Axis.vertical:
+ if (!constraints.hasBoundedWidth) {
+ throw FlutterError(
+ 'Vertical viewport was given unbounded width.\n'
+ 'Viewports expand in the cross axis to fill their container and '
+ 'constrain their children to match their extent in the cross axis. '
+ 'In this case, a vertical shrinkwrapping viewport was given an '
+ 'unlimited amount of horizontal space in which to expand.',
+ );
+ }
+ break;
+ case Axis.horizontal:
+ if (!constraints.hasBoundedHeight) {
+ throw FlutterError(
+ 'Horizontal viewport was given unbounded height.\n'
+ 'Viewports expand in the cross axis to fill their container and '
+ 'constrain their children to match their extent in the cross axis. '
+ 'In this case, a horizontal shrinkwrapping viewport was given an '
+ 'unlimited amount of vertical space in which to expand.',
+ );
+ }
+ break;
+ }
+ return true;
+ }());
+ return true;
+ }
+
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (firstChild == null) {
+ // Shrinkwrapping viewport only requires the cross axis to be bounded.
+ assert(_debugCheckHasBoundedCrossAxis());
switch (axis) {
case Axis.vertical:
- assert(constraints.hasBoundedWidth);
size = Size(constraints.maxWidth, constraints.minHeight);
break;
case Axis.horizontal:
- assert(constraints.hasBoundedHeight);
size = Size(constraints.minWidth, constraints.maxHeight);
break;
}
@@ -1882,14 +1848,14 @@
final double mainAxisExtent;
final double crossAxisExtent;
+ // Shrinkwrapping viewport only requires the cross axis to be bounded.
+ assert(_debugCheckHasBoundedCrossAxis());
switch (axis) {
case Axis.vertical:
- assert(constraints.hasBoundedWidth);
mainAxisExtent = constraints.maxHeight;
crossAxisExtent = constraints.maxWidth;
break;
case Axis.horizontal:
- assert(constraints.hasBoundedHeight);
mainAxisExtent = constraints.maxWidth;
crossAxisExtent = constraints.maxHeight;
break;
diff --git a/packages/flutter/test/rendering/debug_test.dart b/packages/flutter/test/rendering/debug_test.dart
index 74d80e4..034f014 100644
--- a/packages/flutter/test/rendering/debug_test.dart
+++ b/packages/flutter/test/rendering/debug_test.dart
@@ -261,4 +261,15 @@
expect(() => debugAssertAllRenderVarsUnset('ERROR'), throwsFlutterError);
debugDisableOpacityLayers = false;
});
+
+ test('debugCheckHasBoundedAxis warns for vertical and horizontal axis', () {
+ expect(
+ () => debugCheckHasBoundedAxis(Axis.vertical, const BoxConstraints()),
+ throwsFlutterError,
+ );
+ expect(
+ () => debugCheckHasBoundedAxis(Axis.horizontal, const BoxConstraints()),
+ throwsFlutterError,
+ );
+ });
}
diff --git a/packages/flutter/test/rendering/viewport_test.dart b/packages/flutter/test/rendering/viewport_test.dart
index e69057a..e2e0001 100644
--- a/packages/flutter/test/rendering/viewport_test.dart
+++ b/packages/flutter/test/rendering/viewport_test.dart
@@ -2188,4 +2188,76 @@
});
expect(visited, true);
});
+
+ testWidgets('Shrinkwrapping viewport asserts bounded cross axis', (WidgetTester tester) async {
+ final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
+ FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
+ // Vertical
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: <Widget>[
+ ListView(
+ shrinkWrap: true,
+ children: const <Widget>[ SizedBox.square(dimension: 500) ],
+ ),
+ ],
+ ),
+ ));
+
+ expect(errors, isNotEmpty);
+ expect(errors.first.exception, isFlutterError);
+ FlutterError error = errors.first.exception as FlutterError;
+ expect(
+ error.toString(),
+ contains('Viewports expand in the cross axis to fill their container'),
+ );
+ errors.clear();
+
+ // Horizontal
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: ListView(
+ children: <Widget>[
+ ListView(
+ scrollDirection: Axis.horizontal,
+ shrinkWrap: true,
+ children: const <Widget>[ SizedBox.square(dimension: 500) ],
+ ),
+ ],
+ ),
+ ));
+
+ expect(errors, isNotEmpty);
+ expect(errors.first.exception, isFlutterError);
+ error = errors.first.exception as FlutterError;
+ expect(
+ error.toString(),
+ contains('Viewports expand in the cross axis to fill their container'),
+ );
+ errors.clear();
+
+ // No children
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: <Widget>[
+ ListView(
+ shrinkWrap: true,
+ ),
+ ],
+ ),
+ ));
+
+ expect(errors, isNotEmpty);
+ expect(errors.first.exception, isFlutterError);
+ error = errors.first.exception as FlutterError;
+ expect(
+ error.toString(),
+ contains('Viewports expand in the cross axis to fill their container'),
+ );
+ errors.clear();
+ });
}