Cause of Viewport Exception
GridView
/ ListView
grow until constraints (limits) stop this expansion & then scroll to view items beyond that size.
But Column
lays out its children without any constraints, completely ignoring its own size and screen size. There is infinite space inside of Column
.
Viewport was given unbounded height
exception occurs because Grid/ListView
expands to infinity inside a Column
.
Flexible/Expanded
exist to give Column
children constraints based Column's
size. (These two widgets cannot be used anywhere else, except inside Row
and Column
.)
Inside a Flexible/Expanded
widget in Column
, Grid/ListView
only uses remaining space not taken by other, non-flexible widgets, which get laid out first. (See bottom for layout phases info.)
shrinkWrap
is not a good solution
Using shrinkWrap: true
on ListView
inside a Column
isn't really helpful as:
ListView
no longer scrolls
ListView
can still overflow
A ListView
tall enough to show all of its items, will not scroll. Arguably defeats the purpose of using a ScrollView
widget (parent class of ListView
).
In Column
layout phase 1 (see bottom for explanation of layout phases), ListView
can be any height it wants (there are no constraints).
A ListView
with shrinkWrap:true
will grow in height to show all its items.
With enough items, a shrinkWrapped
ListView
will grow & grow (it never scrolls) to overflow whatever Column
is inside, be it screen or other tighter constraint.
Shouldn't shrinkWrap
just make ListView
only as big as its items OR remaining space (up to screen height), and then scroll?
That would make intuitive sense, but inside a Column
in phase 1 layout is done in unbounded space.
So, remaining space is unlimited/infinite. A max height is never reached. shrinkWrap:true
just keeps growing Column
height as items are added until overflowing the screen (or other smaller constraint).
Example shrinkWrap:true
Overflow
Here's an example of adding items to a shrinkwrapped ListView
in a Column
until its height is taller than the screen, creating an overflow warning area:
(just keep pressing the + sign Floating Action Button)
import 'package:flutter/material.dart';
class ColumnListViewPage extends StatefulWidget {
@override
_ColumnListViewPageState createState() => _ColumnListViewPageState();
}
class _ColumnListViewPageState extends State<ColumnListViewPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Column & ListView'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_count++;
});
},
),
body: SafeArea(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red)
),
/// // without center, Column will be as narrow as possible
alignment: Alignment.center,
/// Column is forced (constrained) to match screen dimension,
child: Column(
children: [
Text('Inner (Nested) Column'),
ListView.separated( // ← should be wrapped in Expanded
itemCount: _count,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 38.0),
child: Text('Row $index'),
),
shrinkWrap: true, // should be removed
)
],
),
),
),
);
}
}
For GridView/ListView
in a Column
, wrapping within Expanded
or Flexible
is likely the safest solution.
Expanded / Flexible
Expanded
and Flexible
can only be used inside Column
(and its siblings like Row
, etc.).
They must be used as immediate children of Column
. We can't "nest" Expanded
/Flexible
in SizedBox
or other widgets. Only directly under Column
as immediate children.
Inside Column
, placing ListView/GridView
in Expanded
or Flexible
ensures it will use only available space, rather than infinite space.
Column
→ Expanded
→ ListView
ListView/GridView
inside Expanded/Flexible
in a Column
doesn't need shrinkWrap
because it is constrained (given a max. height) by the Expanded/Flexible
widget. So when that constrained/defined height is used up, ListView/GridView
will stop growing & begin scrolling.
Column Layout Phases
Column
lays out children in 2 phases:
- 1st without constraints (unbounded space)
- 2nd with remaining space based on
Column's
parent
Phase 1
Any Column
child not inside Flexible
or Expanded
will be laid out in phase 1, in infinite, limitless space completely ignoring screen size.
Widgets that need a vertical boundary/constraint to limit their growth (e.g. ListView
) will cause a Vertical viewport was given unbounded height
exception in phase 1 as there are no vertical bounds in unbounded space.
Most widgets have intrinsic size. Text
for example, is only as high as its content & font size/style. It doesn't try to grow to fill a space. Defined-height widgets play well in Phase 1 of Column
layout.
Phase 2
Any widget that grows to fill bounds, should be put in a Flexible
or Expanded
for Phase 2 layout.
Phase 2 calculates Column's
remaining space from its parent constraints minus space used in Phase 1. This remaining space is provided to Phase 2 children as constraints.
ListView
for example, will only grow to use remaining space & stop, avoiding viewport exceptions.
More info on Column
layout algorithm and how Expanded
works within it.