ListView.builder

3 阅读7分钟

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 会做这些事:

  1. 调用 itemBuilder(context, index)
  2. 生成对应的 Widget
  3. 为它创建 Element
  4. 如果有 StatefulWidget,创建 State
  5. 创建对应的 RenderObject
  6. 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:之后又滑回来了

如果之前已经被回收,那么再滑回来时:

  1. itemBuilder 会再次被调用
  2. 会重新创建新的 Widget
  3. 重新创建新的 Element
  4. 如果是 StatefulWidget,重新创建新的 State
  5. 再次 initState
  6. 再次 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 自己内部,而是放在外部数据层,比如:

  • Provider
  • GetX
  • Bloc
  • Riverpod
  • 父组件的 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 高度固定,优先用:

  • itemExtent
  • prototypeItem

这样 Flutter 更容易快速算出哪些 item 该出现,性能更好。

列表会增删改顺序时要用稳定 Key

如果数据会插入、删除、重排,最好给每个 item 一个稳定 Key,否则可能出现状态串位。


10. 一句话总结

ListView.builder 的原理就是:基于滚动位置,借助 Viewport + SliverList + SliverChildBuilderDelegate 按需创建列表项;列表项滑进屏幕时才构建,滑出很远后会被回收并可能 dispose,再滑回来通常会重新创建;如果用了 KeepAlive,则会被缓存保活而不是销毁。