Flutter 列表布局:ListView / GridView 我常用的几种写法

我做 Flutter 页面的时候,最常写的就是列表:消息流、设置页、商品流、相册……几乎所有“内容型页面”,最后都会落到 ListViewGridView 上。

这篇文章不打算把 API 手册从头抄一遍,我更想写成那种“我平时怎么用、为什么这么写、哪里容易翻车”的小结,给你一个可直接套用的思路。


先选对组件:ListView 还是 GridView?

简单到有点粗暴,但很好用:

  • 一行一个ListView
  • 一屏多个卡片(二维排列)→ GridView

它们本质都是可滚动的布局容器。差别主要在“子项怎么排”:List 是单列/单行,Grid 是二维网格。


ListView:从“能跑”到“跑得顺”

1)ListView(children: ...) 适合短列表

如果你只有十几个固定项(比如设置页),直接写 children 最省心:

1
2
3
4
5
6
7
8
ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(title: Text('账号')),
ListTile(title: Text('通知')),
ListTile(title: Text('隐私')),
],
)

但只要数据一多,我会立刻切换到 builder

2)长列表优先 ListView.builder

builder 的核心价值是:只构建屏幕上看得到的那部分。这点在图片、复杂卡片里非常关键。

1
2
3
4
5
6
7
8
9
10
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
);
},
)

3)我很常用:ListView.separated

很多列表就是“内容 + 分割线”,separated 省得你自己在 item 里判断 index:

1
2
3
4
5
6
7
8
9
10
11
12
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
leading: CircleAvatar(child: Text(item.title.substring(0, 1))),
title: Text(item.title),
subtitle: Text(item.subtitle),
);
},
)

4)两个常见坑(我踩过不止一次)

坑 A:把 ListView 塞进 Column,然后直接报错/溢出

一般是因为 ListView 想要“无限高”,但 Column 给不了。我的习惯是:

  • 能用整屏滚动:直接让 ListView 做 body
  • 必须上下还有别的固定区域:用 Expanded 包起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text('标题'),
),
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => ListTile(title: Text(items[i].title)),
),
),
],
)

坑 B:shrinkWrap: true 滥用导致卡顿

shrinkWrap 会让列表去“量身高”,代价就是额外计算和布局;在嵌套滚动里它有时不得不用,但我会把它当成“最后手段”,并尽量改成 CustomScrollView(下面会提)。


GridView:把网格写顺手

1)最常用的两种 gridDelegate

按列数固定:SliverGridDelegateWithFixedCrossAxisCount

适合“每行就是 2/3/4 个卡片”的设计:

1
2
3
4
5
6
7
8
9
10
11
GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 3 / 4,
),
itemCount: items.length,
itemBuilder: (context, index) => ProductCard(item: items[index]),
)

按宽度自适应:SliverGridDelegateWithMaxCrossAxisExtent

适合不同屏幕尺寸下“尽量多排,但别挤太窄”:

1
2
3
4
5
6
7
8
9
10
11
GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 220,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 3 / 4,
),
itemCount: items.length,
itemBuilder: (context, index) => ProductCard(item: items[index]),
)

我选它的理由很简单:你不用为“iPhone / iPad / 横屏”分别写 crossAxisCount,视觉更稳。


一个更通用的解法:CustomScrollView + Sliver(混排神器)

当你遇到这种页面:

  • 顶部一块 banner(可滚动)
  • 下面先来一段横向分类(可滚动)
  • 再接一个纵向列表 / 网格

这时候我会直接上 CustomScrollView,用 SliverList / SliverGrid 混在一起,不用再把 ListView/GridView 套来套去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('今日推荐', style: TextStyle(fontSize: 18)),
),
),
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 3 / 4,
),
delegate: SliverChildBuilderDelegate(
(context, index) => ProductCard(item: items[index]),
childCount: items.length,
),
),
],
)

我现在做“内容流页面”,基本都靠它吃饭。


我自己的小清单(写完会顺很多)

  • 数据多:优先 builderListView.builder / GridView.builder
  • 尽量减少每个 item 的“重活”:图片、阴影、复杂布局能简则简
  • 列表高度固定时,考虑 itemExtent / prototypeItem(能省不少布局成本)
  • 避免把多个可滚动组件互相嵌套;需要混排时直接用 CustomScrollView + Sliver
  • 有状态的 item(比如点赞/展开)记得用合适的 Key,否则滚动复用时容易“串戏”

结尾

ListViewGridView 其实不难,难的是在真实页面里:既要写得顺,又要滑得顺。

Share