ListView.builder 的核心就一句话:
它不是一次把所有列表项都建出来,而是根据当前滚动位置,只创建“屏幕里需要的那几项”和前后少量缓存项。
所以它适合长列表、无限列表、分页列表。
1. 它底层到底是什么
ListView.builder 往下大致可以理解成这条链:
ListView.builder
→ Scrollable
→ Viewport
→ SliverList
→ SliverChildBuilderDelegate(itemBuilder)
你可以把这几个东西这样理解:
Scrollable:负责滚动手势、滚动位置Viewport:负责“当前屏幕窗口里该显示哪一段”SliverList:负责真正摆放列表项SliverChildBuilderDelegate:负责“需要某个 index 时,调用itemBuilder去现做”
所以 itemBuilder 不是提前全部执行,而是 需要哪个 index,才现建哪个 index。
2. 它是怎么工作的
假设你有 1000 条数据,屏幕一次只能看到 8 条。
第一步:初次显示
页面刚出现时,Flutter 会先算:
- 当前屏幕能看到哪些 item
- 前后还要多准备一点缓存区域
比如可能需要:
0 ~ 7 屏幕内
8 ~ 10 提前缓存
这时 itemBuilder 只会被调用这些 index。
第二步:创建这些项
对每个需要的 index,Flutter 会做这些事:
- 调用
itemBuilder(context, index) - 生成对应的
Widget - 为它创建
Element - 如果有
StatefulWidget,创建State - 创建对应的
RenderObject - layout + paint
注意:
真正长期存在、有生命周期的,不是 Widget 本身,而是 Element / State / RenderObject。
Widget 更像一张配置单,随时都能重新生成。
第三步:用户开始滚动
当你往下滑时,滚动偏移发生变化,Viewport 会重新计算:
- 哪些 item 进入了屏幕或缓存区
- 哪些 item 彻底离开了可见范围和缓存范围
然后 SliverList 会:
- 对新需要的 item 调
itemBuilder - 对已经没用的 item 做“垃圾回收”
3. 这里最关键:滑进屏幕、再滑出屏幕,会发生什么
这个问题要分 3 种情况看。
情况 1:刚滑出屏幕,但还在缓存区内
不会立刻销毁。
ListView.builder 通常会保留屏幕前后的一小段缓存区域。
这样做是为了:
- 继续滑时更顺滑
- 避免刚出屏幕就立刻销毁、又立刻重建
所以某个 item 刚离开屏幕时,往往还在内存里,State 也还在。
情况 2:滑得更远,已经离开缓存区
如果这个 item 没有 keep alive,那么 Flutter 会把它回收掉。
回收时通常会发生:
- 这个 item 对应的
Element被移除 RenderObject被销毁- 如果是
StatefulWidget,它的State.dispose()会被调用
也就是说:
这个列表项对应的界面对象没了。
但要注意,没的是 UI 这一层,不是你的业务数据。
比如你列表数据存在 List<Message> 里,那数据还在;只是这条 cell 的界面对象被回收了。
情况 3:之后又滑回来了
如果之前已经被回收,那么再滑回来时:
itemBuilder会再次被调用- 会重新创建新的
Widget - 重新创建新的
Element - 如果是
StatefulWidget,重新创建新的State - 再次
initState - 再次 layout / paint
所以:
滑出很远又滑回来,通常不是“把原来的 cell 拿出来复用”,而是“重新建一个新的”。
这点和 Android RecyclerView 的“ViewHolder 复用”思路不完全一样。
4. 它不是传统意义上的“复用 cell”
很多人会把 ListView.builder 想成:
滑出去一个 cell,就把这个 cell 拿来给新位置用
Flutter 不是这么玩的。
更准确地说:
- Flutter 按需创建
- 不需要了就 销毁
- 再需要时就 重建
- 有些场景下会 缓存/保活
- 但不是 RecyclerView 那种手动/显式 ViewHolder 复用模型
所以你可以记成:
Flutter 更偏“重建配置 + 管理 Element/RenderObject 生命周期”,而不是“循环复用旧 View”。
5. 那列表项里的状态会不会丢?
会丢的情况
如果你的状态写在 cell 自己内部,而且 item 滑出很远被销毁了,那么再滑回来时,这个状态会丢。
比如:
- 某个 cell 内部有一个
bool expanded - 某个 cell 内部有一个临时计数器
- 某个
StatefulWidget里的动画进度
因为这个 State 已经 dispose 了。
不会丢的情况
如果状态不放在 cell 自己内部,而是放在外部数据层,比如:
ProviderGetXBlocRiverpod- 父组件的 list 数据
- controller / model
那即使 cell 被销毁,数据还在,回来时重新根据数据渲染就行。
所以长列表开发里一个很重要的原则是:
列表项里的“业务状态”尽量外置,不要全塞在 cell 自己的 State 里。
6. 什么是 KeepAlive
如果你不想让某个 item 滑出后被销毁,可以让它“保活”。
最常见方式是 AutomaticKeepAliveClientMixin:
class ItemState extends State with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return ...;
}
}
这样当 item 滑出可见区域时,它不一定马上 dispose,而是可能进入一个 keepAlive bucket。
你可以把它理解成:
- 普通 item:滑远了就扔掉
- keepAlive item:滑远了先放仓库里,等你滑回来再拿出来
滑回来时会怎样
如果它是 keepAlive 的:
- 不会重新创建
State - 不会再走一遍
initState - 原来的输入内容、展开状态等可以保留
但要注意
keepAlive 不是越多越好。
因为保活意味着:
- 占更多内存
- 列表很长时成本会升高
所以通常只对确实需要保留界面状态的 item 用,比如:
- 输入框
- 视频播放 cell
- 展开/折叠状态复杂的卡片
7. 一个完整时间线例子
假设屏幕大概能显示 0~7 项。
初始进入页面
构建:
0~7 可见
8~10 缓存
往下滑到 5
此时可能是:
5~12 可见
13~15 缓存
0~2 被回收或准备回收
再往下滑到 30
这时:
30附近的项被新建0~10早就离开缓存范围- 如果没 keepAlive,这些项对应的 State 基本都已经
dispose
再滑回到 0
会发生:
itemBuilder(0)再次执行itemBuilder(1)再次执行- 重新创建新的 cell
如果这些 cell 原来有内部状态,但没保活,那就相当于“重新打开”。
8. 为什么它性能好
因为它避免了两件很贵的事:
1. 避免一次性创建所有 item
如果你有 10000 条数据,一次性全建出来会非常重:
- build 开销大
- layout 开销大
- 内存占用高
builder 只建当前需要的少量项。
2. 及时回收不用的 item
滑远了就回收,避免所有 cell 长期占内存。
所以 ListView.builder 本质上是:
用“按需创建 + 超出回收 + 少量缓存”的方式,换取长列表性能。
9. 几个很关键的补充点
itemBuilder 可能被多次调用
同一个 index,不代表只会 build 一次。
滑出再滑回,或者父组件重建,都可能再次调用。
所以不要把 itemBuilder 当成“只执行一次的初始化函数”。
不要在 itemBuilder 里做重活
比如:
- 大量计算
- JSON 解析
- 同步 IO
- 每次都新建很重的 controller
因为滚动时它会频繁触发,容易卡顿。
有固定高度时尽量告诉 Flutter
如果每个 item 高度固定,优先用:
itemExtentprototypeItem
这样 Flutter 更容易快速算出哪些 item 该出现,性能更好。
列表会增删改顺序时要用稳定 Key
如果数据会插入、删除、重排,最好给每个 item 一个稳定 Key,否则可能出现状态串位。
10. 一句话总结
ListView.builder 的原理就是:基于滚动位置,借助 Viewport + SliverList + SliverChildBuilderDelegate 按需创建列表项;列表项滑进屏幕时才构建,滑出很远后会被回收并可能 dispose,再滑回来通常会重新创建;如果用了 KeepAlive,则会被缓存保活而不是销毁。