Skip to content

Commit 484c226

Browse files
authored
Fix error message for unbounded viewports (flutter#123035)
Fix error message for unbounded viewports
1 parent 76ee8ad commit 484c226

File tree

4 files changed

+198
-71
lines changed

4 files changed

+198
-71
lines changed

packages/flutter/lib/src/rendering/debug.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'box.dart';
56
import 'object.dart';
67

78
export 'package:flutter/foundation.dart' show debugPrint;
@@ -311,3 +312,80 @@ bool debugAssertAllRenderVarsUnset(String reason, { bool debugCheckIntrinsicSize
311312
}());
312313
return true;
313314
}
315+
316+
/// Returns true if the given [Axis] is bounded within the given
317+
/// [BoxConstraints] in both the main and cross axis, throwing an exception
318+
/// otherwise.
319+
///
320+
/// This is used by viewports during `performLayout` and `computeDryLayout`
321+
/// because bounded constraints are required in order to layout their children.
322+
bool debugCheckHasBoundedAxis(Axis axis, BoxConstraints constraints) {
323+
assert(() {
324+
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
325+
switch (axis) {
326+
case Axis.vertical:
327+
if (!constraints.hasBoundedHeight) {
328+
throw FlutterError.fromParts(<DiagnosticsNode>[
329+
ErrorSummary('Vertical viewport was given unbounded height.'),
330+
ErrorDescription(
331+
'Viewports expand in the scrolling direction to fill their container. '
332+
'In this case, a vertical viewport was given an unlimited amount of '
333+
'vertical space in which to expand. This situation typically happens '
334+
'when a scrollable widget is nested inside another scrollable widget.',
335+
),
336+
ErrorHint(
337+
'If this widget is always nested in a scrollable widget there '
338+
'is no need to use a viewport because there will always be enough '
339+
'vertical space for the children. In this case, consider using a '
340+
'Column or Wrap instead. Otherwise, consider using a '
341+
'CustomScrollView to concatenate arbitrary slivers into a '
342+
'single scrollable.',
343+
),
344+
]);
345+
}
346+
if (!constraints.hasBoundedWidth) {
347+
throw FlutterError(
348+
'Vertical viewport was given unbounded width.\n'
349+
'Viewports expand in the cross axis to fill their container and '
350+
'constrain their children to match their extent in the cross axis. '
351+
'In this case, a vertical viewport was given an unlimited amount of '
352+
'horizontal space in which to expand.',
353+
);
354+
}
355+
break;
356+
case Axis.horizontal:
357+
if (!constraints.hasBoundedWidth) {
358+
throw FlutterError.fromParts(<DiagnosticsNode>[
359+
ErrorSummary('Horizontal viewport was given unbounded width.'),
360+
ErrorDescription(
361+
'Viewports expand in the scrolling direction to fill their container. '
362+
'In this case, a horizontal viewport was given an unlimited amount of '
363+
'horizontal space in which to expand. This situation typically happens '
364+
'when a scrollable widget is nested inside another scrollable widget.',
365+
),
366+
ErrorHint(
367+
'If this widget is always nested in a scrollable widget there '
368+
'is no need to use a viewport because there will always be enough '
369+
'horizontal space for the children. In this case, consider using a '
370+
'Row or Wrap instead. Otherwise, consider using a '
371+
'CustomScrollView to concatenate arbitrary slivers into a '
372+
'single scrollable.',
373+
),
374+
]);
375+
}
376+
if (!constraints.hasBoundedHeight) {
377+
throw FlutterError(
378+
'Horizontal viewport was given unbounded height.\n'
379+
'Viewports expand in the cross axis to fill their container and '
380+
'constrain their children to match their extent in the cross axis. '
381+
'In this case, a horizontal viewport was given an unlimited amount of '
382+
'vertical space in which to expand.',
383+
);
384+
}
385+
break;
386+
}
387+
}
388+
return true;
389+
}());
390+
return true;
391+
}

packages/flutter/lib/src/rendering/viewport.dart

Lines changed: 37 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
99
import 'package:flutter/semantics.dart';
1010

1111
import 'box.dart';
12+
import 'debug.dart';
1213
import 'layer.dart';
1314
import 'object.dart';
1415
import 'sliver.dart';
@@ -1388,73 +1389,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
13881389

13891390
@override
13901391
Size computeDryLayout(BoxConstraints constraints) {
1391-
assert(() {
1392-
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
1393-
switch (axis) {
1394-
case Axis.vertical:
1395-
if (!constraints.hasBoundedHeight) {
1396-
throw FlutterError.fromParts(<DiagnosticsNode>[
1397-
ErrorSummary('Vertical viewport was given unbounded height.'),
1398-
ErrorDescription(
1399-
'Viewports expand in the scrolling direction to fill their container. '
1400-
'In this case, a vertical viewport was given an unlimited amount of '
1401-
'vertical space in which to expand. This situation typically happens '
1402-
'when a scrollable widget is nested inside another scrollable widget.',
1403-
),
1404-
ErrorHint(
1405-
'If this widget is always nested in a scrollable widget there '
1406-
'is no need to use a viewport because there will always be enough '
1407-
'vertical space for the children. In this case, consider using a '
1408-
'Column or Wrap instead. Otherwise, consider using a '
1409-
'CustomScrollView to concatenate arbitrary slivers into a '
1410-
'single scrollable.',
1411-
),
1412-
]);
1413-
}
1414-
if (!constraints.hasBoundedWidth) {
1415-
throw FlutterError(
1416-
'Vertical viewport was given unbounded width.\n'
1417-
'Viewports expand in the cross axis to fill their container and '
1418-
'constrain their children to match their extent in the cross axis. '
1419-
'In this case, a vertical viewport was given an unlimited amount of '
1420-
'horizontal space in which to expand.',
1421-
);
1422-
}
1423-
break;
1424-
case Axis.horizontal:
1425-
if (!constraints.hasBoundedWidth) {
1426-
throw FlutterError.fromParts(<DiagnosticsNode>[
1427-
ErrorSummary('Horizontal viewport was given unbounded width.'),
1428-
ErrorDescription(
1429-
'Viewports expand in the scrolling direction to fill their container. '
1430-
'In this case, a horizontal viewport was given an unlimited amount of '
1431-
'horizontal space in which to expand. This situation typically happens '
1432-
'when a scrollable widget is nested inside another scrollable widget.',
1433-
),
1434-
ErrorHint(
1435-
'If this widget is always nested in a scrollable widget there '
1436-
'is no need to use a viewport because there will always be enough '
1437-
'horizontal space for the children. In this case, consider using a '
1438-
'Row or Wrap instead. Otherwise, consider using a '
1439-
'CustomScrollView to concatenate arbitrary slivers into a '
1440-
'single scrollable.',
1441-
),
1442-
]);
1443-
}
1444-
if (!constraints.hasBoundedHeight) {
1445-
throw FlutterError(
1446-
'Horizontal viewport was given unbounded height.\n'
1447-
'Viewports expand in the cross axis to fill their container and '
1448-
'constrain their children to match their extent in the cross axis. '
1449-
'In this case, a horizontal viewport was given an unlimited amount of '
1450-
'vertical space in which to expand.',
1451-
);
1452-
}
1453-
break;
1454-
}
1455-
}
1456-
return true;
1457-
}());
1392+
assert(debugCheckHasBoundedAxis(axis, constraints));
14581393
return constraints.biggest;
14591394
}
14601395

@@ -1858,17 +1793,48 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
18581793
late double _shrinkWrapExtent;
18591794
bool _hasVisualOverflow = false;
18601795

1796+
bool _debugCheckHasBoundedCrossAxis() {
1797+
assert(() {
1798+
switch (axis) {
1799+
case Axis.vertical:
1800+
if (!constraints.hasBoundedWidth) {
1801+
throw FlutterError(
1802+
'Vertical viewport was given unbounded width.\n'
1803+
'Viewports expand in the cross axis to fill their container and '
1804+
'constrain their children to match their extent in the cross axis. '
1805+
'In this case, a vertical shrinkwrapping viewport was given an '
1806+
'unlimited amount of horizontal space in which to expand.',
1807+
);
1808+
}
1809+
break;
1810+
case Axis.horizontal:
1811+
if (!constraints.hasBoundedHeight) {
1812+
throw FlutterError(
1813+
'Horizontal viewport was given unbounded height.\n'
1814+
'Viewports expand in the cross axis to fill their container and '
1815+
'constrain their children to match their extent in the cross axis. '
1816+
'In this case, a horizontal shrinkwrapping viewport was given an '
1817+
'unlimited amount of vertical space in which to expand.',
1818+
);
1819+
}
1820+
break;
1821+
}
1822+
return true;
1823+
}());
1824+
return true;
1825+
}
1826+
18611827
@override
18621828
void performLayout() {
18631829
final BoxConstraints constraints = this.constraints;
18641830
if (firstChild == null) {
1831+
// Shrinkwrapping viewport only requires the cross axis to be bounded.
1832+
assert(_debugCheckHasBoundedCrossAxis());
18651833
switch (axis) {
18661834
case Axis.vertical:
1867-
assert(constraints.hasBoundedWidth);
18681835
size = Size(constraints.maxWidth, constraints.minHeight);
18691836
break;
18701837
case Axis.horizontal:
1871-
assert(constraints.hasBoundedHeight);
18721838
size = Size(constraints.minWidth, constraints.maxHeight);
18731839
break;
18741840
}
@@ -1882,14 +1848,14 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
18821848

18831849
final double mainAxisExtent;
18841850
final double crossAxisExtent;
1851+
// Shrinkwrapping viewport only requires the cross axis to be bounded.
1852+
assert(_debugCheckHasBoundedCrossAxis());
18851853
switch (axis) {
18861854
case Axis.vertical:
1887-
assert(constraints.hasBoundedWidth);
18881855
mainAxisExtent = constraints.maxHeight;
18891856
crossAxisExtent = constraints.maxWidth;
18901857
break;
18911858
case Axis.horizontal:
1892-
assert(constraints.hasBoundedHeight);
18931859
mainAxisExtent = constraints.maxWidth;
18941860
crossAxisExtent = constraints.maxHeight;
18951861
break;

packages/flutter/test/rendering/debug_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,15 @@ void main() {
261261
expect(() => debugAssertAllRenderVarsUnset('ERROR'), throwsFlutterError);
262262
debugDisableOpacityLayers = false;
263263
});
264+
265+
test('debugCheckHasBoundedAxis warns for vertical and horizontal axis', () {
266+
expect(
267+
() => debugCheckHasBoundedAxis(Axis.vertical, const BoxConstraints()),
268+
throwsFlutterError,
269+
);
270+
expect(
271+
() => debugCheckHasBoundedAxis(Axis.horizontal, const BoxConstraints()),
272+
throwsFlutterError,
273+
);
274+
});
264275
}

packages/flutter/test/rendering/viewport_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,4 +2188,76 @@ void main() {
21882188
});
21892189
expect(visited, true);
21902190
});
2191+
2192+
testWidgets('Shrinkwrapping viewport asserts bounded cross axis', (WidgetTester tester) async {
2193+
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
2194+
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
2195+
// Vertical
2196+
await tester.pumpWidget(Directionality(
2197+
textDirection: TextDirection.ltr,
2198+
child: ListView(
2199+
scrollDirection: Axis.horizontal,
2200+
children: <Widget>[
2201+
ListView(
2202+
shrinkWrap: true,
2203+
children: const <Widget>[ SizedBox.square(dimension: 500) ],
2204+
),
2205+
],
2206+
),
2207+
));
2208+
2209+
expect(errors, isNotEmpty);
2210+
expect(errors.first.exception, isFlutterError);
2211+
FlutterError error = errors.first.exception as FlutterError;
2212+
expect(
2213+
error.toString(),
2214+
contains('Viewports expand in the cross axis to fill their container'),
2215+
);
2216+
errors.clear();
2217+
2218+
// Horizontal
2219+
await tester.pumpWidget(Directionality(
2220+
textDirection: TextDirection.ltr,
2221+
child: ListView(
2222+
children: <Widget>[
2223+
ListView(
2224+
scrollDirection: Axis.horizontal,
2225+
shrinkWrap: true,
2226+
children: const <Widget>[ SizedBox.square(dimension: 500) ],
2227+
),
2228+
],
2229+
),
2230+
));
2231+
2232+
expect(errors, isNotEmpty);
2233+
expect(errors.first.exception, isFlutterError);
2234+
error = errors.first.exception as FlutterError;
2235+
expect(
2236+
error.toString(),
2237+
contains('Viewports expand in the cross axis to fill their container'),
2238+
);
2239+
errors.clear();
2240+
2241+
// No children
2242+
await tester.pumpWidget(Directionality(
2243+
textDirection: TextDirection.ltr,
2244+
child: ListView(
2245+
scrollDirection: Axis.horizontal,
2246+
children: <Widget>[
2247+
ListView(
2248+
shrinkWrap: true,
2249+
),
2250+
],
2251+
),
2252+
));
2253+
2254+
expect(errors, isNotEmpty);
2255+
expect(errors.first.exception, isFlutterError);
2256+
error = errors.first.exception as FlutterError;
2257+
expect(
2258+
error.toString(),
2259+
contains('Viewports expand in the cross axis to fill their container'),
2260+
);
2261+
errors.clear();
2262+
});
21912263
}

0 commit comments

Comments
 (0)