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.