目标
- 理解为什么需要滚动:内容高度或宽度超出屏幕。
- 掌握
SingleChildScrollView、ListView、ListView.builder、ListView.separated。 - 掌握
GridView.count、GridView.builder的基础用法。 - 理解
scrollDirection、physics、shrinkWrap的作用与代价。 - 能实现待办列表、商品网格、聊天列表、相册网格。
- 能识别并修复
Vertical viewport was given unbounded height等常见滚动错误。 - 通过“题目要求 → 答案 → 答案解读”的多元化习题完成练中学。

一、本课核心概念速览
| Widget / 属性 | 作用 |
|---|---|
| SingleChildScrollView | 让单个子组件可滚动,适合少量内容 |
| ListView | 可滚动列表,children 适合少量固定项 |
| ListView.builder | 按需构建列表项,适合大量或动态数据 |
| ListView.separated | 带分隔线的列表 |
| GridView.count | 固定列数的网格 |
| GridView.builder | 按需构建网格项 |
| scrollDirection | 滚动方向:垂直或水平 |
| physics | 滚动物理效果,如回弹、禁用滚动 |
| shrinkWrap | 让列表按内容高度收缩,慎用,可能影响性能 |
一句话口诀:
少量内容用 SingleChildScrollView,大量列表用 builder,网格用 GridView。

二、最小可运行示例
把 lib/main.dart 改成:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('滚动与列表')),
body: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('$index')),
title: Text('第 $index 项'),
subtitle: const Text('练中学:滚动列表'),
);
},
),
),
);
}
}
按 r 热重载,滚动页面,观察 ListView.builder 如何按需构建列表项。

三、多元化习题
练习0:热重载体验滚动
题目要求
运行最小示例,完成以下操作并记录变化:
- 把
ListView.builder改成SingleChildScrollView + Column,放 10 个ListTile。 - 再把
scrollDirection改成Axis.horizontal。 - 最后把
physics改成NeverScrollableScrollPhysics()。
答案
// 1. SingleChildScrollView + Column
body: SingleChildScrollView(
child: Column(
children: List.generate(
10,
(index) => ListTile(title: Text('第 $index 项')),
),
),
),
// 2. 水平滚动
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (context, index) {
return Container(
width: 120,
margin: const EdgeInsets.all(8),
color: Colors.blue.shade100,
alignment: Alignment.center,
child: Text('第 $index 项'),
);
},
),
// 3. 禁止滚动
body: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 30,
itemBuilder: (context, index) => ListTile(title: Text('第 $index 项')),
),
答案解读
SingleChildScrollView 适合内容不多的页面,它会一次性构建所有子项。ListView.builder 适合长列表,会按需构建可见项。scrollDirection 控制滚动方向。NeverScrollableScrollPhysics() 可以禁用滚动,常用于嵌套滚动或自定义滚动控制。
练习1:选择题
题目要求
当列表项数量很多且需要动态生成时,推荐使用哪个?
A. ListView(children: [...])
B. ListView.builder
C. Column
D. SingleChildScrollView
答案
B。
答案解读
ListView(children: [...]) 会一次性构建所有子项,适合少量固定内容。ListView.builder 按需构建,性能更好,适合大量或动态数据。Column 本身不滚动,SingleChildScrollView 也是一次性构建所有子项。
练习2:判断题
题目要求
判断对错,并说明理由:
SingleChildScrollView适合超长列表。ListView.builder会按需构建子项。GridView.count可以指定交叉轴列数。shrinkWrap: true总是提升性能。
答案
- 错。
- 对。
- 对。
- 错。
答案解读
SingleChildScrollView 会一次性构建所有子项,超长列表会占用大量内存,应该用 ListView.builder。GridView.count 的 crossAxisCount 可以指定列数。shrinkWrap: true 让列表按内容收缩,但通常会增加布局计算成本,只有在嵌套滚动等必要场景才使用。
练习3:连线题
题目要求
将左侧 Widget 或属性与右侧作用连线:
| 左侧 | 右侧 |
|---|---|
| SingleChildScrollView | 按需构建列表 |
| ListView.builder | 单个子组件可滚动 |
| ListView.separated | 滚动物理效果 |
| GridView.count | 带分隔线的列表 |
| physics | 固定列数网格 |
答案
- SingleChildScrollView — 单个子组件可滚动
- ListView.builder — 按需构建列表
- ListView.separated — 带分隔线的列表
- GridView.count — 固定列数网格
- physics — 滚动物理效果
答案解读
这些是滚动与列表最常用的成员。先记住用途,再通过组合完成页面。
练习4:排序题
题目要求
实现一个页面:顶部标题,下面 20 个列表项。请按从外到内的嵌套顺序排列:
Scaffold、Column、Text、Expanded、ListView.builder、ListTile
答案
Scaffold → Column → Text → Expanded → ListView.builder → ListTile
答案解读
Scaffold 是页面骨架,Column 垂直排列标题和列表。Expanded 让 ListView 占满剩余高度,否则 ListView 在 Column 中会得到无界高度而报错。ListView.builder 生成 ListTile。
练习5:填空题
题目要求
补全代码,生成 20 个列表项:
ListView.builder(
itemCount: ______,
itemBuilder: (context, ______) {
return ListTile(title: Text('第 $index 项'));
},
)
答案
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('第 $index 项'));
},
)
答案解读
itemCount 决定列表项数量。itemBuilder 接收 context 和 index,返回对应位置的 Widget。index 从 0 开始,所以显示 0 到 19。
练习6:代码预测题
题目要求
阅读代码,预测屏幕显示内容:
ListView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return ListTile(title: Text('$index'));
},
)
答案
屏幕显示三行:0、1、2。
答案解读
itemCount: 3 表示生成 3 项,index 分别为 0、1、2。ListTile 的 title 显示当前索引。
练习7:改错题
题目要求
下面代码会报错,找出来并修正:
Column(
children: [
const Text('标题'),
ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
),
],
)
答案
修正方式之一:
Column(
children: [
const Text('标题'),
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
),
),
],
)
答案解读
Column 给子项的高度约束是无界的,而 ListView 在垂直方向需要有限高度。用 Expanded 包裹后,ListView 会获得剩余高度。也可以给 ListView 固定高度,或设置 shrinkWrap: true,但 Expanded 更常用。
练习8:补全代码题
题目要求
做一个水平滚动列表,显示 5 个彩色方块:
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: ______,
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: 100,
margin: const EdgeInsets.all(8),
color: Colors.primaries[index % Colors.primaries.length],
);
},
),
)
答案
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: 100,
margin: const EdgeInsets.all(8),
color: Colors.primaries[index % Colors.primaries.length],
);
},
),
)
答案解读
水平滚动必须给 ListView 一个有限高度,所以外层用 SizedBox(height: 100)。scrollDirection: Axis.horizontal 让列表水平滚动。每个 Container 有固定宽度 100。
练习9:动手编码题:待办列表
题目要求
用 ListView.builder 显示 10 个待办项。每项使用 ListTile,包含:
leading:圆形序号;title:待办内容;trailing:完成图标。
答案
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('待办事项 ${index + 1}'),
subtitle: const Text('练中学:列表项'),
trailing: const Icon(Icons.check_circle_outline),
);
},
)
答案解读
ListTile 是列表项的标准组件,leading 放左侧组件,title 和 subtitle 放文字,trailing 放右侧组件。index + 1 让序号从 1 开始。
练习10:动手编码题:商品网格
题目要求
用 GridView.count 做 2 列商品网格,共 6 个商品。每个格子显示一个图标和商品名称。
答案
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: List.generate(6, (index) {
return Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.shopping_bag, size: 40, color: Colors.blue),
const SizedBox(height: 8),
Text('商品 ${index + 1}'),
],
),
);
}),
)
答案解读
GridView.count 的 crossAxisCount: 2 表示两列。mainAxisSpacing 和 crossAxisSpacing 控制间距。List.generate 快速生成 6 个格子。每个格子用 Container 加圆角和背景色。
练习11:综合挑战:聊天列表 + 输入栏
题目要求
用 Column + Expanded + ListView.builder 做聊天页面:上方是聊天记录列表,下方是输入栏。列表显示 20 条消息,输入栏包含一个 TextField 和一个发送按钮。
答案
body: Column(
children: [
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: 20,
itemBuilder: (context, index) {
final isMe = index % 2 == 0;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isMe ? Colors.blue.shade100 : Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Text('消息 $index'),
),
);
},
),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.white,
child: Row(
children: [
const Expanded(
child: TextField(
decoration: InputDecoration(
hintText: '输入消息',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
child: const Text('发送'),
),
],
),
),
],
)
答案解读
Column 垂直排列聊天区和输入栏。聊天区用 Expanded 包裹 ListView.builder,占满剩余空间。每条消息根据 index 判断左右对齐。底部 Row 中 TextField 用 Expanded 占剩余宽度,按钮固定宽度。
练习12:开放式挑战:相册网格
题目要求
用 GridView.builder 做 3 列相册,共 12 张。每格是彩色方块,带圆角和图标。要求使用 SliverGridDelegateWithFixedCrossAxisCount。
答案示例
body: GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: 12,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length].shade200,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.photo, size: 36, color: Colors.white),
);
},
)
答案解读
GridView.builder 按需构建网格项,适合数量较多的情况。SliverGridDelegateWithFixedCrossAxisCount 指定列数、间距。index % Colors.primaries.length 让颜色循环使用。开放题没有唯一答案,重点是能运行、能滚动、能热重载修改。
练习13:错误观察挑战
题目要求
故意在 Column 中直接放 ListView,不包 Expanded,运行后观察控制台错误,记录关键错误词。
答案
示例错误代码:
Column(
children: [
const Text('标题'),
ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
),
],
)
控制台常见错误:
Vertical viewport was given unbounded height.
或:
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
答案解读
Column 在垂直方向给子项的高度约束是无界的,而 ListView 作为垂直视口需要有限高度。解决方法:用 Expanded 包裹 ListView,或给 ListView 固定高度,或设置 shrinkWrap: true。学会看这个错误,就能快速定位嵌套滚动问题。
四、本课小结
这一课你练了:
SingleChildScrollView与ListView的区别;ListView.builder、ListView.separated的按需构建;GridView.count、GridView.builder的网格布局;scrollDirection、physics、shrinkWrap的用途;- 待办列表、商品网格、聊天页面、相册网格的实现;
- 通过故意制造无界高度错误,学会看
Vertical viewport was given unbounded height。
下一课可以进入 状态管理与交互:setState 进阶、TextField、Checkbox、Switch、Slider、Form,继续用“练中学”的方式做可交互页面。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/jackchuanqi/article/details/166139076




