案例
代码
import 'package:flutter/material.dart';
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
import 'dart:io';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: RefreshDemoPage());
}
}
class RefreshDemoPage extends StatefulWidget {
const RefreshDemoPage({super.key});
@override
State<RefreshDemoPage> createState() => _RefreshDemoPageState();
}
class _RefreshDemoPageState extends State<RefreshDemoPage> {
final RefreshController _refreshController = RefreshController();
List<int> items = List.generate(20, (i) => i);
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
items = List.generate(20, (i) => i);
});
_refreshController.refreshCompleted();
}
Future<void> _onLoading() async {
await Future.delayed(const Duration(seconds: 2));
setState(() {
items.addAll(List.generate(10, (i) => items.length + i));
debugPrint('---------加载了10个新元素,items 长度 ${items.length}---------');
});
_refreshController.loadComplete();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('模拟第15项构建很慢')),
body: SmartRefresher(
controller: _refreshController,
enablePullDown: true,
enablePullUp: true,
onRefresh: _onRefresh,
onLoading: _onLoading,
header: const WaterDropHeader(),
footer: const ClassicFooter(),
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
debugPrint('开始构建 item $index, items 长度 ${items.length}');
if (index == 15) {
// ⚠️ 故意卡住主线程 10 秒钟
sleep(const Duration(seconds: 10));
}
return Container(
height: 80,
alignment: Alignment.center,
color: index == 15 ? Colors.red[200] : Colors.blue[100],
child: Text('Item $index', style: const TextStyle(fontSize: 20)),
);
},
),
),
);
}
}
程序刚启动
日志如下:
Restarted application in 1,837ms.
I/flutter ( 8681): 开始构建 item 0, items 长度 20
I/flutter ( 8681): 开始构建 item 1, items 长度 20
I/flutter ( 8681): 开始构建 item 2, items 长度 20
I/flutter ( 8681): 开始构建 item 3, items 长度 20
I/flutter ( 8681): 开始构建 item 4, items 长度 20
I/flutter ( 8681): 开始构建 item 5, items 长度 20
I/flutter ( 8681): 开始构建 item 6, items 长度 20
I/flutter ( 8681): 开始构建 item 7, items 长度 20
I/flutter ( 8681): 开始构建 item 8, items 长度 20
I/flutter ( 8681): 开始构建 item 9, items 长度 20
I/flutter ( 8681): 开始构建 item 10, items 长度 20
I/flutter ( 8681): 开始构建 item 11, items 长度 20
I/flutter ( 8681): 开始构建 item 0, items 长度 20
I/flutter ( 8681): 开始构建 item 1, items 长度 20
I/flutter ( 8681): 开始构建 item 2, items 长度 20
I/flutter ( 8681): 开始构建 item 3, items 长度 20
I/flutter ( 8681): 开始构建 item 4, items 长度 20
I/flutter ( 8681): 开始构建 item 5, items 长度 20
I/flutter ( 8681): 开始构建 item 6, items 长度 20
I/flutter ( 8681): 开始构建 item 7, items 长度 20
I/flutter ( 8681): 开始构建 item 8, items 长度 20
I/flutter ( 8681): 开始构建 item 9, items 长度 20
I/flutter ( 8681): 开始构建 item 10, items 长度 20
I/flutter ( 8681): 开始构建 item 11, items 长度 20
预构建
在屏幕上,我只能看到Item7,但是日志显示已经渲染到item11了。
这是因为Flutter 的渲染机制会「预构建屏幕外的子项」
ListView.builder是懒加载的,但会在可视区域上下都「多渲染一点」- 这样做是为了让用户滚动更平滑
- 所以虽然屏幕只显示到
item 7,但实际上item 8~11 已经预先构建了,只是暂时不在屏幕上显示。
这个「提前构建」的数量叫 cacheExtent(缓存范围)
- 默认值是
250.0像素 - 意味着距离当前可视区 250 像素以内的 item 也会被提前构建
测量构建
这是 Flutter 在首次渲染时的布局测量过程造成的:
- 第一次构建时,Flutter 不知道
ListView需要多高 - 它会 先快速试着构建一遍所有可见项(甚至多构建一些),计算它们的尺寸
- 知道尺寸后,再 进行一次真正的布局渲染,因此你会看到打印了两轮
这个行为是 SliverList 的特性:首次 layout 时会进行一次「测量构建」,测完再来一遍正式构建。
限制缓存范围
可以手动限制缓存范围,例如:
child: ListView.builder(
itemCount: items.length,
cacheExtent: 0, // 禁止预加载
itemBuilder: ...
)
这样首次就只会构建屏幕上真正可见的那些 item 了。
拉到15个Item
会停在item7这里,要等着预构建完成,才会允许向下滚动。
加载的时候
I/flutter ( 8681): 开始构建 item 12, items 长度 20
I/flutter ( 8681): 开始构建 item 13, items 长度 20
I/flutter ( 8681): 开始构建 item 14, items 长度 20
I/flutter ( 8681): 开始构建 item 15, items 长度 20
I/flutter ( 8681): 开始构建 item 16, items 长度 20
I/flutter ( 8681): 开始构建 item 17, items 长度 20
I/flutter ( 8681): 开始构建 item 18, items 长度 20
I/flutter ( 8681): 开始构建 item 19, items 长度 20
I/flutter ( 8681): ---------加载了10个新元素,items 长度 30---------
I/flutter ( 8681): 开始构建 item 9, items 长度 30
I/flutter ( 8681): 开始构建 item 10, items 长度 30
I/flutter ( 8681): 开始构建 item 11, items 长度 30
I/flutter ( 8681): 开始构建 item 12, items 长度 30
I/flutter ( 8681): 开始构建 item 13, items 长度 30
I/flutter ( 8681): 开始构建 item 14, items 长度 30
I/flutter ( 8681): 开始构建 item 15, items 长度 30
I/flutter ( 8681): 开始构建 item 16, items 长度 30
I/flutter ( 8681): 开始构建 item 17, items 长度 30
I/flutter ( 8681): 开始构建 item 18, items 长度 30
I/flutter ( 8681): 开始构建 item 19, items 长度 30
I/flutter ( 8681): 开始构建 item 20, items 长度 30
I/flutter ( 8681): 开始构建 item 21, items 长度 30
I/flutter ( 8681): 开始构建 item 22, items 长度 30
I/flutter ( 8681): 开始构建 item 23, items 长度 30
为什么不是「整页白屏重构」
由于执行了setState,Flutter会自动在执行Widget build(BuildContext context)方法,进而ListView.builder也从新执行了,重新构建了ListView。
但 Flutter 会复用已有元素树,只增量构建新增的 item,不会整页重绘,因此整个页面还停留在item 13到item 20的区间。
这是 Flutter 的 「Widget 是描述,Element 是持久化」 的机制
层级 说明 是否会重建 Widget 轻量配置对象(build 返回的) 每次 build 都会新建 Element Widget 的持久化实例,负责管理生命周期和状态 尽量复用,不会频繁销毁 RenderObject 负责绘制和布局的底层对象 尽量复用,不会频繁销毁
ListView.builder本身没有被销毁,只是重新 build 出一份新的 Widget 描述
ListView内部已经存在的Element/RenderObject会被复用只有新增加的那 10 个 item 会被创建新的元素树
所以:页面不会整页闪烁、白屏,只是局部 diff 更新
ListView的缓存区间
ListView.builder 只会懒加载(lazy build)当前屏幕附近的元素。当你滚动到 item13--item20 附近时,屏幕上实际可见的是 item13--item20,屏幕上方的 9--12 早就构建过、但可能被 SliverList 回收了。
当调用 setState 更新 items 数组长度从 20 → 30 时:
ListView会重新计算可视区域的元素索引。- 它会根据当前滚动位置,重新构建一段「缓存区间」的 item,比如从 item 9 开始,一直构建到 23,确保你向上/向下滚动都有数据可以马上显示。
┌─────────────────────────────────────┐
│ 屏幕外(已回收) │
│ item 0 item 1 ... item 8 │
├─────────────────────────────────────┤
│ 缓存区 (预构建区域) │
│ item 9 item 10 ... item 12 │
├─────────────────────────────────────┤
│ 可见区域 (当前屏幕) │
│ item 13 item 20 │
├─────────────────────────────────────┤
│ 缓存区 (预构建区域) │
│ item 21 item 22 item 23 │
├─────────────────────────────────────┤
│ 屏幕外(未构建) │
│ item 24 ... item 29 │
└─────────────────────────────────────┘
可见区域:就是当前屏幕能看到的那些 item。
缓存区(预构建区域):在可见区域的前后各有一段不可见区域,框架会提前把这些 item 构建出来并保留在内存中。
- 向上缓存:保证你往上快速滚动时不会卡顿。
- 向下缓存:保证你往下快速滚动时不会卡顿。
屏幕外区域:离得太远的 item 不会保留,会被销毁,等再次滚动回来时再重建。
可以通过
cacheExtent参数来控制缓存区的长度(单位是像素),例如:ListView.builder( cacheExtent: 500, // 缓存前后各 500 像素范围内的 item itemBuilder: ... )注意:
cacheExtent控制的是距离而不是item数量,实际缓存多少个 item 取决于 item 的高度
总结:ListView.builder 的懒加载机制
按需构建(on-demand build)
- 只会为当前屏幕可见区域 + 缓存区域内的 item 调用
itemBuilder。 - 未滚动到的 item 不会提前构建。
缓存区域(cacheExtent)
- 在可见区域的上下各有一段缓存区域,会预先构建 item。
- 默认值约为一屏的长度,可以通过
cacheExtent自定义。
回收与重建(reuse/rebuild)
- 离开缓存区的 item 会被销毁释放内存。
- 滚动回来时会重新调用
itemBuilder重建。
按需增量构建
- 随着滚动,逐步加载更多 item,不会一次性构建整个列表,提高性能。
无限列表支持
- 因为不会一次性创建所有 item,所以可以处理无限或超大列表。