【Flutter入门练中学】第3课:滚动与列表

目标

  • 理解为什么需要滚动:内容高度或宽度超出屏幕。
  • 掌握 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 改成:

dart 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  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:热重载体验滚动

题目要求

运行最小示例,完成以下操作并记录变化:

  1. 把 ListView.builder 改成 SingleChildScrollView + Column,放 10 个 ListTile。
  2. 再把 scrollDirection 改成 Axis.horizontal。
  3. 最后把 physics 改成 NeverScrollableScrollPhysics()。

答案

dart 复制代码
// 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:判断题

题目要求

判断对错,并说明理由:

  1. SingleChildScrollView 适合超长列表。
  2. ListView.builder 会按需构建子项。
  3. GridView.count 可以指定交叉轴列数。
  4. shrinkWrap: true 总是提升性能。

答案

  1. 错。
  2. 对。
  3. 对。
  4. 错。

答案解读

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 个列表项:

dart 复制代码
ListView.builder(
  itemCount: ______,
  itemBuilder: (context, ______) {
    return ListTile(title: Text('第 $index 项'));
  },
)

答案

dart 复制代码
ListView.builder(
  itemCount: 20,
  itemBuilder: (context, index) {
    return ListTile(title: Text('第 $index 项'));
  },
)

答案解读

itemCount 决定列表项数量。itemBuilder 接收 context 和 index,返回对应位置的 Widget。index 从 0 开始,所以显示 0 到 19。


练习6:代码预测题

题目要求

阅读代码,预测屏幕显示内容:

dart 复制代码
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:改错题

题目要求

下面代码会报错,找出来并修正:

dart 复制代码
Column(
  children: [
    const Text('标题'),
    ListView.builder(
      itemCount: 10,
      itemBuilder: (context, index) => ListTile(title: Text('$index')),
    ),
  ],
)

答案

修正方式之一:

dart 复制代码
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 个彩色方块:

dart 复制代码
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],
      );
    },
  ),
)

答案

dart 复制代码
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:完成图标。

答案

dart 复制代码
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 个商品。每个格子显示一个图标和商品名称。

答案

dart 复制代码
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 和一个发送按钮。

答案

dart 复制代码
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。

答案示例

dart 复制代码
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,运行后观察控制台错误,记录关键错误词。

答案

示例错误代码:

dart 复制代码
Column(
  children: [
    const Text('标题'),
    ListView.builder(
      itemCount: 10,
      itemBuilder: (context, index) => ListTile(title: Text('$index')),
    ),
  ],
)

控制台常见错误:

text 复制代码
Vertical viewport was given unbounded height.

或:

text 复制代码
RenderFlex children have non-zero flex but incoming height constraints are unbounded.

答案解读

Column 在垂直方向给子项的高度约束是无界的,而 ListView 作为垂直视口需要有限高度。解决方法:用 Expanded 包裹 ListView,或给 ListView 固定高度,或设置 shrinkWrap: true。学会看这个错误,就能快速定位嵌套滚动问题。


四、本课小结

这一课你练了:

  1. SingleChildScrollView 与 ListView 的区别;
  2. ListView.builder、ListView.separated 的按需构建;
  3. GridView.count、GridView.builder 的网格布局;
  4. scrollDirection、physics、shrinkWrap 的用途;
  5. 待办列表、商品网格、聊天页面、相册网格的实现;
  6. 通过故意制造无界高度错误,学会看 Vertical viewport was given unbounded height。

下一课可以进入 状态管理与交互 :setState 进阶、TextField、Checkbox、Switch、Slider、Form,继续用"练中学"的方式做可交互页面。

相关推荐
for_ever_love__1 小时前
iOS: CABasicAnimation动画
macos·ios·cocoa
JosieBook1 小时前
【数据库】MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战
android·数据库·mysql
勤劳打代码1 小时前
从 Demo 搭建的Flutter 演示项目 —— Forge
flutter·客户端·vibecoding
李游Leo1 小时前
HarmonyOS 7 + ArkTS + Image Kit 学习笔记:图像超分处理链路与 PixelMap 数据流实践【鸿蒙心迹】
笔记·学习·harmonyos
mmsx1 小时前
Android UI 复用体系:泛型基类、骨架与插槽
android·kotlin
m0_738185822 小时前
Flutter 鸿蒙化实战:flutter_background_service 适配 OpenHarmony,后台服务
flutter·华为·harmonyos·鸿蒙
Cici_ovo2 小时前
手机数据删除原理、运行内存、闪存、机械硬盘完整原理详解
android·智能手机
李游Leo2 小时前
HarmonyOS 7 + ArkTS + NAPI 学习笔记:Native 模块桥接、CMake 构建与跨语言调用实践【鸿蒙心迹】
笔记·学习·harmonyos
hi_LeTian2 小时前
【Linphone】Ubuntu20.04 编译 Linphone5.2 Android SDK 适配RK3326硬解码完整教程(避坑版)
android